Compare commits

...

55 Commits

Author SHA1 Message Date
Star Brilliant
475894baaa Update Changelog 2019-03-20 12:58:59 +08:00
qyb
2df81db465 log real client ip behind a HTTPS gateway (#38)
* log real client ip behind a HTTPS gateway

* fix tab/space indent

* better compatible for apache/nginx log default format

* add  config option
2019-03-16 05:36:52 +08:00
Sherlock Holo
871604f577 Add LVS weight round robin selector (#36)
* Add upstream selector, there are two selector now:
    - random selector
    - weight random selector

random selector will choose upstream at random; weight random selector will choose upstream at random with weight

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Rewrite config and config file example, prepare for weight round robbin selector

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Replace bad implement of weight random selector with weight round robbin selector, the algorithm is nginx weight round robbin like

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Use new config module

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Disable deprecated DualStack set

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Fix typo

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Optimize upstreamSelector judge

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Fix typo

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add config timeout unit tips

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Set wrr http client timeout to replace http request timeout

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add weight value range

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add a line ending for .gitignore

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Optimize config file style

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Modify Weight type to int32

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add upstreamError

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Rewrite Selector interface and wrr implement

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Use http module predefined constant to judge req.response.StatusCode

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Use Selector.ReportUpstreamError to report upstream error for evaluation loop in real time

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Make client selector field private

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Replace config file url to URL
Add miss space for 'weight= 50'

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Rewrite Selector.ReportUpstreamError to Selector.ReportUpstreamStatus, report upstream ok in real time

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Fix checkIETFResponse: if upstream OK, won't increase weight

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Fix typo

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Rewrite wrr evaluation, concurrent check upstream and reduce interval to 15s

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add lvs wrr selector config

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add DebugReporter interface, when client verbose is true and the selector implements it, will report all upstream weights every 15s

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Rename WeightRoundRobinSelector to NginxWRRSelector

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add LVSSelector

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Remove useless log

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>
2019-03-13 14:52:54 +08:00
Star Brilliant
a400f03960 Bump version to 2.0.0 2019-03-09 19:10:30 +08:00
Star Brilliant
7839eed014 Update build scripts 2019-03-09 19:09:35 +08:00
Star Brilliant
0f35971118 Replace Url with URL 2019-03-09 19:05:07 +08:00
Sherlock Holo
fec1e84d5e Add backend weight round robin select (#34)
* Add upstream selector, there are two selector now:
    - random selector
    - weight random selector

random selector will choose upstream at random; weight random selector will choose upstream at random with weight

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Rewrite config and config file example, prepare for weight round robbin selector

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Replace bad implement of weight random selector with weight round robbin selector, the algorithm is nginx weight round robbin like

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Use new config module

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Disable deprecated DualStack set

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Fix typo

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Optimize upstreamSelector judge

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Fix typo

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add config timeout unit tips

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Set wrr http client timeout to replace http request timeout

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add weight value range

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add a line ending for .gitignore

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Optimize config file style

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Modify Weight type to int32

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Add upstreamError

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Rewrite Selector interface and wrr implement

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Use http module predefined constant to judge req.response.StatusCode

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Use Selector.ReportUpstreamError to report upstream error for evaluation loop in real time

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Make client selector field private

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Replace config file url to URL
Add miss space for 'weight= 50'

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Rewrite Selector.ReportUpstreamError to Selector.ReportUpstreamStatus, report upstream ok in real time

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Fix checkIETFResponse: if upstream OK, won't increase weight

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>

* Fix typo

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>
2019-03-09 18:12:44 +08:00
Star Brilliant
8f2004d1de Bump to version 1.4.3 2018-12-05 15:57:51 +08:00
Star Brilliant
a3f4468325 Release 1.4.2 2018-12-05 15:57:25 +08:00
Star Brilliant
fa2bcf74a9 Remove dns.ErrTruncated according to https://github.com/miekg/dns/pull/815 2018-11-28 15:31:02 +08:00
Star Brilliant
01d60df9cd Merge pull request #30 from Sherlock-Holo/master
Refine runtime.GOOS check, use switch case to replace a long if
2018-11-28 00:11:52 +08:00
Sherlock Holo
4c0cae7111 Refine runtime.GOOS check, use switch case to replace a long if 2018-11-28 00:08:21 +08:00
Star Brilliant
95fe3e3b4e Use time.Since to replace time.Now().Sub 2018-11-27 20:18:30 +08:00
Star Brilliant
35ddf43505 Add PID file support 2018-11-27 17:37:57 +08:00
Star Brilliant
3083b668ca Remove an item from Changelog since it was actually fixed in eariler version 2018-11-10 23:02:41 +08:00
Star Brilliant
dd8ea973f4 Correct spelling 2018-11-10 23:01:46 +08:00
Star Brilliant
0df0002e6b Bump version to 1.4.2 2018-11-10 23:00:06 +08:00
Star Brilliant
3affb2c227 Release 1.4.1 2018-11-10 22:59:37 +08:00
Star Brilliant
7c7b7d969d Add detectportal.firefox.com to default passthrough list 2018-11-08 23:58:28 +08:00
Star Brilliant
4754aa0414 Enable CORS by default, which is necessary for AJAX resolver to run 2018-11-07 20:13:51 +08:00
Star Brilliant
2d9c9eba50 Detect context.DeadlineExceeded 2018-11-07 19:41:55 +08:00
Star Brilliant
c51be0e69c Use context for more functions 2018-11-07 19:25:46 +08:00
Star Brilliant
95ec839409 Put cancel() earlier 2018-11-07 19:10:06 +08:00
Star Brilliant
502fe6b048 Use RCODE_REFUSED for unsupported Qclass 2018-11-07 18:56:22 +08:00
Star Brilliant
f8b40c4bfc Try to use context.WithTimeout to detect HTTP timeout. Hopefully it might work. 2018-11-07 18:47:01 +08:00
Star Brilliant
bb1e21778a Slightly change the log format 2018-11-07 18:11:12 +08:00
Star Brilliant
afa0d563d0 Add passthrough feature, tests are welcome 2018-11-07 17:10:39 +08:00
Star Brilliant
017a18f20c Fix HTTP stream leaking problem 2018-11-06 14:46:45 +08:00
Star Brilliant
0577ff6dca Merge pull request #28 from Chaz6/patch-1
doh-server: change to google.go
2018-11-02 10:48:33 +08:00
Chris Hills
ef2c6bbdc8 Update google.go
Make "cd" check case-insensitive.
2018-11-01 20:12:28 +00:00
Chris Hills
4d742bd15e doh-server: change to google.go
Allow the "cd" parameter to be case insensitive to work with some clients that send True/False instead of true/false such as gDNS.
2018-10-31 23:40:33 +00:00
Star Brilliant
3b112b946e Congratulations RFC 8484, remove the word "draft" from Readme 2018-10-20 13:59:37 +08:00
Star Brilliant
6d19cbb9ad Congratulations RFC 8484, remove the word "draft" from Readme 2018-10-20 13:58:26 +08:00
Star Brilliant
b094a8d4fd Update Readme, fix issue #27 2018-10-04 23:03:51 +08:00
Star Brilliant
c1f6fe1997 Update Readme 2018-10-04 02:12:55 +08:00
Star Brilliant
1fb3ed3513 Add a ink to a guide 2018-10-04 02:11:55 +08:00
Star Brilliant
c85ef45840 Fix panic with debug_http_headers 2018-09-27 16:46:36 +08:00
Star Brilliant
85d81d3d0b Merge pull request #22 from paulie-g/master
Fix segfault when no_cookies=true
2018-09-24 03:21:00 +10:00
Paul G
ab0eddb0ba Fix segfault when no_cookies=true 2018-09-23 08:25:15 -04:00
Star Brilliant
aa3389b1d0 Build doh-logger with static libswiftCore, fix #20 2018-09-22 04:28:12 +08:00
Star Brilliant
6eb7b29142 Add configuration option: debug_http_headers 2018-09-22 04:23:55 +08:00
Star Brilliant
ea0a769389 Bump version to 1.3.11 2018-08-21 01:44:56 +08:00
Star Brilliant
e480251e67 Release 1.3.10 2018-08-21 01:44:35 +08:00
Star Brilliant
027480afeb Enable application/dns-message (draft-13) by default, since Google has finally supported it 2018-08-21 01:43:46 +08:00
Star Brilliant
4839498ad5 Move linux-install.* to contrib/ 2018-08-14 09:11:09 +08:00
Star Brilliant
a303c21036 Bump version to 1.3.10 2018-08-14 09:08:46 +08:00
Star Brilliant
3586688aa6 Release 1.3.9 2018-08-14 09:08:27 +08:00
Star Brilliant
ffe5573552 Change the ECS prefix length from /48 to /56 for IPv6, per RFC 7871 2018-08-14 09:06:13 +08:00
Star Brilliant
f40116b1f8 Update Readme to instruct Debian users to set $GOROOT 2018-08-14 01:43:41 +08:00
Star Brilliant
58e6cdfb71 If $GOROOT is defined, Makefile should respect the value, fix #8 2018-08-14 01:37:19 +08:00
Star Brilliant
1491138f69 Add 5380 as an additional default doh-client port 2018-08-10 03:50:38 +08:00
Star Brilliant
83df8964d8 Fix #16: doh-client panics when connecting no_cookies = true 2018-07-04 22:43:08 +08:00
Star Brilliant
07f39088d4 Update example configuration 2018-07-02 20:42:11 +08:00
Star Brilliant
db007fbded Update example configuration 2018-07-02 20:40:56 +08:00
Star Brilliant
89d809d469 Bump version to 1.3.9 2018-07-02 20:12:04 +08:00
31 changed files with 1318 additions and 231 deletions

2
.gitignore vendored
View File

@@ -12,3 +12,5 @@
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/ .glide/
.idea/

View File

@@ -4,6 +4,40 @@ This Changelog records major changes between versions.
Not all changes are recorded. Please check git log for details. Not all changes are recorded. Please check git log for details.
## Version 2.0.0
**This is a breaking change!** Please update the configuration file after upgrading.
- Implemented two upstream server selector algorithms: `weighted_round_robin` and `lvs_weighted_round_robin`.
- Add a configuration option for doh-server: `log_guessed_client_ip`.
## Version 1.4.2
- Add PID file feature for systems which lacks a cgroup-based process tracker.
- Remove dns.ErrTruncated according to <https://github.com/miekg/dns/pull/815>.
## Version 1.4.1
- Add a configuration option: `debug_http_headers` (e.g. Add `CF-Ray` to diagnose Cloudflare's resolver)
- Add a configuration option: `passrthrough`
- macOS logger is rebuilt with static libswiftCore
- Fix HTTP stream leaking problem, which may cause massive half-open connections if HTTP/1 is in use
- Utilize Go's cancelable context to detect timeouts more reliably.
- Fix interoperation problems with gDNS
- CORS is enabled by default in doh-server
- Documentation updates
## Version 1.3.10
- Enable application/dns-message (draft-13) by default, since Google has finally supported it
## Version 1.3.9
- Fix client crash with `no_cookies = true`
- Add 5380 as an additional default doh-client port
- If `$GOROOT` is defined, Makefile now respects the value for the convenience of Debian/Ubuntu users
- Change the ECS prefix length from /48 to /56 for IPv6, per RFC 7871
## Version 1.3.8 ## Version 1.3.8
- Workaround a bug causing Firefox 61-62 to reject responses with Content-Type = application/dns-message - Workaround a bug causing Firefox 61-62 to reject responses with Content-Type = application/dns-message

View File

@@ -1,13 +1,21 @@
.PHONY: all clean install uninstall deps .PHONY: all clean install uninstall deps
GOBUILD=go build PREFIX = /usr/local
GOGET=go get -d -v
GOGET_UPDATE=go get -d -u -v ifeq ($(GOROOT),)
PREFIX=/usr/local GOBUILD = go build
ifeq ($(shell uname),Darwin) GOGET = go get -d -v
CONFDIR=/usr/local/etc/dns-over-https GOGET_UPDATE = go get -d -u -v
else else
CONFDIR=/etc/dns-over-https GOBUILD = $(GOROOT)/bin/go build
GOGET = $(GOROOT)/bin/go get -d -v
GOGET_UPDATE = $(GOROOT)/bin/go get -d -u -v
endif
ifeq ($(shell uname),Darwin)
CONFDIR = /usr/local/etc/dns-over-https
else
CONFDIR = /etc/dns-over-https
endif endif
all: doh-client/doh-client doh-server/doh-server all: doh-client/doh-client doh-server/doh-server
@@ -54,7 +62,7 @@ deps:
$(GOGET_UPDATE) github.com/m13253/dns-over-https/json-dns $(GOGET_UPDATE) github.com/m13253/dns-over-https/json-dns
$(GOGET) ./doh-client ./doh-server $(GOGET) ./doh-client ./doh-server
doh-client/doh-client: deps doh-client/client.go doh-client/config.go doh-client/google.go doh-client/ietf.go doh-client/main.go doh-client/version.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go doh-client/doh-client: deps doh-client/client.go doh-client/config/config.go doh-client/google.go doh-client/ietf.go doh-client/main.go doh-client/version.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go
cd doh-client && $(GOBUILD) cd doh-client && $(GOBUILD)
doh-server/doh-server: deps doh-server/config.go doh-server/google.go doh-server/ietf.go doh-server/main.go doh-server/server.go doh-server/version.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go doh-server/doh-server: deps doh-server/config.go doh-server/google.go doh-server/ietf.go doh-server/main.go doh-server/server.go doh-server/version.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go

View File

@@ -2,11 +2,17 @@ DNS-over-HTTPS
============== ==============
Client and server software to query DNS over HTTPS, using [Google DNS-over-HTTPS protocol](https://developers.google.com/speed/public-dns/docs/dns-over-https) Client and server software to query DNS over HTTPS, using [Google DNS-over-HTTPS protocol](https://developers.google.com/speed/public-dns/docs/dns-over-https)
and [draft-ietf-doh-dns-over-https](https://github.com/dohwg/draft-ietf-doh-dns-over-https). and [IETF DNS-over-HTTPS (RFC 8484)](https://www.rfc-editor.org/rfc/rfc8484.txt).
## Easy start ## Guide
Install [Go](https://golang.org), at least version 1.9. [Tutorial to setup your own DNS-over-HTTPS (DoH) server](https://www.aaflalo.me/2018/10/tutorial-setup-dns-over-https-server/). (Thanks to Antoine Aflalo)
## Installing
Install [Go](https://golang.org), at least version 1.10.
(Note for Debian/Ubuntu users: You need to set `$GOROOT` if you could not get your new version of Go selected by the Makefile.)
First create an empty directory, used for `$GOPATH`: First create an empty directory, used for `$GOPATH`:
@@ -83,7 +89,7 @@ records.
## EDNS0-Client-Subnet (GeoDNS) ## EDNS0-Client-Subnet (GeoDNS)
DNS-over-HTTPS supports EDNS0-Client-Subnet protocol, which submits part of the DNS-over-HTTPS supports EDNS0-Client-Subnet protocol, which submits part of the
client's IP address (/24 for IPv4, /48 for IPv6 by default) to the upstream client's IP address (/24 for IPv4, /56 for IPv6 by default) to the upstream
server. This is useful for GeoDNS and CDNs to work, and is exactly the same server. This is useful for GeoDNS and CDNs to work, and is exactly the same
configuration as most public DNS servers. configuration as most public DNS servers.
@@ -107,11 +113,9 @@ except for absolute expire time is preferred to relative TTL value. Refer to
[json-dns/response.go](json-dns/response.go) for a complete description of the [json-dns/response.go](json-dns/response.go) for a complete description of the
API. API.
### IETF DNS-over-HTTPS Protocol (Draft) ### IETF DNS-over-HTTPS Protocol
DNS-over-HTTPS uses a protocol compatible to [draft-ietf-doh-dns-over-https](https://github.com/dohwg/draft-ietf-doh-dns-over-https). DNS-over-HTTPS uses a protocol compatible to [IETF DNS-over-HTTPS (RFC 8484)](https://www.rfc-editor.org/rfc/rfc8484.txt).
This protocol is in draft stage. Any incompatibility may be introduced before
it is finished.
### Supported features ### Supported features
@@ -119,7 +123,7 @@ Currently supported features are:
- [X] IPv4 / IPv6 - [X] IPv4 / IPv6
- [X] EDNS0 large UDP packet (4 KiB by default) - [X] EDNS0 large UDP packet (4 KiB by default)
- [X] EDNS0-Client-Subnet (/24 for IPv4, /48 for IPv6 by default) - [X] EDNS0-Client-Subnet (/24 for IPv4, /56 for IPv6 by default)
## The name of the project ## The name of the project

View File

@@ -6,7 +6,7 @@ PREFIX = /usr/local
all: doh-logger all: doh-logger
doh-logger: doh-logger.swift doh-logger: doh-logger.swift
$(SWIFTC) -o $@ -O $< $(SWIFTC) -o $@ -O -static-stdlib $<
clean: clean:
rm -f doh-logger rm -f doh-logger

View File

@@ -25,31 +25,41 @@ package main
import ( import (
"context" "context"
"fmt"
"log" "log"
"math/rand" "math/rand"
"net" "net"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/m13253/dns-over-https/doh-client/config"
"github.com/m13253/dns-over-https/doh-client/selector"
"github.com/m13253/dns-over-https/json-dns" "github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/idna"
) )
type Client struct { type Client struct {
conf *config conf *config.Config
bootstrap []string bootstrap []string
passthrough []string
udpClient *dns.Client
tcpClient *dns.Client
udpServers []*dns.Server udpServers []*dns.Server
tcpServers []*dns.Server tcpServers []*dns.Server
bootstrapResolver *net.Resolver bootstrapResolver *net.Resolver
cookieJar *cookiejar.Jar cookieJar http.CookieJar
httpClientMux *sync.RWMutex httpClientMux *sync.RWMutex
httpTransport *http.Transport httpTransport *http.Transport
httpClient *http.Client httpClient *http.Client
httpClientLastCreate time.Time httpClientLastCreate time.Time
selector selector.Selector
} }
type DNSRequest struct { type DNSRequest struct {
@@ -62,13 +72,22 @@ type DNSRequest struct {
err error err error
} }
func NewClient(conf *config) (c *Client, err error) { func NewClient(conf *config.Config) (c *Client, err error) {
c = &Client{ c = &Client{
conf: conf, conf: conf,
} }
udpHandler := dns.HandlerFunc(c.udpHandlerFunc) udpHandler := dns.HandlerFunc(c.udpHandlerFunc)
tcpHandler := dns.HandlerFunc(c.tcpHandlerFunc) tcpHandler := dns.HandlerFunc(c.tcpHandlerFunc)
c.udpClient = &dns.Client{
Net: "udp",
UDPSize: dns.DefaultMsgSize,
Timeout: time.Duration(conf.Other.Timeout) * time.Second,
}
c.tcpClient = &dns.Client{
Net: "tcp",
Timeout: time.Duration(conf.Other.Timeout) * time.Second,
}
for _, addr := range conf.Listen { for _, addr := range conf.Listen {
c.udpServers = append(c.udpServers, &dns.Server{ c.udpServers = append(c.udpServers, &dns.Server{
Addr: addr, Addr: addr,
@@ -83,9 +102,9 @@ func NewClient(conf *config) (c *Client, err error) {
}) })
} }
c.bootstrapResolver = net.DefaultResolver c.bootstrapResolver = net.DefaultResolver
if len(conf.Bootstrap) != 0 { if len(conf.Other.Bootstrap) != 0 {
c.bootstrap = make([]string, len(conf.Bootstrap)) c.bootstrap = make([]string, len(conf.Other.Bootstrap))
for i, bootstrap := range conf.Bootstrap { for i, bootstrap := range conf.Other.Bootstrap {
bootstrapAddr, err := net.ResolveUDPAddr("udp", bootstrap) bootstrapAddr, err := net.ResolveUDPAddr("udp", bootstrap)
if err != nil { if err != nil {
bootstrapAddr, err = net.ResolveUDPAddr("udp", "["+bootstrap+"]:53") bootstrapAddr, err = net.ResolveUDPAddr("udp", "["+bootstrap+"]:53")
@@ -105,38 +124,120 @@ func NewClient(conf *config) (c *Client, err error) {
return conn, err return conn, err
}, },
} }
if len(conf.Other.Passthrough) != 0 {
c.passthrough = make([]string, len(conf.Other.Passthrough))
for i, passthrough := range conf.Other.Passthrough {
if punycode, err := idna.ToASCII(passthrough); err != nil {
passthrough = punycode
}
c.passthrough[i] = "." + strings.ToLower(strings.Trim(passthrough, ".")) + "."
}
}
} }
// Most CDNs require Cookie support to prevent DDoS attack. // Most CDNs require Cookie support to prevent DDoS attack.
// Disabling Cookie does not effectively prevent tracking, // Disabling Cookie does not effectively prevent tracking,
// so I will leave it on to make anti-DDoS services happy. // so I will leave it on to make anti-DDoS services happy.
if !c.conf.NoCookies { if !c.conf.Other.NoCookies {
c.cookieJar, err = cookiejar.New(nil) c.cookieJar, err = cookiejar.New(nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else {
c.cookieJar = nil
} }
c.httpClientMux = new(sync.RWMutex) c.httpClientMux = new(sync.RWMutex)
err = c.newHTTPClient() err = c.newHTTPClient()
if err != nil { if err != nil {
return nil, err return nil, err
} }
switch c.conf.Upstream.UpstreamSelector {
case config.NginxWRR:
if c.conf.Other.Verbose {
log.Println(config.NginxWRR, " mode start")
}
s := selector.NewNginxWRRSelector(time.Duration(c.conf.Other.Timeout) * time.Second)
for _, u := range c.conf.Upstream.UpstreamGoogle {
if err := s.Add(u.URL, selector.Google, u.Weight); err != nil {
return nil, err
}
}
for _, u := range c.conf.Upstream.UpstreamIETF {
if err := s.Add(u.URL, selector.IETF, u.Weight); err != nil {
return nil, err
}
}
c.selector = s
case config.LVSWRR:
if c.conf.Other.Verbose {
log.Println(config.LVSWRR, " mode start")
}
s := selector.NewLVSWRRSelector(time.Duration(c.conf.Other.Timeout) * time.Second)
for _, u := range c.conf.Upstream.UpstreamGoogle {
if err := s.Add(u.URL, selector.Google, u.Weight); err != nil {
return nil, err
}
}
for _, u := range c.conf.Upstream.UpstreamIETF {
if err := s.Add(u.URL, selector.IETF, u.Weight); err != nil {
return nil, err
}
}
c.selector = s
default:
if c.conf.Other.Verbose {
log.Println(config.Random, " mode start")
}
// if selector is invalid or random, use random selector, or should we stop program and let user knows he is wrong?
s := selector.NewRandomSelector()
for _, u := range c.conf.Upstream.UpstreamGoogle {
if err := s.Add(u.URL, selector.Google); err != nil {
return nil, err
}
}
for _, u := range c.conf.Upstream.UpstreamIETF {
if err := s.Add(u.URL, selector.IETF); err != nil {
return nil, err
}
}
c.selector = s
}
if c.conf.Other.Verbose {
if reporter, ok := c.selector.(selector.DebugReporter); ok {
reporter.ReportWeights()
}
}
return c, nil return c, nil
} }
func (c *Client) newHTTPClient() error { func (c *Client) newHTTPClient() error {
c.httpClientMux.Lock() c.httpClientMux.Lock()
defer c.httpClientMux.Unlock() defer c.httpClientMux.Unlock()
if !c.httpClientLastCreate.IsZero() && time.Now().Sub(c.httpClientLastCreate) < time.Duration(c.conf.Timeout)*time.Second { if !c.httpClientLastCreate.IsZero() && time.Since(c.httpClientLastCreate) < time.Duration(c.conf.Other.Timeout)*time.Second {
return nil return nil
} }
if c.httpTransport != nil { if c.httpTransport != nil {
c.httpTransport.CloseIdleConnections() c.httpTransport.CloseIdleConnections()
} }
dialer := &net.Dialer{ dialer := &net.Dialer{
Timeout: time.Duration(c.conf.Timeout) * time.Second, Timeout: time.Duration(c.conf.Other.Timeout) * time.Second,
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
DualStack: true, // DualStack: true,
Resolver: c.bootstrapResolver, Resolver: c.bootstrapResolver,
} }
c.httpTransport = &http.Transport{ c.httpTransport = &http.Transport{
DialContext: dialer.DialContext, DialContext: dialer.DialContext,
@@ -145,10 +246,9 @@ func (c *Client) newHTTPClient() error {
MaxIdleConns: 100, MaxIdleConns: 100,
MaxIdleConnsPerHost: 10, MaxIdleConnsPerHost: 10,
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
ResponseHeaderTimeout: time.Duration(c.conf.Timeout) * time.Second, TLSHandshakeTimeout: time.Duration(c.conf.Other.Timeout) * time.Second,
TLSHandshakeTimeout: time.Duration(c.conf.Timeout) * time.Second,
} }
if c.conf.NoIPv6 { if c.conf.Other.NoIPv6 {
c.httpTransport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { c.httpTransport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
if strings.HasPrefix(network, "tcp") { if strings.HasPrefix(network, "tcp") {
network = "tcp4" network = "tcp4"
@@ -180,6 +280,9 @@ func (c *Client) Start() error {
}(srv) }(srv)
} }
// start evaluation loop
c.selector.StartEvaluate()
for i := 0; i < cap(results); i++ { for i := 0; i < cap(results); i++ {
err := <-results err := <-results
if err != nil { if err != nil {
@@ -187,65 +290,152 @@ func (c *Client) Start() error {
} }
} }
close(results) close(results)
return nil return nil
} }
func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) { func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
if r.Response == true { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.conf.Other.Timeout)*time.Second)
defer cancel()
if r.Response {
log.Println("Received a response packet") log.Println("Received a response packet")
return return
} }
requestType := "" if len(r.Question) != 1 {
if len(c.conf.UpstreamIETF) == 0 { log.Println("Number of questions is not 1")
requestType = "application/dns-json" reply := jsonDNS.PrepareReply(r)
} else if len(c.conf.UpstreamGoogle) == 0 { reply.Rcode = dns.RcodeFormatError
requestType = "application/dns-message" w.WriteMsg(reply)
return
}
question := &r.Question[0]
questionName := question.Name
questionClass := ""
if qclass, ok := dns.ClassToString[question.Qclass]; ok {
questionClass = qclass
} else { } else {
numServers := len(c.conf.UpstreamGoogle) + len(c.conf.UpstreamIETF) questionClass = strconv.FormatUint(uint64(question.Qclass), 10)
random := rand.Intn(numServers) }
if random < len(c.conf.UpstreamGoogle) { questionType := ""
requestType = "application/dns-json" if qtype, ok := dns.TypeToString[question.Qtype]; ok {
} else { questionType = qtype
requestType = "application/dns-message" } else {
questionType = strconv.FormatUint(uint64(question.Qtype), 10)
}
if c.conf.Other.Verbose {
fmt.Printf("%s - - [%s] \"%s %s %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType)
}
shouldPassthrough := false
passthroughQuestionName := questionName
if punycode, err := idna.ToASCII(passthroughQuestionName); err != nil {
passthroughQuestionName = punycode
}
passthroughQuestionName = "." + strings.ToLower(strings.Trim(passthroughQuestionName, ".")) + "."
for _, passthrough := range c.passthrough {
if strings.HasSuffix(passthroughQuestionName, passthrough) {
shouldPassthrough = true
break
} }
} }
if shouldPassthrough {
numServers := len(c.bootstrap)
upstream := c.bootstrap[rand.Intn(numServers)]
log.Printf("Request \"%s %s %s\" is passed through %s.\n", questionName, questionClass, questionType, upstream)
var reply *dns.Msg
var err error
if !isTCP {
reply, _, err = c.udpClient.Exchange(r, upstream)
} else {
reply, _, err = c.tcpClient.Exchange(r, upstream)
}
if err == nil {
w.WriteMsg(reply)
return
}
log.Println(err)
reply = jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
upstream := c.selector.Get()
requestType := upstream.RequestType
if c.conf.Other.Verbose {
log.Println("choose upstream:", upstream)
}
var req *DNSRequest var req *DNSRequest
if requestType == "application/dns-json" { switch requestType {
req = c.generateRequestGoogle(w, r, isTCP) case "application/dns-json":
} else if requestType == "application/dns-message" { req = c.generateRequestGoogle(ctx, w, r, isTCP, upstream)
req = c.generateRequestIETF(w, r, isTCP)
} else { case "application/dns-message":
req = c.generateRequestIETF(ctx, w, r, isTCP, upstream)
default:
panic("Unknown request Content-Type") panic("Unknown request Content-Type")
} }
if req.err != nil { if req.err != nil {
if urlErr, ok := req.err.(*url.Error); ok {
// should we only check timeout?
if urlErr.Timeout() {
c.selector.ReportUpstreamStatus(upstream, selector.Timeout)
}
}
return return
} }
contentType := "" // if req.err == nil, req.response != nil
candidateType := strings.SplitN(req.response.Header.Get("Content-Type"), ";", 2)[0] defer req.response.Body.Close()
if candidateType == "application/json" {
contentType = "application/json" for _, header := range c.conf.Other.DebugHTTPHeaders {
} else if candidateType == "application/dns-message" { if value := req.response.Header.Get(header); value != "" {
contentType = "application/dns-message" log.Printf("%s: %s\n", header, value)
} else if candidateType == "application/dns-udpwireformat" {
contentType = "application/dns-message"
} else {
if requestType == "application/dns-json" {
contentType = "application/json"
} else if requestType == "application/dns-message" {
contentType = "application/dns-message"
} }
} }
if contentType == "application/json" { candidateType := strings.SplitN(req.response.Header.Get("Content-Type"), ";", 2)[0]
c.parseResponseGoogle(w, r, isTCP, req)
} else if contentType == "application/dns-message" { switch candidateType {
c.parseResponseIETF(w, r, isTCP, req) case "application/json":
} else { c.parseResponseGoogle(ctx, w, r, isTCP, req)
panic("Unknown response Content-Type")
case "application/dns-message", "application/dns-udpwireformat":
c.parseResponseIETF(ctx, w, r, isTCP, req)
default:
switch requestType {
case "application/dns-json":
c.parseResponseGoogle(ctx, w, r, isTCP, req)
case "application/dns-message":
c.parseResponseIETF(ctx, w, r, isTCP, req)
default:
panic("Unknown response Content-Type")
}
}
// https://developers.cloudflare.com/1.1.1.1/dns-over-https/request-structure/ says
// returns code will be 200 / 400 / 413 / 415 / 504, some server will return 503, so
// I think if status code is 5xx, upstream must has some problems
/*if req.response.StatusCode/100 == 5 {
c.selector.ReportUpstreamStatus(upstream, selector.Medium)
}*/
switch req.response.StatusCode / 100 {
case 5:
c.selector.ReportUpstreamStatus(upstream, selector.Error)
case 2:
c.selector.ReportUpstreamStatus(upstream, selector.OK)
} }
} }
@@ -259,12 +449,12 @@ func (c *Client) tcpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) {
var ( var (
ipv4Mask24 = net.IPMask{255, 255, 255, 0} ipv4Mask24 = net.IPMask{255, 255, 255, 0}
ipv6Mask48 = net.CIDRMask(48, 128) ipv6Mask56 = net.CIDRMask(56, 128)
) )
func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddress net.IP, ednsClientNetmask uint8) { func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddress net.IP, ednsClientNetmask uint8) {
ednsClientNetmask = 255 ednsClientNetmask = 255
if c.conf.NoECS { if c.conf.Other.NoECS {
return net.IPv4(0, 0, 0, 0), 0 return net.IPv4(0, 0, 0, 0), 0
} }
if opt := r.IsEdns0(); opt != nil { if opt := r.IsEdns0(); opt != nil {
@@ -286,8 +476,8 @@ func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddre
ednsClientAddress = ipv4.Mask(ipv4Mask24) ednsClientAddress = ipv4.Mask(ipv4Mask24)
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientAddress = ip.Mask(ipv6Mask48) ednsClientAddress = ip.Mask(ipv6Mask56)
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
} }
return return

View File

@@ -21,7 +21,7 @@
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
*/ */
package main package config
import ( import (
"fmt" "fmt"
@@ -29,20 +29,42 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
type config struct { const (
Listen []string `toml:"listen"` Random = "random"
UpstreamGoogle []string `toml:"upstream_google"` NginxWRR = "weighted_round_robin"
UpstreamIETF []string `toml:"upstream_ietf"` LVSWRR = "lvs_weighted_round_robin"
Bootstrap []string `toml:"bootstrap"` )
Timeout uint `toml:"timeout"`
NoCookies bool `toml:"no_cookies"` type upstreamDetail struct {
NoECS bool `toml:"no_ecs"` URL string `toml:"url"`
NoIPv6 bool `toml:"no_ipv6"` Weight int32 `toml:"weight"`
Verbose bool `toml:"verbose"`
} }
func loadConfig(path string) (*config, error) { type upstream struct {
conf := &config{} UpstreamGoogle []upstreamDetail `toml:"upstream_google"`
UpstreamIETF []upstreamDetail `toml:"upstream_ietf"`
UpstreamSelector string `toml:"upstream_selector"` // usable: random or weighted_random
}
type others struct {
Bootstrap []string `toml:"bootstrap"`
Passthrough []string `toml:"passthrough"`
Timeout uint `toml:"timeout"`
NoCookies bool `toml:"no_cookies"`
NoECS bool `toml:"no_ecs"`
NoIPv6 bool `toml:"no_ipv6"`
Verbose bool `toml:"verbose"`
DebugHTTPHeaders []string `toml:"debug_http_headers"`
}
type Config struct {
Listen []string `toml:"listen"`
Upstream upstream `toml:"upstream"`
Other others `toml:"others"`
}
func LoadConfig(path string) (*Config, error) {
conf := &Config{}
metaData, err := toml.DecodeFile(path, conf) metaData, err := toml.DecodeFile(path, conf)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -54,11 +76,15 @@ func loadConfig(path string) (*config, error) {
if len(conf.Listen) == 0 { if len(conf.Listen) == 0 {
conf.Listen = []string{"127.0.0.1:53", "[::1]:53"} conf.Listen = []string{"127.0.0.1:53", "[::1]:53"}
} }
if len(conf.UpstreamGoogle) == 0 && len(conf.UpstreamIETF) == 0 { if len(conf.Upstream.UpstreamGoogle) == 0 && len(conf.Upstream.UpstreamIETF) == 0 {
conf.UpstreamGoogle = []string{"https://dns.google.com/resolve"} conf.Upstream.UpstreamGoogle = []upstreamDetail{{URL: "https://dns.google.com/resolve", Weight: 50}}
} }
if conf.Timeout == 0 { if conf.Other.Timeout == 0 {
conf.Timeout = 10 conf.Other.Timeout = 10
}
if conf.Upstream.UpstreamSelector == "" {
conf.Upstream.UpstreamSelector = Random
} }
return conf, nil return conf, nil

View File

@@ -1,44 +1,59 @@
# DNS listen port # DNS listen port
listen = [ listen = [
"127.0.0.1:53", "127.0.0.1:53",
"127.0.0.1:5380",
"[::1]:53", "[::1]:53",
"[::1]:5380",
] ]
# HTTP path for upstream resolver # HTTP path for upstream resolver
# If multiple servers are specified, a random one will be chosen each time.
upstream_google = [
# Google's productive resolver, good ECS, bad DNSSEC [upstream]
"https://dns.google.com/resolve",
# CloudFlare's resolver, bad ECS, good DNSSEC # available selector: random or weighted_round_robin or lvs_weighted_round_robin
#"https://cloudflare-dns.com/dns-query", upstream_selector = "random"
#"https://1.1.1.1/dns-query",
#"https://1.0.0.1/dns-query",
# CloudFlare's resolver for Tor, available only with Tor # weight should in (0, 100], if upstream_selector is random, weight will be ignored
# Remember to disable ECS below when using Tor!
# Blog: https://blog.cloudflare.com/welcome-hidden-resolver/
#"https://dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion/dns-query",
] ## Google's productive resolver, good ECS, bad DNSSEC
upstream_ietf = [ #[[upstream.upstream_google]]
# url = "https://dns.google.com/resolve"
# weight = 50
# Google's experimental resolver, good ECS, good DNSSEC ## CloudFlare's resolver, bad ECS, good DNSSEC
#"https://dns.google.com/experimental", #[[upstream.upstream_google]]
# url = "https://cloudflare-dns.com/dns-query"
# weight = 50
# CloudFlare's resolver, bad ECS, good DNSSEC ## CloudFlare's resolver, bad ECS, good DNSSEC
#"https://cloudflare-dns.com/dns-query", #[[upstream.upstream_google]]
#"https://1.1.1.1/dns-query", # url = "https://1.1.1.1/dns-query"
#"https://1.0.0.1/dns-query", # weight = 50
# CloudFlare's resolver for Tor, available only with Tor # CloudFlare's resolver, bad ECS, good DNSSEC
# Remember to disable ECS below when using Tor! [[upstream.upstream_ietf]]
# Blog: https://blog.cloudflare.com/welcome-hidden-resolver/ url = "https://cloudflare-dns.com/dns-query"
#"https://dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion/dns-query", weight = 50
] ## CloudFlare's resolver, bad ECS, good DNSSEC
#[[upstream.upstream_ietf]]
# url = "https://1.1.1.1/dns-query"
# weight = 50
## Google's experimental resolver, good ECS, good DNSSEC
#[[upstream.upstream_ietf]]
# url = "https://dns.google.com/experimental"
# weight = 50
## CloudFlare's resolver for Tor, available only with Tor
## Remember to disable ECS below when using Tor!
## Blog: https://blog.cloudflare.com/welcome-hidden-resolver/
#[[upstream.upstream_ietf]]
# url = "https://dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion/dns-query"
# weight = 50
[others]
# Bootstrap DNS server to resolve the address of the upstream resolver # Bootstrap DNS server to resolve the address of the upstream resolver
# If multiple servers are specified, a random one will be chosen each time. # If multiple servers are specified, a random one will be chosen each time.
# If empty, use the system DNS settings. # If empty, use the system DNS settings.
@@ -56,7 +71,25 @@ bootstrap = [
] ]
# Timeout for upstream request # The domain names here are directly passed to bootstrap servers listed above,
# allowing captive portal detection and systems without RTC to work.
# Only effective if at least one bootstrap server is configured.
passthrough = [
"captive.apple.com",
"connectivitycheck.gstatic.com",
"detectportal.firefox.com",
"msftconnecttest.com",
"nmcheck.gnome.org",
"pool.ntp.org",
"time.apple.com",
"time.asia.apple.com",
"time.euro.apple.com",
"time.nist.gov",
"time.windows.com",
]
# Timeout for upstream request in seconds
timeout = 30 timeout = 30
# Disable HTTP Cookies # Disable HTTP Cookies
@@ -70,7 +103,7 @@ no_cookies = true
# Disable EDNS0-Client-Subnet (ECS) # Disable EDNS0-Client-Subnet (ECS)
# #
# DNS-over-HTTPS supports EDNS0-Client-Subnet protocol, which submits part of # DNS-over-HTTPS supports EDNS0-Client-Subnet protocol, which submits part of
# the client's IP address (/24 for IPv4, /48 for IPv6 by default) to the # the client's IP address (/24 for IPv4, /56 for IPv6 by default) to the
# upstream server. This is useful for GeoDNS and CDNs to work, and is exactly # upstream server. This is useful for GeoDNS and CDNs to work, and is exactly
# the same configuration as most public DNS servers. # the same configuration as most public DNS servers.
no_ecs = false no_ecs = false

View File

@@ -24,48 +24,41 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"math/rand"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/m13253/dns-over-https/doh-client/selector"
"github.com/m13253/dns-over-https/json-dns" "github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func (c *Client) generateRequestGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP bool) *DNSRequest { func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool, upstream *selector.Upstream) *DNSRequest {
reply := jsonDNS.PrepareReply(r) question := &r.Question[0]
questionName := question.Name
if len(r.Question) != 1 { questionClass := question.Qclass
log.Println("Number of questions is not 1") if questionClass != dns.ClassINET {
reply.Rcode = dns.RcodeFormatError reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeRefused
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
err: &dns.Error{}, err: &dns.Error{},
} }
} }
question := &r.Question[0]
questionName := question.Name
questionType := "" questionType := ""
if qtype, ok := dns.TypeToString[question.Qtype]; ok { if qtype, ok := dns.TypeToString[question.Qtype]; ok {
questionType = qtype questionType = qtype
} else { } else {
questionType = strconv.Itoa(int(question.Qtype)) questionType = strconv.FormatUint(uint64(question.Qtype), 10)
} }
if c.conf.Verbose { requestURL := fmt.Sprintf("%s?ct=application/dns-json&name=%s&type=%s", upstream.URL, url.QueryEscape(questionName), url.QueryEscape(questionType))
fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType)
}
numServers := len(c.conf.UpstreamGoogle)
upstream := c.conf.UpstreamGoogle[rand.Intn(numServers)]
requestURL := fmt.Sprintf("%s?ct=application/dns-json&name=%s&type=%s", upstream, url.QueryEscape(questionName), url.QueryEscape(questionType))
if r.CheckingDisabled { if r.CheckingDisabled {
requestURL += "&cd=1" requestURL += "&cd=1"
@@ -81,28 +74,39 @@ func (c *Client) generateRequestGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP b
requestURL += fmt.Sprintf("&edns_client_subnet=%s/%d", ednsClientAddress.String(), ednsClientNetmask) requestURL += fmt.Sprintf("&edns_client_subnet=%s/%d", ednsClientAddress.String(), ednsClientNetmask)
} }
req, err := http.NewRequest("GET", requestURL, nil) req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
} }
req.Header.Set("Accept", "application/json, application/dns-message, application/dns-udpwireformat") req.Header.Set("Accept", "application/json, application/dns-message, application/dns-udpwireformat")
req.Header.Set("User-Agent", USER_AGENT) req.Header.Set("User-Agent", USER_AGENT)
req = req.WithContext(ctx)
c.httpClientMux.RLock() c.httpClientMux.RLock()
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
c.httpClientMux.RUnlock() c.httpClientMux.RUnlock()
// if http Client.Do returns non-nil error, it always *url.Error
/*if err == context.DeadlineExceeded {
// Do not respond, silently fail to prevent caching of SERVFAIL
log.Println(err)
return &DNSRequest{
err: err,
}
}*/
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
err1 := c.newHTTPClient()
if err1 != nil {
log.Fatalln(err1)
}
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
@@ -110,16 +114,16 @@ func (c *Client) generateRequestGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP b
return &DNSRequest{ return &DNSRequest{
response: resp, response: resp,
reply: reply, reply: jsonDNS.PrepareReply(r),
udpSize: udpSize, udpSize: udpSize,
ednsClientAddress: ednsClientAddress, ednsClientAddress: ednsClientAddress,
ednsClientNetmask: ednsClientNetmask, ednsClientNetmask: ednsClientNetmask,
currentUpstream: upstream, currentUpstream: upstream.URL,
} }
} }
func (c *Client) parseResponseGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP bool, req *DNSRequest) { func (c *Client) parseResponseGoogle(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool, req *DNSRequest) {
if req.response.StatusCode != 200 { if req.response.StatusCode != http.StatusOK {
log.Printf("HTTP error from upstream %s: %s\n", req.currentUpstream, req.response.Status) log.Printf("HTTP error from upstream %s: %s\n", req.currentUpstream, req.response.Status)
req.reply.Rcode = dns.RcodeServerFailure req.reply.Rcode = dns.RcodeServerFailure
contentType := req.response.Header.Get("Content-Type") contentType := req.response.Header.Get("Content-Type")

View File

@@ -25,47 +25,22 @@ package main
import ( import (
"bytes" "bytes"
"context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"math/rand"
"net" "net"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
"github.com/m13253/dns-over-https/doh-client/selector"
"github.com/m13253/dns-over-https/json-dns" "github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool) *DNSRequest { func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool, upstream *selector.Upstream) *DNSRequest {
reply := jsonDNS.PrepareReply(r)
if len(r.Question) != 1 {
log.Println("Number of questions is not 1")
reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply)
return &DNSRequest{
err: &dns.Error{},
}
}
question := &r.Question[0]
questionName := question.Name
questionType := ""
if qtype, ok := dns.TypeToString[question.Qtype]; ok {
questionType = qtype
} else {
questionType = strconv.Itoa(int(question.Qtype))
}
if c.conf.Verbose {
fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType)
}
question.Name = questionName
opt := r.IsEdns0() opt := r.IsEdns0()
udpSize := uint16(512) udpSize := uint16(512)
if opt == nil { if opt == nil {
@@ -96,7 +71,7 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
edns0Subnet = new(dns.EDNS0_SUBNET) edns0Subnet = new(dns.EDNS0_SUBNET)
edns0Subnet.Code = dns.EDNS0SUBNET edns0Subnet.Code = dns.EDNS0SUBNET
@@ -115,6 +90,7 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
requestBinary, err := r.Pack() requestBinary, err := r.Pack()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeFormatError reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
@@ -124,26 +100,27 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
r.Id = requestID r.Id = requestID
requestBase64 := base64.RawURLEncoding.EncodeToString(requestBinary) requestBase64 := base64.RawURLEncoding.EncodeToString(requestBinary)
numServers := len(c.conf.UpstreamIETF) requestURL := fmt.Sprintf("%s?ct=application/dns-message&dns=%s", upstream.URL, requestBase64)
upstream := c.conf.UpstreamIETF[rand.Intn(numServers)]
requestURL := fmt.Sprintf("%s?ct=application/dns-udpwireformat&dns=%s", upstream, requestBase64)
//requestURL := fmt.Sprintf("%s?ct=application/dns-message&dns=%s", upstream, requestBase64)
var req *http.Request var req *http.Request
if len(requestURL) < 2048 { if len(requestURL) < 2048 {
req, err = http.NewRequest("GET", requestURL, nil) req, err = http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil { if err != nil {
// Do not respond, silently fail to prevent caching of SERVFAIL
log.Println(err) log.Println(err)
reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
} }
} else { } else {
req, err = http.NewRequest("POST", upstream, bytes.NewReader(requestBinary)) req, err = http.NewRequest(http.MethodPost, upstream.URL, bytes.NewReader(requestBinary))
if err != nil { if err != nil {
// Do not respond, silently fail to prevent caching of SERVFAIL
log.Println(err) log.Println(err)
reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
@@ -152,17 +129,25 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
} }
req.Header.Set("Accept", "application/dns-message, application/dns-udpwireformat, application/json") req.Header.Set("Accept", "application/dns-message, application/dns-udpwireformat, application/json")
req.Header.Set("User-Agent", USER_AGENT) req.Header.Set("User-Agent", USER_AGENT)
req = req.WithContext(ctx)
c.httpClientMux.RLock() c.httpClientMux.RLock()
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
c.httpClientMux.RUnlock() c.httpClientMux.RUnlock()
// if http Client.Do returns non-nil error, it always *url.Error
/*if err == context.DeadlineExceeded {
// Do not respond, silently fail to prevent caching of SERVFAIL
log.Println(err)
return &DNSRequest{
err: err,
}
}*/
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
err1 := c.newHTTPClient()
if err1 != nil {
log.Fatalln(err1)
}
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
@@ -170,16 +155,16 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
return &DNSRequest{ return &DNSRequest{
response: resp, response: resp,
reply: reply, reply: jsonDNS.PrepareReply(r),
udpSize: udpSize, udpSize: udpSize,
ednsClientAddress: ednsClientAddress, ednsClientAddress: ednsClientAddress,
ednsClientNetmask: ednsClientNetmask, ednsClientNetmask: ednsClientNetmask,
currentUpstream: upstream, currentUpstream: upstream.URL,
} }
} }
func (c *Client) parseResponseIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool, req *DNSRequest) { func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool, req *DNSRequest) {
if req.response.StatusCode != 200 { if req.response.StatusCode != http.StatusOK {
log.Printf("HTTP error from upstream %s: %s\n", req.currentUpstream, req.response.Status) log.Printf("HTTP error from upstream %s: %s\n", req.currentUpstream, req.response.Status)
req.reply.Rcode = dns.RcodeServerFailure req.reply.Rcode = dns.RcodeServerFailure
contentType := req.response.Header.Get("Content-Type") contentType := req.response.Header.Get("Content-Type")

View File

@@ -25,21 +25,91 @@ package main
import ( import (
"flag" "flag"
"fmt"
"io"
"io/ioutil"
"log" "log"
"os"
"runtime"
"strconv"
"github.com/m13253/dns-over-https/doh-client/config"
) )
func checkPIDFile(pidFile string) (bool, error) {
retry:
f, err := os.OpenFile(pidFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if os.IsExist(err) {
pidStr, err := ioutil.ReadFile(pidFile)
if err != nil {
return false, err
}
pid, err := strconv.ParseUint(string(pidStr), 10, 0)
if err != nil {
return false, err
}
_, err = os.Stat(fmt.Sprintf("/proc/%d", pid))
if os.IsNotExist(err) {
err = os.Remove(pidFile)
if err != nil {
return false, err
}
goto retry
} else if err != nil {
return false, err
}
log.Printf("Already running on PID %d, exiting.\n", pid)
return false, nil
} else if err != nil {
return false, err
}
defer f.Close()
_, err = io.WriteString(f, strconv.FormatInt(int64(os.Getpid()), 10))
if err != nil {
return false, err
}
return true, nil
}
func main() { func main() {
confPath := flag.String("conf", "doh-client.conf", "Configuration file") confPath := flag.String("conf", "doh-client.conf", "Configuration file")
verbose := flag.Bool("verbose", false, "Enable logging") verbose := flag.Bool("verbose", false, "Enable logging")
showVersion := flag.Bool("version", false, "Show software version and exit")
var pidFile *string
// I really want to push the technology forward by recommending cgroup-based
// process tracking. But I understand some cloud service providers have
// their own monitoring system. So this feature is only enabled on Linux and
// BSD series platforms which lacks functionality similar to cgroup.
switch runtime.GOOS {
case "dragonfly", "freebsd", "linux", "netbsd", "openbsd":
pidFile = flag.String("pid-file", "", "PID file for legacy supervision systems lacking support for reliable cgroup-based process tracking")
}
flag.Parse() flag.Parse()
conf, err := loadConfig(*confPath) if *showVersion {
fmt.Printf("doh-server %s\nHomepage: https://github.com/m13253/dns-over-https\n", VERSION)
return
}
if pidFile != nil && *pidFile != "" {
ok, err := checkPIDFile(*pidFile)
if err != nil {
log.Printf("Error checking PID file: %v\n", err)
}
if !ok {
return
}
}
conf, err := config.LoadConfig(*confPath)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
if *verbose { if *verbose {
conf.Verbose = true conf.Other.Verbose = true
} }
client, err := NewClient(conf) client, err := NewClient(conf)

View File

@@ -0,0 +1,262 @@
package selector
import (
"encoding/json"
"errors"
"log"
"net/http"
"sync"
"sync/atomic"
"time"
)
type LVSWRRSelector struct {
upstreams []*Upstream // upstreamsInfo
client http.Client // http client to check the upstream
lastChoose int32
currentWeight int32
}
func NewLVSWRRSelector(timeout time.Duration) *LVSWRRSelector {
return &LVSWRRSelector{
client: http.Client{Timeout: timeout},
lastChoose: -1,
}
}
func (ls *LVSWRRSelector) Add(url string, upstreamType UpstreamType, weight int32) (err error) {
if weight < 1 {
return errors.New("weight is 1")
}
switch upstreamType {
case Google:
ls.upstreams = append(ls.upstreams, &Upstream{
Type: Google,
URL: url,
RequestType: "application/dns-json",
weight: weight,
effectiveWeight: weight,
})
case IETF:
ls.upstreams = append(ls.upstreams, &Upstream{
Type: IETF,
URL: url,
RequestType: "application/dns-message",
weight: weight,
effectiveWeight: weight,
})
default:
return errors.New("unknown upstream type")
}
return nil
}
func (ls *LVSWRRSelector) StartEvaluate() {
go func() {
for {
wg := sync.WaitGroup{}
for i := range ls.upstreams {
wg.Add(1)
go func(i int) {
defer wg.Done()
upstreamURL := ls.upstreams[i].URL
var acceptType string
switch ls.upstreams[i].Type {
case Google:
upstreamURL += "?name=www.example.com&type=A"
acceptType = "application/dns-json"
case IETF:
// www.example.com
upstreamURL += "?dns=q80BAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB"
acceptType = "application/dns-message"
}
req, err := http.NewRequest(http.MethodGet, upstreamURL, nil)
if err != nil {
/*log.Println("upstream:", upstreamURL, "type:", typeMap[upstream.Type], "check failed:", err)
continue*/
// should I only log it? But if there is an error, I think when query the server will return error too
panic("upstream: " + upstreamURL + " type: " + typeMap[ls.upstreams[i].Type] + " check failed: " + err.Error())
}
req.Header.Set("accept", acceptType)
resp, err := ls.client.Do(req)
if err != nil {
// should I check error in detail?
if atomic.AddInt32(&ls.upstreams[i].effectiveWeight, -5) < 1 {
atomic.StoreInt32(&ls.upstreams[i].effectiveWeight, 1)
}
return
}
switch ls.upstreams[i].Type {
case Google:
ls.checkGoogleResponse(resp, ls.upstreams[i])
case IETF:
ls.checkIETFResponse(resp, ls.upstreams[i])
}
}(i)
}
wg.Wait()
time.Sleep(15 * time.Second)
}
}()
}
func (ls *LVSWRRSelector) Get() *Upstream {
if len(ls.upstreams) == 1 {
return ls.upstreams[0]
}
for {
atomic.StoreInt32(&ls.lastChoose, (atomic.LoadInt32(&ls.lastChoose)+1)%int32(len(ls.upstreams)))
if atomic.LoadInt32(&ls.lastChoose) == 0 {
atomic.AddInt32(&ls.currentWeight, -ls.gcdWeight())
if atomic.LoadInt32(&ls.currentWeight) <= 0 {
atomic.AddInt32(&ls.currentWeight, ls.maxWeight())
if atomic.LoadInt32(&ls.currentWeight) == 0 {
panic("current weight is 0")
}
}
}
if atomic.LoadInt32(&ls.upstreams[atomic.LoadInt32(&ls.lastChoose)].effectiveWeight) >= atomic.LoadInt32(&ls.currentWeight) {
return ls.upstreams[atomic.LoadInt32(&ls.lastChoose)]
}
}
}
func (ls *LVSWRRSelector) gcdWeight() (res int32) {
res = gcd(atomic.LoadInt32(&ls.upstreams[0].effectiveWeight), atomic.LoadInt32(&ls.upstreams[0].effectiveWeight))
for i := 1; i < len(ls.upstreams); i++ {
res = gcd(res, atomic.LoadInt32(&ls.upstreams[i].effectiveWeight))
}
return
}
func (ls *LVSWRRSelector) maxWeight() (res int32) {
for _, upstream := range ls.upstreams {
w := atomic.LoadInt32(&upstream.effectiveWeight)
if w > res {
res = w
}
}
return
}
func gcd(x, y int32) int32 {
for {
if x < y {
x, y = y, x
}
tmp := x % y
if tmp == 0 {
return y
}
x = tmp
}
}
func (ls *LVSWRRSelector) ReportUpstreamStatus(upstream *Upstream, upstreamStatus upstreamStatus) {
switch upstreamStatus {
case Timeout:
if atomic.AddInt32(&upstream.effectiveWeight, -5) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
case Error:
if atomic.AddInt32(&upstream.effectiveWeight, -2) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
case OK:
if atomic.AddInt32(&upstream.effectiveWeight, 1) > upstream.weight {
atomic.StoreInt32(&upstream.effectiveWeight, upstream.weight)
}
}
}
func (ls *LVSWRRSelector) checkGoogleResponse(resp *http.Response, upstream *Upstream) {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// server error
if atomic.AddInt32(&upstream.effectiveWeight, -3) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
return
}
m := make(map[string]interface{})
if err := json.NewDecoder(resp.Body).Decode(&m); err != nil {
// should I check error in detail?
if atomic.AddInt32(&upstream.effectiveWeight, -2) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
return
}
if status, ok := m["Status"]; ok {
if statusNum, ok := status.(float64); ok && statusNum == 0 {
if atomic.AddInt32(&upstream.effectiveWeight, 5) > upstream.weight {
atomic.StoreInt32(&upstream.effectiveWeight, upstream.weight)
}
return
}
}
// should I check error in detail?
if atomic.AddInt32(&upstream.effectiveWeight, -2) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
}
func (ls *LVSWRRSelector) checkIETFResponse(resp *http.Response, upstream *Upstream) {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// server error
if atomic.AddInt32(&upstream.effectiveWeight, -3) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
return
}
if atomic.AddInt32(&upstream.effectiveWeight, 5) > upstream.weight {
atomic.StoreInt32(&upstream.effectiveWeight, upstream.weight)
}
}
func (ls *LVSWRRSelector) ReportWeights() {
go func() {
for {
time.Sleep(15 * time.Second)
for _, u := range ls.upstreams {
log.Printf("%s, effect weight: %d", u, atomic.LoadInt32(&u.effectiveWeight))
}
}
}()
}

View File

@@ -0,0 +1,215 @@
package selector
import (
"encoding/json"
"errors"
"log"
"net/http"
"sync"
"sync/atomic"
"time"
)
type NginxWRRSelector struct {
upstreams []*Upstream // upstreamsInfo
client http.Client // http client to check the upstream
}
func NewNginxWRRSelector(timeout time.Duration) *NginxWRRSelector {
return &NginxWRRSelector{
client: http.Client{Timeout: timeout},
}
}
func (ws *NginxWRRSelector) Add(url string, upstreamType UpstreamType, weight int32) (err error) {
switch upstreamType {
case Google:
ws.upstreams = append(ws.upstreams, &Upstream{
Type: Google,
URL: url,
RequestType: "application/dns-json",
weight: weight,
effectiveWeight: weight,
})
case IETF:
ws.upstreams = append(ws.upstreams, &Upstream{
Type: IETF,
URL: url,
RequestType: "application/dns-message",
weight: weight,
effectiveWeight: weight,
})
default:
return errors.New("unknown upstream type")
}
return nil
}
func (ws *NginxWRRSelector) StartEvaluate() {
go func() {
for {
wg := sync.WaitGroup{}
for i := range ws.upstreams {
wg.Add(1)
go func(i int) {
defer wg.Done()
upstreamURL := ws.upstreams[i].URL
var acceptType string
switch ws.upstreams[i].Type {
case Google:
upstreamURL += "?name=www.example.com&type=A"
acceptType = "application/dns-json"
case IETF:
// www.example.com
upstreamURL += "?dns=q80BAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB"
acceptType = "application/dns-message"
}
req, err := http.NewRequest(http.MethodGet, upstreamURL, nil)
if err != nil {
/*log.Println("upstream:", upstreamURL, "type:", typeMap[upstream.Type], "check failed:", err)
continue*/
// should I only log it? But if there is an error, I think when query the server will return error too
panic("upstream: " + upstreamURL + " type: " + typeMap[ws.upstreams[i].Type] + " check failed: " + err.Error())
}
req.Header.Set("accept", acceptType)
resp, err := ws.client.Do(req)
if err != nil {
// should I check error in detail?
if atomic.AddInt32(&ws.upstreams[i].effectiveWeight, -10) < 1 {
atomic.StoreInt32(&ws.upstreams[i].effectiveWeight, 1)
}
return
}
switch ws.upstreams[i].Type {
case Google:
ws.checkGoogleResponse(resp, ws.upstreams[i])
case IETF:
ws.checkIETFResponse(resp, ws.upstreams[i])
}
}(i)
}
wg.Wait()
time.Sleep(15 * time.Second)
}
}()
}
// nginx wrr like
func (ws *NginxWRRSelector) Get() *Upstream {
var (
total int32
bestUpstreamIndex = -1
)
for i := range ws.upstreams {
effectiveWeight := atomic.LoadInt32(&ws.upstreams[i].effectiveWeight)
atomic.AddInt32(&ws.upstreams[i].currentWeight, effectiveWeight)
total += effectiveWeight
if bestUpstreamIndex == -1 || atomic.LoadInt32(&ws.upstreams[i].currentWeight) > atomic.LoadInt32(&ws.upstreams[bestUpstreamIndex].currentWeight) {
bestUpstreamIndex = i
}
}
atomic.AddInt32(&ws.upstreams[bestUpstreamIndex].currentWeight, -total)
return ws.upstreams[bestUpstreamIndex]
}
func (ws *NginxWRRSelector) ReportUpstreamStatus(upstream *Upstream, upstreamStatus upstreamStatus) {
switch upstreamStatus {
case Timeout:
if atomic.AddInt32(&upstream.effectiveWeight, -5) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
case Error:
if atomic.AddInt32(&upstream.effectiveWeight, -3) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
case OK:
if atomic.AddInt32(&upstream.effectiveWeight, 1) > upstream.weight {
atomic.StoreInt32(&upstream.effectiveWeight, upstream.weight)
}
}
}
func (ws *NginxWRRSelector) checkGoogleResponse(resp *http.Response, upstream *Upstream) {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// server error
if atomic.AddInt32(&upstream.effectiveWeight, -3) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
return
}
m := make(map[string]interface{})
if err := json.NewDecoder(resp.Body).Decode(&m); err != nil {
// should I check error in detail?
if atomic.AddInt32(&upstream.effectiveWeight, -2) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
return
}
if status, ok := m["Status"]; ok {
if statusNum, ok := status.(float64); ok && statusNum == 0 {
if atomic.AddInt32(&upstream.effectiveWeight, 5) > upstream.weight {
atomic.StoreInt32(&upstream.effectiveWeight, upstream.weight)
}
return
}
}
// should I check error in detail?
if atomic.AddInt32(&upstream.effectiveWeight, -2) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
}
func (ws *NginxWRRSelector) checkIETFResponse(resp *http.Response, upstream *Upstream) {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// server error
if atomic.AddInt32(&upstream.effectiveWeight, -5) < 1 {
atomic.StoreInt32(&upstream.effectiveWeight, 1)
}
return
}
if atomic.AddInt32(&upstream.effectiveWeight, 5) > upstream.weight {
atomic.StoreInt32(&upstream.effectiveWeight, upstream.weight)
}
}
func (ws *NginxWRRSelector) ReportWeights() {
go func() {
for {
time.Sleep(15 * time.Second)
for _, u := range ws.upstreams {
log.Printf("%s, effect weight: %d", u, atomic.LoadInt32(&u.effectiveWeight))
}
}
}()
}

View File

@@ -0,0 +1,50 @@
package selector
import (
"errors"
"math/rand"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
type RandomSelector struct {
upstreams []*Upstream
}
func NewRandomSelector() *RandomSelector {
return new(RandomSelector)
}
func (rs *RandomSelector) Add(url string, upstreamType UpstreamType) (err error) {
switch upstreamType {
case Google:
rs.upstreams = append(rs.upstreams, &Upstream{
Type: Google,
URL: url,
RequestType: "application/dns-json",
})
case IETF:
rs.upstreams = append(rs.upstreams, &Upstream{
Type: IETF,
URL: url,
RequestType: "application/dns-message",
})
default:
return errors.New("unknown upstream type")
}
return nil
}
func (rs *RandomSelector) Get() *Upstream {
return rs.upstreams[rand.Intn(len(rs.upstreams)-1)]
}
func (rs *RandomSelector) StartEvaluate() {}
func (rs *RandomSelector) ReportUpstreamStatus(upstream *Upstream, upstreamStatus upstreamStatus) {}

View File

@@ -0,0 +1,17 @@
package selector
type Selector interface {
// Get returns a upstream
Get() *Upstream
// StartEvaluate start upstream evaluation loop
StartEvaluate()
// ReportUpstreamStatus report upstream status
ReportUpstreamStatus(upstream *Upstream, upstreamStatus upstreamStatus)
}
type DebugReporter interface {
// ReportWeights starts a goroutine to report all upstream weights, recommend interval is 15s
ReportWeights()
}

View File

@@ -0,0 +1,28 @@
package selector
import "fmt"
type UpstreamType int
const (
Google UpstreamType = iota
IETF
)
var typeMap = map[UpstreamType]string{
Google: "Google",
IETF: "IETF",
}
type Upstream struct {
Type UpstreamType
URL string
RequestType string
weight int32
effectiveWeight int32
currentWeight int32
}
func (u Upstream) String() string {
return fmt.Sprintf("upstream type: %s, upstream url: %s", typeMap[u.Type], u.URL)
}

View File

@@ -0,0 +1,14 @@
package selector
type upstreamStatus int
const (
// when query upstream timeout, usually upstream is unavailable for a long time
Timeout upstreamStatus = iota
// when query upstream return 5xx response, upstream still alive, maybe just a lof of query for him
Error
// when query upstream ok, means upstream is available
OK
)

View File

@@ -24,6 +24,6 @@
package main package main
const ( const (
VERSION = "1.3.8" VERSION = "2.0.0"
USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)" USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)"
) )

View File

@@ -30,15 +30,17 @@ import (
) )
type config struct { type config struct {
Listen []string `toml:"listen"` Listen []string `toml:"listen"`
Cert string `toml:"cert"` Cert string `toml:"cert"`
Key string `toml:"key"` Key string `toml:"key"`
Path string `toml:"path"` Path string `toml:"path"`
Upstream []string `toml:"upstream"` Upstream []string `toml:"upstream"`
Timeout uint `toml:"timeout"` Timeout uint `toml:"timeout"`
Tries uint `toml:"tries"` Tries uint `toml:"tries"`
TCPOnly bool `toml:"tcp_only"` TCPOnly bool `toml:"tcp_only"`
Verbose bool `toml:"verbose"` Verbose bool `toml:"verbose"`
DebugHTTPHeaders []string `toml:"debug_http_headers"`
LogGuessedIP bool `toml:"log_guessed_client_ip"`
} }
func loadConfig(path string) (*config, error) { func loadConfig(path string) (*config, error) {

View File

@@ -6,10 +6,10 @@ listen = [
# TLS certification file # TLS certification file
# If left empty, plain-text HTTP will be used. # If left empty, plain-text HTTP will be used.
# Please be informed that this program does not do OCSP Stapling, which is # You are recommended to leave empty and to use a server load balancer (e.g.
# necessary for some clients to bootstrap itself. # Caddy, Nginx) and set up TLS there, because this program does not do OCSP
# You are recommended to use a server load balancer (Caddy, Nginx) and set up # Stapling, which is necessary for client bootstrapping in a network
# TLS there. # environment with completely no traditional DNS service.
cert = "" cert = ""
# TLS private key file # TLS private key file
@@ -21,6 +21,8 @@ path = "/dns-query"
# Upstream DNS resolver # Upstream DNS resolver
# If multiple servers are specified, a random one will be chosen each time. # If multiple servers are specified, a random one will be chosen each time.
upstream = [ upstream = [
"1.1.1.1:53",
"1.0.0.1:53",
"8.8.8.8:53", "8.8.8.8:53",
"8.8.4.4:53", "8.8.4.4:53",
] ]
@@ -36,3 +38,7 @@ tcp_only = false
# Enable logging # Enable logging
verbose = false verbose = false
# Enable log IP from HTTPS-reverse proxy header: X-Forwarded-For or X-Real-IP
# Note: http uri/useragent log cannot be controlled by this config
log_guessed_client_ip = false

View File

@@ -24,6 +24,7 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
@@ -38,7 +39,7 @@ import (
"golang.org/x/net/idna" "golang.org/x/net/idna"
) )
func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNSRequest { func (s *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest {
name := r.FormValue("name") name := r.FormValue("name")
if name == "" { if name == "" {
return &DNSRequest{ return &DNSRequest{
@@ -71,9 +72,9 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
cdStr := r.FormValue("cd") cdStr := r.FormValue("cd")
cd := false cd := false
if cdStr == "1" || cdStr == "true" { if cdStr == "1" || strings.EqualFold(cdStr, "true") {
cd = true cd = true
} else if cdStr == "0" || cdStr == "false" || cdStr == "" { } else if cdStr == "0" || strings.EqualFold(cdStr, "false") || cdStr == "" {
} else { } else {
return &DNSRequest{ return &DNSRequest{
errcode: 400, errcode: 400,
@@ -104,7 +105,7 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
} else { } else {
ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash]) ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash])
@@ -139,7 +140,7 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
} }
@@ -168,7 +169,7 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
} }
} }
func (s *Server) generateResponseGoogle(w http.ResponseWriter, r *http.Request, req *DNSRequest) { func (s *Server) generateResponseGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) {
respJSON := jsonDNS.Marshal(req.response) respJSON := jsonDNS.Marshal(req.response)
respStr, err := json.Marshal(respJSON) respStr, err := json.Marshal(respJSON)
if err != nil { if err != nil {
@@ -184,9 +185,9 @@ func (s *Server) generateResponseGoogle(w http.ResponseWriter, r *http.Request,
w.Header().Set("Vary", "Accept") w.Header().Set("Vary", "Accept")
if respJSON.HaveTTL { if respJSON.HaveTTL {
if req.isTailored { if req.isTailored {
w.Header().Set("Cache-Control", "private, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) w.Header().Set("Cache-Control", "private, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10))
} else { } else {
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) w.Header().Set("Cache-Control", "public, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10))
} }
w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat)) w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat))
} }

View File

@@ -25,10 +25,12 @@ package main
import ( import (
"bytes" "bytes"
"context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@@ -38,7 +40,7 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRequest { func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest {
requestBase64 := r.FormValue("dns") requestBase64 := r.FormValue("dns")
requestBinary, err := base64.RawURLEncoding.DecodeString(requestBase64) requestBinary, err := base64.RawURLEncoding.DecodeString(requestBase64)
if err != nil { if err != nil {
@@ -85,15 +87,23 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
if qclass, ok := dns.ClassToString[question.Qclass]; ok { if qclass, ok := dns.ClassToString[question.Qclass]; ok {
questionClass = qclass questionClass = qclass
} else { } else {
questionClass = strconv.Itoa(int(question.Qclass)) questionClass = strconv.FormatUint(uint64(question.Qclass), 10)
} }
questionType := "" questionType := ""
if qtype, ok := dns.TypeToString[question.Qtype]; ok { if qtype, ok := dns.TypeToString[question.Qtype]; ok {
questionType = qtype questionType = qtype
} else { } else {
questionType = strconv.Itoa(int(question.Qtype)) questionType = strconv.FormatUint(uint64(question.Qtype), 10)
}
var clientip net.IP = nil
if s.conf.LogGuessedIP {
clientip = s.findClientIP(r)
}
if clientip != nil {
fmt.Printf("%s - - [%s] \"%s %s %s\"\n", clientip, time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType)
} else {
fmt.Printf("%s - - [%s] \"%s %s %s\"\n", r.RemoteAddr, time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType)
} }
fmt.Printf("%s - - [%s] \"%s %s %s\"\n", r.RemoteAddr, time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType)
} }
transactionID := msg.Id transactionID := msg.Id
@@ -126,7 +136,7 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
edns0Subnet = new(dns.EDNS0_SUBNET) edns0Subnet = new(dns.EDNS0_SUBNET)
edns0Subnet.Code = dns.EDNS0SUBNET edns0Subnet.Code = dns.EDNS0SUBNET
@@ -145,7 +155,7 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
} }
} }
func (s *Server) generateResponseIETF(w http.ResponseWriter, r *http.Request, req *DNSRequest) { func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) {
respJSON := jsonDNS.Marshal(req.response) respJSON := jsonDNS.Marshal(req.response)
req.response.Id = req.transactionID req.response.Id = req.transactionID
respBytes, err := req.response.Pack() respBytes, err := req.response.Pack()
@@ -165,9 +175,9 @@ func (s *Server) generateResponseIETF(w http.ResponseWriter, r *http.Request, re
if respJSON.HaveTTL { if respJSON.HaveTTL {
if req.isTailored { if req.isTailored {
w.Header().Set("Cache-Control", "private, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) w.Header().Set("Cache-Control", "private, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10))
} else { } else {
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) w.Header().Set("Cache-Control", "public, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10))
} }
w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat)) w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat))
} }

View File

@@ -25,14 +25,82 @@ package main
import ( import (
"flag" "flag"
"fmt"
"io"
"io/ioutil"
"log" "log"
"os"
"runtime"
"strconv"
) )
func checkPIDFile(pidFile string) (bool, error) {
retry:
f, err := os.OpenFile(pidFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if os.IsExist(err) {
pidStr, err := ioutil.ReadFile(pidFile)
if err != nil {
return false, err
}
pid, err := strconv.ParseUint(string(pidStr), 10, 0)
if err != nil {
return false, err
}
_, err = os.Stat(fmt.Sprintf("/proc/%d", pid))
if os.IsNotExist(err) {
err = os.Remove(pidFile)
if err != nil {
return false, err
}
goto retry
} else if err != nil {
return false, err
}
log.Printf("Already running on PID %d, exiting.\n", pid)
return false, nil
} else if err != nil {
return false, err
}
defer f.Close()
_, err = io.WriteString(f, strconv.FormatInt(int64(os.Getpid()), 10))
if err != nil {
return false, err
}
return true, nil
}
func main() { func main() {
confPath := flag.String("conf", "doh-server.conf", "Configuration file") confPath := flag.String("conf", "doh-server.conf", "Configuration file")
verbose := flag.Bool("verbose", false, "Enable logging") verbose := flag.Bool("verbose", false, "Enable logging")
showVersion := flag.Bool("version", false, "Show software version and exit")
var pidFile *string
// I really want to push the technology forward by recommending cgroup-based
// process tracking. But I understand some cloud service providers have
// their own monitoring system. So this feature is only enabled on Linux and
// BSD series platforms which lacks functionality similar to cgroup.
switch runtime.GOOS {
case "dragonfly", "freebsd", "linux", "netbsd", "openbsd":
pidFile = flag.String("pid-file", "", "PID file for legacy supervision systems lacking support for reliable cgroup-based process tracking")
}
flag.Parse() flag.Parse()
if *showVersion {
fmt.Printf("doh-server %s\nHomepage: https://github.com/m13253/dns-over-https\n", VERSION)
return
}
if pidFile != nil && *pidFile != "" {
ok, err := checkPIDFile(*pidFile)
if err != nil {
log.Printf("Error checking PID file: %v\n", err)
}
if !ok {
return
}
}
conf, err := loadConfig(*confPath) conf, err := loadConfig(*confPath)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)

View File

@@ -24,6 +24,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"math/rand" "math/rand"
@@ -105,13 +106,31 @@ func (s *Server) Start() error {
} }
func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) { func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS, POST")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Max-Age", "3600")
w.Header().Set("Server", USER_AGENT) w.Header().Set("Server", USER_AGENT)
w.Header().Set("X-Powered-By", USER_AGENT) w.Header().Set("X-Powered-By", USER_AGENT)
if r.Method == "OPTIONS" {
w.Header().Set("Content-Length", "0")
return
}
if r.Form == nil { if r.Form == nil {
const maxMemory = 32 << 20 // 32 MB const maxMemory = 32 << 20 // 32 MB
r.ParseMultipartForm(maxMemory) r.ParseMultipartForm(maxMemory)
} }
for _, header := range s.conf.DebugHTTPHeaders {
if value := r.Header.Get(header); value != "" {
log.Printf("%s: %s\n", header, value)
}
}
contentType := r.Header.Get("Content-Type") contentType := r.Header.Get("Content-Type")
if ct := r.FormValue("ct"); ct != "" { if ct := r.FormValue("ct"); ct != "" {
contentType = ct contentType = ct
@@ -151,11 +170,11 @@ func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
var req *DNSRequest var req *DNSRequest
if contentType == "application/dns-json" { if contentType == "application/dns-json" {
req = s.parseRequestGoogle(w, r) req = s.parseRequestGoogle(ctx, w, r)
} else if contentType == "application/dns-message" { } else if contentType == "application/dns-message" {
req = s.parseRequestIETF(w, r) req = s.parseRequestIETF(ctx, w, r)
} else if contentType == "application/dns-udpwireformat" { } else if contentType == "application/dns-udpwireformat" {
req = s.parseRequestIETF(w, r) req = s.parseRequestIETF(ctx, w, r)
} else { } else {
jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 415) jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 415)
return return
@@ -171,16 +190,16 @@ func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
req = s.patchRootRD(req) req = s.patchRootRD(req)
var err error var err error
req, err = s.doDNSQuery(req) req, err = s.doDNSQuery(ctx, req)
if err != nil { if err != nil {
jsonDNS.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503) jsonDNS.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503)
return return
} }
if responseType == "application/json" { if responseType == "application/json" {
s.generateResponseGoogle(w, r, req) s.generateResponseGoogle(ctx, w, r, req)
} else if responseType == "application/dns-message" { } else if responseType == "application/dns-message" {
s.generateResponseIETF(w, r, req) s.generateResponseIETF(ctx, w, r, req)
} else { } else {
panic("Unknown response Content-Type") panic("Unknown response Content-Type")
} }
@@ -225,13 +244,14 @@ func (s *Server) patchRootRD(req *DNSRequest) *DNSRequest {
return req return req
} }
func (s *Server) doDNSQuery(req *DNSRequest) (resp *DNSRequest, err error) { func (s *Server) doDNSQuery(ctx context.Context, req *DNSRequest) (resp *DNSRequest, err error) {
// TODO(m13253): Make ctx work. Waiting for a patch for ExchangeContext from miekg/dns.
numServers := len(s.conf.Upstream) numServers := len(s.conf.Upstream)
for i := uint(0); i < s.conf.Tries; i++ { for i := uint(0); i < s.conf.Tries; i++ {
req.currentUpstream = s.conf.Upstream[rand.Intn(numServers)] req.currentUpstream = s.conf.Upstream[rand.Intn(numServers)]
if !s.conf.TCPOnly { if !s.conf.TCPOnly {
req.response, _, err = s.udpClient.Exchange(req.request, req.currentUpstream) req.response, _, err = s.udpClient.Exchange(req.request, req.currentUpstream)
if err == dns.ErrTruncated { if err == nil && req.response != nil && req.response.Truncated {
log.Println(err) log.Println(err)
req.response, _, err = s.tcpClient.Exchange(req.request, req.currentUpstream) req.response, _, err = s.tcpClient.Exchange(req.request, req.currentUpstream)
} }

View File

@@ -24,6 +24,6 @@
package main package main
const ( const (
VERSION = "1.3.8" VERSION = "2.0.0"
USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)" USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)"
) )

13
go.mod Normal file
View File

@@ -0,0 +1,13 @@
module github.com/m13253/dns-over-https
go 1.12
require (
github.com/BurntSushi/toml v0.3.1
github.com/gorilla/handlers v1.4.0
github.com/miekg/dns v1.1.6
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
golang.org/x/net v0.0.0-20190311183353-d8887717615a
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect
golang.org/x/sys v0.0.0-20190312061237-fead79001313 // indirect
)

25
go.sum Normal file
View File

@@ -0,0 +1,25 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0=
github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.6 h1:jVwb4GDwD65q/gtItR/lIZHjNH93QfeGxZUkzJcW9mc=
github.com/miekg/dns v1.1.6/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190308023053-584f3b12f43e h1:K7CV15oJ823+HLXQ+M7MSMrUg8LjfqY7O3naO+8Pp/I=
golang.org/x/sys v0.0.0-20190308023053-584f3b12f43e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -90,7 +90,7 @@ func Marshal(msg *dns.Msg) *Response {
} else if ipv4 := clientAddress.To4(); ipv4 != nil { } else if ipv4 := clientAddress.To4(); ipv4 != nil {
clientAddress = ipv4 clientAddress = ipv4
} }
resp.EdnsClientSubnet = clientAddress.String() + "/" + strconv.Itoa(int(edns0.SourceScope)) resp.EdnsClientSubnet = clientAddress.String() + "/" + strconv.FormatUint(uint64(edns0.SourceScope), 10)
} }
} }
continue continue

View File

@@ -119,7 +119,7 @@ func Unmarshal(msg *dns.Msg, resp *Response, udpSize uint16, ednsClientNetmask u
if ednsClientFamily == 1 { if ednsClientFamily == 1 {
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
} }
edns0Subnet := new(dns.EDNS0_SUBNET) edns0Subnet := new(dns.EDNS0_SUBNET)