Compare commits

...

63 Commits

Author SHA1 Message Date
Star Brilliant
72165bffff Release 2.2.1 2019-10-29 06:07:09 +08:00
Star Brilliant
82317bd63e Remove weird logs, fix #59 2019-10-29 03:23:00 +08:00
Star Brilliant
acf3e3c328 Bump version to 2.2.1 2019-10-27 22:41:14 +08:00
Star Brilliant
b708ff47b9 Release 2.2.0 2019-10-27 22:40:48 +08:00
Star Brilliant
4f4966878f Merge pull request #58 from gdm85/master
Add support for DNS-over-TLS upstream resolvers
2019-10-16 19:18:26 +08:00
gdm85
a09dfbbbc1 Add support for type prefix for upstream addresses
Add support for DNS-over-TLS upstream addresses
Remove tcp_only configuration option
2019-10-16 13:14:03 +02:00
gdm85
cc60be718c Improve error logging/checking 2019-10-16 13:14:03 +02:00
gdm85
2067eb688f Fix Opcode never assigned in jsonDNS.PrepareReply 2019-10-16 13:14:03 +02:00
Star Brilliant
ba9b14045e Merge pull request #57 from NESC1US/patch-1
Update Readme.md
2019-10-03 03:11:34 +08:00
NESC1US
ebcc85c01a Update Readme.md
Typo
2019-10-02 20:13:53 +02:00
Star Brilliant
48618aa6e2 Merge pull request #55 from fuero/feature-rpm-package
RPM package + SELinux policy
2019-09-11 21:05:49 +08:00
Star Brilliant
b78329afbc Merge pull request #54 from fuero/feature-nginx-config
Example nginx config
2019-09-11 20:51:36 +08:00
fuero
b1c41e5818 adds example nginx config 2019-09-11 14:48:17 +02:00
fuero
637d50ad91 initial package 2019-09-11 14:13:08 +02:00
Star Brilliant
ce13a961db Fix build error 2019-09-11 02:08:02 +08:00
Star Brilliant
b74220718f Add an option no_user_agent 2019-09-11 00:23:20 +08:00
Star Brilliant
db522591a1 Add example Apache and Caddy configurations 2019-08-31 21:30:20 +08:00
Star Brilliant
1eda33aec3 Add example Apache and Caddy configurations
Solves issue #51
2019-08-31 21:27:32 +08:00
Star Brilliant
268e203540 Release 2.1.2 2019-08-30 01:27:06 +08:00
Star Brilliant
21264c78cf Merge pull request #52 from takumin/patch-1
fix typo
2019-08-30 01:22:44 +08:00
Takumi Takahashi
ae74f1efe5 fix typo 2019-08-29 18:02:24 +09:00
Star Brilliant
d02c31d3ee Merge pull request #50 from felixonmars/patch-1
Update address for google's resolver
2019-06-27 17:45:23 +08:00
Felix Yan
edc86f32e5 Update address for google's resolver
The new ietf endpoint is the only one in the documentation now:
https://developers.google.com/speed/public-dns/docs/doh/

Their blog post prefers the new address too:
https://security.googleblog.com/2019/06/google-public-dns-over-https-doh.html
2019-06-27 14:57:37 +08:00
Star Brilliant
1c321be49c Release 2.1.1 2019-06-24 10:19:03 +08:00
Star Brilliant
852d0f6767 Fix a typo 2019-06-14 17:47:10 +08:00
Star Brilliant
a2d65bc89a Include DNS.SB's resolver in example configuration 2019-05-27 15:17:03 +08:00
Star Brilliant
6d8efe0939 Merge pull request #47 from rwv/master
slightly optimize the order of instructions in Dockerfile
2019-05-20 13:02:13 +08:00
seedgou
7e35e18164 optimize the order of instructions in Dockerfile 2019-05-20 11:17:40 +08:00
Star Brilliant
f40a7160b8 Merge pull request #46 from jangrewe/master
Add Dockerfiles
2019-05-17 22:50:33 +08:00
Jan Grewe
c8c22873bb Build separate Docker image for doh-server and doh-client
Make doh-client also listen on both IPv4 and IPv6
2019-05-16 20:47:40 +02:00
Star Brilliant
cb64f6694b Update the sample configuration to teach users how to listen on both IPv4 and IPv6 2019-05-17 02:37:52 +08:00
Jan Grewe
5c27ae02c0 Update Dockerfile to make doh-server listen on IPv4 and IPv6 2019-05-16 20:31:03 +02:00
Jan Grewe
f5ba377d2a Add Dockerfile 2019-05-16 00:28:46 +02:00
Star Brilliant
1ec9548ff1 Release 2.1.0 2019-05-14 01:39:46 +08:00
Star Brilliant
81f1cfba5d Disable static linking to Swift standard libraries
According to Apple: Swift compiler no longer supports statically linking the Swift libraries. They're included in the OS by default starting with macOS Mojave 10.14.4. For macOS Mojave 10.14.3 and earlier, there's an optional Swift library package that can be downloaded from "More Downloads" for Apple Developers at https://developer.apple.com/download/more/
2019-05-14 01:36:10 +08:00
Star Brilliant
ebba9c8ef5 Explain why ECS is disabled by some servers 2019-05-14 01:13:06 +08:00
Star Brilliant
6a2f2cea22 Merge pull request #44 from modib/quad9-dns-config
Added Quad9 servers in config.
2019-05-14 01:01:07 +08:00
B. Modi
63f07d20af Updated Quad9 config with ECS, DNSSEC info. 2019-05-13 09:55:20 -07:00
Star Brilliant
f0dec57e1a Merge pull request #45 from wsquasher/master
Use TCP when appropriate for the given query type/response
2019-05-13 11:40:26 +08:00
Wesley Squasher
f6b52a653a Use TCP when appropriate for the given query type/response 2019-05-12 08:17:52 +00:00
B. Modi
9a07f5b856 Added Quad9 servers in config. Good for malware threat prevention. 2019-05-10 13:50:17 -07:00
Star Brilliant
8787921faf Merge pull request #43 from modib/macos-build-error-fix
Make Makefile compatible with swift5 and older swift versions
2019-05-11 02:13:12 +08:00
B. Modi
1642730af0 Make Makefile compatible with swift5 and older swift versions 2019-05-10 11:02:07 -07:00
Ming Aldrich-Gan
2332d9b7c1 Add local_addr configuration for doh-server (#39)
* Add local_addr configuration for doh-server

This commit adds a `local_addr` string value to `doh-server.conf`, specifying the IP address and port from which outgoing calls to upstream DNS resolvers should originate. This value is set as the `udpClient`'s and `tcpClient`'s `Dialer.LocalAddr` when initializing a `NewServer`. If the value is left empty in `doh-server.conf`, it defaults to the first `listen` address (which in turn defaults to `"127.0.0.1:8053"`).

One use case for this would be if `doh-server` is proxying requests to a local DNS resolver (e.g. `unbound` or Pi-hole). Up to version 2.0.0, all DNS queries from `doh-server` are sent from `127.0.0.1` (even if the `listen` address is set to a different loopback IP address), making it hard to distinguish them from all other local DNS queries from the same machine in the query logs.

* Revert defaulting of local_addr to listen address

This commit reverts to the existing behavior when `conf.LocalAddr == ""`, i.e. letting `dns.Client` instantiate its own `Dialer` with the default local address.

* Fixup comment in configuration file

* Log errors from Dialer instantiation (e.g. if LocalAddr port is missing)

* Fixup other comment in configuration file

* Return error and log fatal
2019-03-25 04:01:32 +08:00
Star Brilliant
7f5a23584f Release 2.0.1 2019-03-24 19:11:12 +08:00
Sherlock Holo
17e5848178 Fix random selector (#41)
* Fix a bug: when only have one upstream, random selector will panic

Signed-off-by: Sherlock Holo <sherlockya@gmail.com>
2019-03-24 09:13:14 +08:00
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
37 changed files with 1988 additions and 175 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,12 +4,60 @@ 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.2.1
- Fix messy log
## Version 2.2.0
- Breaking change: The configuration format of doh-server is changed
- Add support for type prefix for upstream addresses of doh-server
- Add support for DNS-over-TLS upstream addresses of doh-server
- Remove `tcp_only` configuration option in doh-server
- Add `no_user_agent` configuration option in doh-server
- Add an RPM package script with SELinux policy
- Fix Opcode never assigned in `jsonDNS.PrepareReply`
- Improve error logging / checking
- Updated Readme
## Version 2.1.2
- Update address for google's resolver
- Fix a typo
## Version 2.1.1
- Add a set of Dockerfile contributed by the community
- Include DNS.SB's resolver in example configuration
## Version 2.1.0
- Add `local_addr` configuration for doh-server (#39)
- Fix a problem when compiling on macOS 10.14.4 or newer
- Add Quad9 DoH server to the example `doh-client.conf`
- Use TCP when appropriate for the given query type/response (AXFR/IXFR)
## Version 2.0.1
- Fix a crash with the random load balancing algorithm.
## 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 ## 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: `debug_http_headers` (e.g. Add `CF-Ray` to diagnose Cloudflare's resolver)
- Add a configuration option: `passrthrough` - Add a configuration option: `passrthrough`
- macOS logger is rebuilt with static libswiftCore - macOS logger is rebuilt with static libswiftCore
- Fix a segfault when `no_cookies=true`
- Fix HTTP stream leaking problem, which may cause massive half-open connections if HTTP/1 is in use - 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. - Utilize Go's cancelable context to detect timeouts more reliably.
- Fix interoperation problems with gDNS - Fix interoperation problems with gDNS

22
Dockerfile.client Normal file
View File

@@ -0,0 +1,22 @@
FROM golang:alpine AS build-env
RUN apk add --no-cache git make
WORKDIR /src
ADD . /src
RUN make doh-client/doh-client
FROM alpine:latest
COPY --from=build-env /src/doh-client/doh-client /doh-client
ADD doh-client/doh-client.conf /doh-client.conf
RUN sed -i '$!N;s/"127.0.0.1:53",.*"127.0.0.1:5380",/":53",/;P;D' /doh-client.conf
RUN sed -i '$!N;s/"\[::1\]:53",.*"\[::1\]:5380",/":5380",/;P;D' /doh-client.conf
EXPOSE 53
EXPOSE 5380
ENTRYPOINT ["/doh-client"]
CMD ["-conf", "/doh-client.conf"]

20
Dockerfile.server Normal file
View File

@@ -0,0 +1,20 @@
FROM golang:alpine AS build-env
RUN apk add --no-cache git make
WORKDIR /src
ADD . /src
RUN make doh-server/doh-server
FROM alpine:latest
COPY --from=build-env /src/doh-server/doh-server /doh-server
ADD doh-server/doh-server.conf /doh-server.conf
RUN sed -i '$!N;s/"127.0.0.1:8053",\s*"\[::1\]:8053",/":8053",/;P;D' /doh-server.conf
EXPOSE 8053
ENTRYPOINT ["/doh-server"]
CMD ["-conf", "/doh-server.conf"]

View File

@@ -59,10 +59,11 @@ uninstall:
deps: deps:
@# I am not sure if it is the correct way to keep the common library updated @# I am not sure if it is the correct way to keep the common library updated
$(GOGET_UPDATE) github.com/m13253/dns-over-https/doh-client/config
$(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

@@ -44,7 +44,7 @@ To test your configuration, type:
dig www.google.com dig www.google.com
If it is OK, you will wee: If it is OK, you will see:
;; SERVER: 127.0.0.1#53(127.0.0.1) ;; SERVER: 127.0.0.1#53(127.0.0.1)
@@ -79,6 +79,81 @@ you can host DNS-over-HTTPS along with other HTTPS services.
HTTP/2 with at least TLS v1.3 is recommended. OCSP stapling must be enabled, HTTP/2 with at least TLS v1.3 is recommended. OCSP stapling must be enabled,
otherwise DNS recursion may happen. otherwise DNS recursion may happen.
### Example configuration: Apache
SSLProtocol TLSv1.2
SSLHonorCipherOrder On
SSLCipherSuite ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+3DES:!aNULL:!MD5:!DSS:!eNULL:!EXP:!LOW:!MD5
SSLUseStapling on
SSLStaplingCache shmcb:/var/lib/apache2/stapling_cache(512000)
<VirtualHost *:443>
ServerName MY_SERVER_NAME
Protocols h2 http/1.1
ProxyPass /dns-query http://[::1]:8053/dns-query
ProxyPassReverse /dns-query http://[::1]:8053/dns-query
</VirtualHost>
(Credit: [Joan Moreau](https://github.com/m13253/dns-over-https/issues/51#issuecomment-526820884))
### Example configuration: Nginx
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name MY_SERVER_NAME;
server_tokens off;
ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.3 requires nginx >= 1.13.0
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -dsaparam -out /etc/nginx/dhparam.pem 4096
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
ssl_early_data off; # 0-RTT, enable if desired - Requires nginx >= 1.15.4
resolver 1.1.1.1 valid=300s; # Replace with your local resolver
resolver_timeout 5s;
# HTTP Security Headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=63072000";
ssl_certificate /path/to/your/server/certificates/fullchain.pem;
ssl_certificate_key /path/to/your/server/certificates/privkey.pem;
location /dns-query {
proxy_pass http://localhost:8053/dns-query;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
(Credit: [Cipherli.st](https://cipherli.st/))
### Example configuration: Caddy
https://MY_SERVER_NAME {
log / syslog "{remote} - {user} [{when}] \"{method} {scheme}://{host}{uri} {proto}\" {status} {size} \"{>Referer}\" \"{>User-Agent}\" {>X-Forwarded-For}"
errors syslog
gzip
proxy /dns-query http://[::1]:18053 {
header_upstream Host {host}
header_upstream X-Real-IP {remote}
header_upstream X-Forwarded-For {>X-Forwarded-For},{remote}
header_upstream X-Forwarded-Proto {scheme}
}
root /var/www
tls {
ciphers ECDHE-ECDSA-WITH-CHACHA20-POLY1305 ECDHE-RSA-WITH-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256
curves X25519 p384 p521
must_staple
}
}
## DNSSEC ## DNSSEC
DNS-over-HTTPS is compatible with DNSSEC, and requests DNSSEC signatures by DNS-over-HTTPS is compatible with DNSSEC, and requests DNSSEC signatures by

View File

@@ -0,0 +1,36 @@
diff -Naur dns-over-https-2.1.2.org/systemd/doh-client.service dns-over-https-2.1.2/systemd/doh-client.service
--- dns-over-https-2.1.2.org/systemd/doh-client.service 2019-09-10 12:08:35.177574074 +0200
+++ dns-over-https-2.1.2/systemd/doh-client.service 2019-09-10 12:10:05.473700374 +0200
@@ -7,12 +7,12 @@
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
-ExecStart=/usr/local/bin/doh-client -conf /etc/dns-over-https/doh-client.conf
+ExecStart=/usr/bin/doh-client -conf /etc/dns-over-https/doh-client.conf
LimitNOFILE=1048576
Restart=always
RestartSec=3
Type=simple
-User=nobody
+User=doh-client
[Install]
WantedBy=multi-user.target
diff -Naur dns-over-https-2.1.2.org/systemd/doh-server.service dns-over-https-2.1.2/systemd/doh-server.service
--- dns-over-https-2.1.2.org/systemd/doh-server.service 2019-09-10 12:08:35.177574074 +0200
+++ dns-over-https-2.1.2/systemd/doh-server.service 2019-09-10 12:10:20.980273992 +0200
@@ -5,12 +5,12 @@
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
-ExecStart=/usr/local/bin/doh-server -conf /etc/dns-over-https/doh-server.conf
+ExecStart=/usr/bin/doh-server -conf /etc/dns-over-https/doh-server.conf
LimitNOFILE=1048576
Restart=always
RestartSec=3
Type=simple
-User=nobody
+User=doh-server
[Install]
WantedBy=multi-user.target

240
contrib/rpm/doh.spec Normal file
View File

@@ -0,0 +1,240 @@
# vim: tabstop=4 shiftwidth=4 expandtab
%global _hardened_build 1
# Debug package is empty anyway
%define debug_package %{nil}
%global _release 1
%global provider github
%global provider_tld com
%global project m13253
%global repo dns-over-https
%global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo}
%global import_path %{provider_prefix}
#define commit 984df34ca7b45897ecb5871791e398cc160a4b93
%if 0%{?commit:1}
%define shortcommit %(c=%{commit}; echo ${c:0:7})
%define _date %(date +'%%Y%%m%%dT%%H%%M%%S')
%endif
%define rand_id %(head -c20 /dev/urandom|od -An -tx1|tr -d '[[:space:]]')
%if ! 0%{?gobuild:1}
%define gobuild(o:) go build -ldflags "${LDFLAGS:-} -B 0x%{rand_id}" -a -v -x %{?**};
%endif
%if ! 0%{?gotest:1}
%define gotest() go test -ldflags "${LDFLAGS:-}" %{?**}
%endif
Name: %{repo}
Version: 2.1.2
%if 0%{?commit:1}
Release: %{_release}.git%{shortcommit}.%{_date}%{?dist}
Source0: https://%{import_path}/archive/%{commit}.tar.gz
%else
Release: %{_release}%{?dist}
Source0: https://%{import_path}/archive/v%{version}.tar.gz
%endif
Patch0: %{name}-%{version}-systemd.patch
Summary: High performance DNS over HTTPS client & server
License: MIT
URL: https://github.com/m13253/dns-over-https
# e.g. el6 has ppc64 arch without gcc-go, so EA tag is required
# If go_compiler is not set to 1, there is no virtual provide. Use golang instead.
#BuildRequires: %{?go_compiler:compiler(go-compiler)}%{!?go_compiler:golang} >= 1.10
BuildRequires: golang >= 1.10
BuildRequires: systemd
BuildRequires: upx
%description
%{summary}
%package common
BuildArch: noarch
Summary: %{summary} - common files
%description common
%{summary}
%package server
ExclusiveArch: %{?go_arches:%{go_arches}}%{!?go_arches:%{ix86} x86_64 %{arm}}
Summary: %{summary} - Server
Requires(pre): shadow-utils
Requires(post): systemd
Requires(preun): systemd
Requires(postun): systemd
%description server
%{summary}
%package client
ExclusiveArch: %{?go_arches:%{go_arches}}%{!?go_arches:%{ix86} x86_64 %{arm}}
Summary: %{summary} - Client
Requires(pre): shadow-utils
Requires(post): systemd
Requires(preun): systemd
Requires(postun): systemd
%description client
%{summary}
%package selinux
BuildArch: noarch
Source3: doh_server.fc
Source4: doh_server.if
Source5: doh_server.te
Source6: doh_client.fc
Source7: doh_client.if
Source8: doh_client.te
BuildRequires: selinux-policy
BuildRequires: selinux-policy-devel
Requires: %{name}
Requires(post): policycoreutils
Requires(post): policycoreutils-python
Requires(postun): policycoreutils
Summary: SELinux policy for %{name}
%description selinux
%summary
%prep
%if 0%{?commit:1}
%autosetup -n %{name}-%{commit} -p1
%else
%autosetup -n %{name}-%{version} -p1
%endif
mkdir -p selinux
cp %{SOURCE3} %{SOURCE4} %{SOURCE5} %{SOURCE6} %{SOURCE7} %{SOURCE8} selinux
%build
cd selinux
make -f /usr/share/selinux/devel/Makefile doh_server.pp doh_client.pp || exit
cd -
%set_build_flags
%make_build \
PREFIX=%{_prefix} \
GOBUILD="go build -ldflags \"-s -w -B 0x%{rand_id}\" -a -v -x"
%install
%make_install \
PREFIX=%{_prefix}
install -Dpm 0600 selinux/doh_server.pp %{buildroot}%{_datadir}/selinux/packages/doh_server.pp
install -Dpm 0644 selinux/doh_server.if %{buildroot}%{_datadir}/selinux/devel/include/contrib/doh_server.if
install -Dpm 0600 selinux/doh_client.pp %{buildroot}%{_datadir}/selinux/packages/doh_client.pp
install -Dpm 0644 selinux/doh_client.if %{buildroot}%{_datadir}/selinux/devel/include/contrib/doh_client.if
mkdir -p %{buildroot}%{_docdir}/%{name}
mv %{buildroot}%{_sysconfdir}/%{name}/*.example %{buildroot}%{_docdir}/%{name}
mkdir -p %{buildroot}%{_libdir}
mv %{buildroot}%{_sysconfdir}/NetworkManager %{buildroot}%{_libdir}/
for i in $(find %{_buildroot}%{_bindir} -type f)
do
upx $i
done
%files common
%license LICENSE
%doc Changelog.md Readme.md
%files server
%{_libdir}/NetworkManager/dispatcher.d/doh-server
%{_docdir}/%{name}/doh-server.conf.example
%config(noreplace) %{_sysconfdir}/%{name}/doh-server.conf
%{_bindir}/doh-server
%{_unitdir}/doh-server.service
%files client
%{_libdir}/NetworkManager/dispatcher.d/doh-client
%{_docdir}/%{name}/doh-client.conf.example
%config(noreplace) %{_sysconfdir}/%{name}/doh-client.conf
%{_bindir}/doh-client
%{_unitdir}/doh-client.service
%pre server
test -d %{_sharedstatedir}/home || mkdir -p %{_sharedstatedir}/home
getent group doh-server > /dev/null || groupadd -r doh-server
getent passwd doh-server > /dev/null || \
useradd -r -d %{_sharedstatedir}/home/doh-server -g doh-server \
-s /sbin/nologin -c "%{name} - server" doh-server
exit 0
%pre client
test -d %{_sharedstatedir}/home || mkdir -p %{_sharedstatedir}/home
getent group doh-client > /dev/null || groupadd -r doh-client
getent passwd doh-client > /dev/null || \
useradd -r -d %{_sharedstatedir}/home/doh-client -g doh-client \
-s /sbin/nologin -c "%{name} - client" doh-client
exit 0
%post server
%systemd_post doh-server.service
%preun server
%systemd_preun doh-server.service
%postun server
%systemd_postun_with_restart doh-server.service
%post client
%systemd_post doh-client.service
%preun client
%systemd_preun doh-client.service
%postun client
%systemd_postun_with_restart doh-client.service
%files selinux
%{_datadir}/selinux/packages/doh_server.pp
%{_datadir}/selinux/devel/include/contrib/doh_server.if
%{_datadir}/selinux/packages/doh_client.pp
%{_datadir}/selinux/devel/include/contrib/doh_client.if
%post selinux
semodule -n -i %{_datadir}/selinux/packages/doh_server.pp
semodule -n -i %{_datadir}/selinux/packages/doh_client.pp
if /usr/sbin/selinuxenabled ; then
/usr/sbin/load_policy
/usr/sbin/fixfiles -R %{name}-server restore
/usr/sbin/fixfiles -R %{name}-client restore
fi;
semanage -i - << __eof
port -a -t doh_server_port_t -p tcp "8053"
port -a -t doh_client_port_t -p udp "5380"
__eof
exit 0
%postun selinux
if [ $1 -eq 0 ]; then
semanage -i - << __eof
port -d -t doh_server_port_t -p tcp "8053"
port -d -t doh_client_port_t -p udp "5380"
__eof
semodule -n -r doh_server
semodule -n -r doh_client
if /usr/sbin/selinuxenabled ; then
/usr/sbin/load_policy
/usr/sbin/fixfiles -R %{name}-server restore
/usr/sbin/fixfiles -R %{name}-client restore
fi;
fi;
exit 0
%changelog
* Tue Sep 10 2019 fuero <fuerob@gmail.com> 2.1.2-1
- initial package

View File

@@ -0,0 +1,2 @@
/usr/bin/doh-client -- gen_context(system_u:object_r:doh_client_exec_t,s0)
/usr/lib/systemd/system/doh-client.service -- gen_context(system_u:object_r:doh_client_unit_file_t,s0)

103
contrib/rpm/doh_client.if Normal file
View File

@@ -0,0 +1,103 @@
## <summary>policy for doh_client</summary>
########################################
## <summary>
## Execute doh_client_exec_t in the doh_client domain.
## </summary>
## <param name="domain">
## <summary>
## Domain allowed to transition.
## </summary>
## </param>
#
interface(`doh_client_domtrans',`
gen_require(`
type doh_client_t, doh_client_exec_t;
')
corecmd_search_bin($1)
domtrans_pattern($1, doh_client_exec_t, doh_client_t)
')
######################################
## <summary>
## Execute doh_client in the caller domain.
## </summary>
## <param name="domain">
## <summary>
## Domain allowed access.
## </summary>
## </param>
#
interface(`doh_client_exec',`
gen_require(`
type doh_client_exec_t;
')
corecmd_search_bin($1)
can_exec($1, doh_client_exec_t)
')
########################################
## <summary>
## Execute doh_client server in the doh_client domain.
## </summary>
## <param name="domain">
## <summary>
## Domain allowed to transition.
## </summary>
## </param>
#
interface(`doh_client_systemctl',`
gen_require(`
type doh_client_t;
type doh_client_unit_file_t;
')
systemd_exec_systemctl($1)
systemd_read_fifo_file_passwd_run($1)
allow $1 doh_client_unit_file_t:file read_file_perms;
allow $1 doh_client_unit_file_t:service manage_service_perms;
ps_process_pattern($1, doh_client_t)
')
########################################
## <summary>
## All of the rules required to administrate
## an doh_client environment
## </summary>
## <param name="domain">
## <summary>
## Domain allowed access.
## </summary>
## </param>
## <param name="role">
## <summary>
## Role allowed access.
## </summary>
## </param>
## <rolecap/>
#
interface(`doh_client_admin',`
gen_require(`
type doh_client_t;
type doh_client_unit_file_t;
')
allow $1 doh_client_t:process { signal_perms };
ps_process_pattern($1, doh_client_t)
tunable_policy(`deny_ptrace',`',`
allow $1 doh_client_t:process ptrace;
')
doh_client_systemctl($1)
admin_pattern($1, doh_client_unit_file_t)
allow $1 doh_client_unit_file_t:service all_service_perms;
optional_policy(`
systemd_passwd_agent_exec($1)
systemd_read_fifo_file_passwd_run($1)
')
')

49
contrib/rpm/doh_client.te Normal file
View File

@@ -0,0 +1,49 @@
policy_module(doh_client, 1.0.0)
########################################
#
# Declarations
#
type doh_client_t;
type doh_client_exec_t;
init_daemon_domain(doh_client_t, doh_client_exec_t)
type doh_client_port_t;
corenet_port(doh_client_port_t)
type doh_client_unit_file_t;
systemd_unit_file(doh_client_unit_file_t)
########################################
#
# doh_client local policy
#
allow doh_client_t self:fifo_file rw_fifo_file_perms;
allow doh_client_t self:unix_stream_socket create_stream_socket_perms;
allow doh_client_t self:capability net_bind_service;
allow doh_client_t self:process execmem;
allow doh_client_t self:tcp_socket { accept bind connect create getattr getopt listen read setopt write };
allow doh_client_t self:udp_socket { bind connect create getattr read setopt write };
allow doh_client_t doh_client_exec_t:file execmod;
allow doh_client_t doh_client_port_t:tcp_socket name_bind;
corenet_tcp_bind_dns_port(doh_client_t)
corenet_tcp_bind_generic_node(doh_client_t)
corenet_tcp_connect_http_port(doh_client_t)
corenet_udp_bind_dns_port(doh_client_t)
corenet_udp_bind_generic_node(doh_client_t)
corenet_udp_bind_generic_port(doh_client_t)
kernel_read_net_sysctls(doh_client_t)
kernel_search_network_sysctl(doh_client_t)
miscfiles_read_certs(doh_client_t)
sysnet_read_config(doh_client_t)
domain_use_interactive_fds(doh_client_t)
files_read_etc_files(doh_client_t)
miscfiles_read_localization(doh_client_t)

View File

@@ -0,0 +1,2 @@
/usr/bin/doh-server -- gen_context(system_u:object_r:doh_server_exec_t,s0)
/usr/lib/systemd/system/doh-server.service -- gen_context(system_u:object_r:doh_server_unit_file_t,s0)

122
contrib/rpm/doh_server.if Normal file
View File

@@ -0,0 +1,122 @@
## <summary>policy for doh_server</summary>
########################################
## <summary>
## Execute doh_server_exec_t in the doh_server domain.
## </summary>
## <param name="domain">
## <summary>
## Domain allowed to transition.
## </summary>
## </param>
#
interface(`doh_server_domtrans',`
gen_require(`
type doh_server_t, doh_server_exec_t;
')
corecmd_search_bin($1)
domtrans_pattern($1, doh_server_exec_t, doh_server_t)
')
######################################
## <summary>
## Execute doh_server in the caller domain.
## </summary>
## <param name="domain">
## <summary>
## Domain allowed access.
## </summary>
## </param>
#
interface(`doh_server_exec',`
gen_require(`
type doh_server_exec_t;
')
corecmd_search_bin($1)
can_exec($1, doh_server_exec_t)
')
########################################
## <summary>
## Execute doh_server server in the doh_server domain.
## </summary>
## <param name="domain">
## <summary>
## Domain allowed to transition.
## </summary>
## </param>
#
interface(`doh_server_systemctl',`
gen_require(`
type doh_server_t;
type doh_server_unit_file_t;
')
systemd_exec_systemctl($1)
systemd_read_fifo_file_passwd_run($1)
allow $1 doh_server_unit_file_t:file read_file_perms;
allow $1 doh_server_unit_file_t:service manage_service_perms;
ps_process_pattern($1, doh_server_t)
')
########################################
## <summary>
## All of the rules required to administrate
## an doh_server environment
## </summary>
## <param name="domain">
## <summary>
## Domain allowed access.
## </summary>
## </param>
## <param name="role">
## <summary>
## Role allowed access.
## </summary>
## </param>
## <rolecap/>
#
interface(`doh_server_admin',`
gen_require(`
type doh_server_t;
type doh_server_unit_file_t;
')
allow $1 doh_server_t:process { signal_perms };
ps_process_pattern($1, doh_server_t)
tunable_policy(`deny_ptrace',`',`
allow $1 doh_server_t:process ptrace;
')
doh_server_systemctl($1)
admin_pattern($1, doh_server_unit_file_t)
allow $1 doh_server_unit_file_t:service all_service_perms;
optional_policy(`
systemd_passwd_agent_exec($1)
systemd_read_fifo_file_passwd_run($1)
')
')
########################################
## <summary>
## Make a TCP connection to the vault_ocsp_responder port.
## </summary>
## <param name="domain">
## <summary>
## Domain allowed access.
## </summary>
## </param>
#
interface(`doh_server_connect',`
gen_require(`
type doh_server_port_t;
type $1;
')
allow $1 doh_server_port_t:tcp_socket name_connect;
')

42
contrib/rpm/doh_server.te Normal file
View File

@@ -0,0 +1,42 @@
policy_module(doh_server, 1.0.0)
require {
class process execmem;
class tcp_socket { accept bind create read write getattr listen setopt connect getopt };
class udp_socket { connect create getattr setopt read write };
class file execmod;
}
type doh_server_t;
type doh_server_exec_t;
init_daemon_domain(doh_server_t, doh_server_exec_t)
type doh_server_port_t;
corenet_port(doh_server_port_t)
type doh_server_unit_file_t;
systemd_unit_file(doh_server_unit_file_t)
allow doh_server_t self:fifo_file rw_fifo_file_perms;
allow doh_server_t self:unix_stream_socket create_stream_socket_perms;
allow doh_server_t self:process execmem;
allow doh_server_t self:tcp_socket { accept read write bind create getattr listen setopt connect getopt};
allow doh_server_t self:udp_socket { connect create getattr setopt read write };
allow doh_server_t doh_server_exec_t:file execmod;
allow doh_server_t doh_server_port_t:tcp_socket name_bind;
domain_use_interactive_fds(doh_server_t)
files_read_etc_files(doh_server_t)
corenet_tcp_bind_generic_node(doh_server_t)
corenet_tcp_connect_dns_port(doh_server_t)
doh_server_connect(httpd_t)
kernel_read_net_sysctls(doh_server_t)
kernel_search_network_sysctl(doh_server_t)
miscfiles_read_localization(doh_server_t)

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 -static-stdlib $< $(SWIFTC) -o $@ -O $<
clean: clean:
rm -f doh-logger rm -f doh-logger

View File

@@ -31,11 +31,14 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url"
"strconv" "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"
@@ -43,7 +46,7 @@ import (
) )
type Client struct { type Client struct {
conf *config conf *config.Config
bootstrap []string bootstrap []string
passthrough []string passthrough []string
udpClient *dns.Client udpClient *dns.Client
@@ -56,6 +59,7 @@ type Client struct {
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 {
@@ -68,7 +72,7 @@ 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,
} }
@@ -78,11 +82,11 @@ func NewClient(conf *config) (c *Client, err error) {
c.udpClient = &dns.Client{ c.udpClient = &dns.Client{
Net: "udp", Net: "udp",
UDPSize: dns.DefaultMsgSize, UDPSize: dns.DefaultMsgSize,
Timeout: time.Duration(conf.Timeout) * time.Second, Timeout: time.Duration(conf.Other.Timeout) * time.Second,
} }
c.tcpClient = &dns.Client{ c.tcpClient = &dns.Client{
Net: "tcp", Net: "tcp",
Timeout: time.Duration(conf.Timeout) * time.Second, 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{
@@ -98,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")
@@ -120,9 +124,9 @@ func NewClient(conf *config) (c *Client, err error) {
return conn, err return conn, err
}, },
} }
if len(conf.Passthrough) != 0 { if len(conf.Other.Passthrough) != 0 {
c.passthrough = make([]string, len(conf.Passthrough)) c.passthrough = make([]string, len(conf.Other.Passthrough))
for i, passthrough := range conf.Passthrough { for i, passthrough := range conf.Other.Passthrough {
if punycode, err := idna.ToASCII(passthrough); err != nil { if punycode, err := idna.ToASCII(passthrough); err != nil {
passthrough = punycode passthrough = punycode
} }
@@ -133,7 +137,7 @@ func NewClient(conf *config) (c *Client, err error) {
// 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
@@ -147,22 +151,92 @@ func NewClient(conf *config) (c *Client, err error) {
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{
@@ -172,9 +246,9 @@ func (c *Client) newHTTPClient() error {
MaxIdleConns: 100, MaxIdleConns: 100,
MaxIdleConnsPerHost: 10, MaxIdleConnsPerHost: 10,
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
TLSHandshakeTimeout: time.Duration(c.conf.Timeout) * time.Second, TLSHandshakeTimeout: time.Duration(c.conf.Other.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"
@@ -206,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 {
@@ -213,14 +290,15 @@ 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) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.conf.Timeout)*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.conf.Other.Timeout)*time.Second)
defer cancel() defer cancel()
if r.Response == true { if r.Response {
log.Println("Received a response packet") log.Println("Received a response packet")
return return
} }
@@ -246,7 +324,7 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
} else { } else {
questionType = strconv.FormatUint(uint64(question.Qtype), 10) questionType = strconv.FormatUint(uint64(question.Qtype), 10)
} }
if c.conf.Verbose { 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) fmt.Printf("%s - - [%s] \"%s %s %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType)
} }
@@ -273,7 +351,7 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
} else { } else {
reply, _, err = c.tcpClient.Exchange(r, upstream) reply, _, err = c.tcpClient.Exchange(r, upstream)
} }
if err == nil || err == dns.ErrTruncated { if err == nil {
w.WriteMsg(reply) w.WriteMsg(reply)
return return
} }
@@ -284,64 +362,80 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
return return
} }
requestType := "" upstream := c.selector.Get()
if len(c.conf.UpstreamIETF) == 0 { requestType := upstream.RequestType
requestType = "application/dns-json"
} else if len(c.conf.UpstreamGoogle) == 0 { if c.conf.Other.Verbose {
requestType = "application/dns-message" log.Println("choose upstream:", upstream)
} else {
numServers := len(c.conf.UpstreamGoogle) + len(c.conf.UpstreamIETF)
random := rand.Intn(numServers)
if random < len(c.conf.UpstreamGoogle) {
requestType = "application/dns-json"
} else {
requestType = "application/dns-message"
}
} }
var req *DNSRequest var req *DNSRequest
if requestType == "application/dns-json" { switch requestType {
req = c.generateRequestGoogle(ctx, w, r, isTCP) case "application/dns-json":
} else if requestType == "application/dns-message" { req = c.generateRequestGoogle(ctx, w, r, isTCP, upstream)
req = c.generateRequestIETF(ctx, 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.response != 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
}
// if req.err == nil, req.response != nil
defer req.response.Body.Close() defer req.response.Body.Close()
for _, header := range c.conf.DebugHTTPHeaders {
for _, header := range c.conf.Other.DebugHTTPHeaders {
if value := req.response.Header.Get(header); value != "" { if value := req.response.Header.Get(header); value != "" {
log.Printf("%s: %s\n", header, value) log.Printf("%s: %s\n", header, value)
} }
} }
}
if req.err != nil {
return
}
contentType := ""
candidateType := strings.SplitN(req.response.Header.Get("Content-Type"), ";", 2)[0] candidateType := strings.SplitN(req.response.Header.Get("Content-Type"), ";", 2)[0]
if candidateType == "application/json" {
contentType = "application/json" switch candidateType {
} else if candidateType == "application/dns-message" { case "application/json":
contentType = "application/dns-message" c.parseResponseGoogle(ctx, w, r, isTCP, req)
} else if candidateType == "application/dns-udpwireformat" {
contentType = "application/dns-message" case "application/dns-message", "application/dns-udpwireformat":
} else { c.parseResponseIETF(ctx, w, r, isTCP, req)
if requestType == "application/dns-json" {
contentType = "application/json" default:
} else if requestType == "application/dns-message" { switch requestType {
contentType = "application/dns-message" 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")
} }
} }
if contentType == "application/json" { // https://developers.cloudflare.com/1.1.1.1/dns-over-https/request-structure/ says
c.parseResponseGoogle(ctx, w, r, isTCP, req) // returns code will be 200 / 400 / 413 / 415 / 504, some server will return 503, so
} else if contentType == "application/dns-message" { // I think if status code is 5xx, upstream must has some problems
c.parseResponseIETF(ctx, w, r, isTCP, req) /*if req.response.StatusCode/100 == 5 {
} else { c.selector.ReportUpstreamStatus(upstream, selector.Medium)
panic("Unknown response Content-Type") }*/
switch req.response.StatusCode / 100 {
case 5:
c.selector.ReportUpstreamStatus(upstream, selector.Error)
case 2:
c.selector.ReportUpstreamStatus(upstream, selector.OK)
} }
} }
@@ -360,7 +454,7 @@ var (
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 {

View File

@@ -21,7 +21,7 @@
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
*/ */
package main package config
import ( import (
"fmt" "fmt"
@@ -29,22 +29,43 @@ 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"
)
type upstreamDetail struct {
URL string `toml:"url"`
Weight int32 `toml:"weight"`
}
type upstream struct {
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"` Bootstrap []string `toml:"bootstrap"`
Passthrough []string `toml:"passthrough"` Passthrough []string `toml:"passthrough"`
Timeout uint `toml:"timeout"` Timeout uint `toml:"timeout"`
NoCookies bool `toml:"no_cookies"` NoCookies bool `toml:"no_cookies"`
NoECS bool `toml:"no_ecs"` NoECS bool `toml:"no_ecs"`
NoIPv6 bool `toml:"no_ipv6"` NoIPv6 bool `toml:"no_ipv6"`
NoUserAgent bool `toml:"no_user_agent"`
Verbose bool `toml:"verbose"` Verbose bool `toml:"verbose"`
DebugHTTPHeaders []string `toml:"debug_http_headers"` DebugHTTPHeaders []string `toml:"debug_http_headers"`
} }
func loadConfig(path string) (*config, error) { type Config struct {
conf := &config{} 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
@@ -56,11 +77,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

@@ -4,43 +4,59 @@ listen = [
"127.0.0.1:5380", "127.0.0.1:5380",
"[::1]:53", "[::1]:53",
"[::1]:5380", "[::1]:5380",
## To listen on both 0.0.0.0:53 and [::]:53, use the following line
# ":53",
] ]
# 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 resolver, good ECS, good DNSSEC
upstream_ietf = [ #[[upstream.upstream_ietf]]
# url = "https://dns.google/dns-query"
# weight = 50
# Google's experimental resolver, good ECS, good DNSSEC ## CloudFlare's resolver, bad ECS, good DNSSEC
#"https://dns.google.com/experimental", ## ECS is disabled for privacy by design: https://developers.cloudflare.com/1.1.1.1/nitty-gritty-details/#edns-client-subnet
[[upstream.upstream_ietf]]
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", ## ECS is disabled for privacy by design: https://developers.cloudflare.com/1.1.1.1/nitty-gritty-details/#edns-client-subnet
#"https://1.1.1.1/dns-query", ## Note that some ISPs have problems connecting to 1.1.1.1, try 1.0.0.1 if problems happen.
#"https://1.0.0.1/dns-query", #[[upstream.upstream_ietf]]
# url = "https://1.1.1.1/dns-query"
# weight = 50
# CloudFlare's resolver for Tor, available only with Tor ## DNS.SB's resolver, good ECS, good DNSSEC
# Remember to disable ECS below when using Tor! ## The provider claims no logging: https://dns.sb/doh/
# Blog: https://blog.cloudflare.com/welcome-hidden-resolver/ #[[upstream.upstream_ietf]]
#"https://dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion/dns-query", # url = "https://doh.dns.sb/dns-query"
# weight = 50
] ## Quad9's resolver, bad ECS, good DNSSEC
## ECS is disabled for privacy by design: https://www.quad9.net/faq/#What_is_EDNS_Client-Subnet
#[[upstream.upstream_ietf]]
# url = "https://9.9.9.9/dns-query"
# 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.
@@ -76,7 +92,7 @@ passthrough = [
"time.windows.com", "time.windows.com",
] ]
# Timeout for upstream request # Timeout for upstream request in seconds
timeout = 30 timeout = 30
# Disable HTTP Cookies # Disable HTTP Cookies
@@ -103,5 +119,16 @@ no_ecs = false
# Note that DNS listening and bootstrapping is not controlled by this option. # Note that DNS listening and bootstrapping is not controlled by this option.
no_ipv6 = false no_ipv6 = false
# Disable submitting User-Agent
#
# It is generally not recommended to disable submitting User-Agent because it
# is still possible to probe client version according to behavior differences,
# such as TLS handshaking, handling of malformed packets, and specific bugs.
# Additionally, User-Agent is an important way for the server to distinguish
# buggy, old, or insecure clients, and to workaround specific bugs.
# (e.g. doh-server can detect and workaround certain issues of DNSCrypt-Proxy
# and older Firefox.)
no_user_agent = false
# Enable logging # Enable logging
verbose = false verbose = false

View File

@@ -29,17 +29,17 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"math/rand"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"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(ctx context.Context, 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 {
question := &r.Question[0] question := &r.Question[0]
questionName := question.Name questionName := question.Name
questionClass := question.Qclass questionClass := question.Qclass
@@ -58,9 +58,7 @@ func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter
questionType = strconv.FormatUint(uint64(question.Qtype), 10) questionType = strconv.FormatUint(uint64(question.Qtype), 10)
} }
numServers := len(c.conf.UpstreamGoogle) requestURL := fmt.Sprintf("%s?ct=application/dns-json&name=%s&type=%s", upstream.URL, url.QueryEscape(questionName), url.QueryEscape(questionType))
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"
@@ -76,7 +74,7 @@ func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter
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 := jsonDNS.PrepareReply(r)
@@ -86,19 +84,28 @@ func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter
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")
if !c.conf.Other.NoUserAgent {
req.Header.Set("User-Agent", USER_AGENT) req.Header.Set("User-Agent", USER_AGENT)
} else {
req.Header.Set("User-Agent", "")
}
req = req.WithContext(ctx) 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 err == context.DeadlineExceeded {
// 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 // Do not respond, silently fail to prevent caching of SERVFAIL
log.Println(err) log.Println(err)
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
} }*/
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsonDNS.PrepareReply(r) reply := jsonDNS.PrepareReply(r)
@@ -115,12 +122,12 @@ func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter
udpSize: udpSize, udpSize: udpSize,
ednsClientAddress: ednsClientAddress, ednsClientAddress: ednsClientAddress,
ednsClientNetmask: ednsClientNetmask, ednsClientNetmask: ednsClientNetmask,
currentUpstream: upstream, currentUpstream: upstream.URL,
} }
} }
func (c *Client) parseResponseGoogle(ctx context.Context, 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

@@ -30,17 +30,17 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"math/rand"
"net" "net"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/m13253/dns-over-https/json-dns" "github.com/m13253/dns-over-https/doh-client/selector"
jsonDNS "github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func (c *Client) generateRequestIETF(ctx context.Context, 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 {
opt := r.IsEdns0() opt := r.IsEdns0()
udpSize := uint16(512) udpSize := uint16(512)
if opt == nil { if opt == nil {
@@ -100,13 +100,11 @@ func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter,
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-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 {
log.Println(err) log.Println(err)
reply := jsonDNS.PrepareReply(r) reply := jsonDNS.PrepareReply(r)
@@ -117,7 +115,7 @@ func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter,
} }
} }
} 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 {
log.Println(err) log.Println(err)
reply := jsonDNS.PrepareReply(r) reply := jsonDNS.PrepareReply(r)
@@ -130,18 +128,25 @@ func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter,
req.Header.Set("Content-Type", "application/dns-message") req.Header.Set("Content-Type", "application/dns-message")
} }
req.Header.Set("Accept", "application/dns-message, application/dns-udpwireformat, application/json") req.Header.Set("Accept", "application/dns-message, application/dns-udpwireformat, application/json")
if !c.conf.Other.NoUserAgent {
req.Header.Set("User-Agent", USER_AGENT) req.Header.Set("User-Agent", USER_AGENT)
} else {
req.Header.Set("User-Agent", "")
}
req = req.WithContext(ctx) 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 err == context.DeadlineExceeded {
// 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 // Do not respond, silently fail to prevent caching of SERVFAIL
log.Println(err) log.Println(err)
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
} }*/
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsonDNS.PrepareReply(r) reply := jsonDNS.PrepareReply(r)
@@ -158,12 +163,12 @@ func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter,
udpSize: udpSize, udpSize: udpSize,
ednsClientAddress: ednsClientAddress, ednsClientAddress: ednsClientAddress,
ednsClientNetmask: ednsClientNetmask, ednsClientNetmask: ednsClientNetmask,
currentUpstream: upstream, currentUpstream: upstream.URL,
} }
} }
func (c *Client) parseResponseIETF(ctx context.Context, 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")
@@ -175,7 +180,7 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
body, err := ioutil.ReadAll(req.response.Body) body, err := ioutil.ReadAll(req.response.Body)
if err != nil { if err != nil {
log.Println(err) log.Printf("read error from upstream %s: %v\n", req.currentUpstream, err)
req.reply.Rcode = dns.RcodeServerFailure req.reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(req.reply) w.WriteMsg(req.reply)
return return
@@ -186,7 +191,7 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
if nowDate, err := time.Parse(http.TimeFormat, headerNow); err == nil { if nowDate, err := time.Parse(http.TimeFormat, headerNow); err == nil {
now = nowDate now = nowDate
} else { } else {
log.Println(err) log.Printf("Date header parse error from upstream %s: %v\n", req.currentUpstream, err)
} }
} }
headerLastModified := req.response.Header.Get("Last-Modified") headerLastModified := req.response.Header.Get("Last-Modified")
@@ -195,7 +200,7 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
if lastModifiedDate, err := time.Parse(http.TimeFormat, headerLastModified); err == nil { if lastModifiedDate, err := time.Parse(http.TimeFormat, headerLastModified); err == nil {
lastModified = lastModifiedDate lastModified = lastModifiedDate
} else { } else {
log.Println(err) log.Printf("Last-Modified header parse error from upstream %s: %v\n", req.currentUpstream, err)
} }
} }
timeDelta := now.Sub(lastModified) timeDelta := now.Sub(lastModified)
@@ -205,8 +210,8 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
fullReply := new(dns.Msg) fullReply := new(dns.Msg)
err = fullReply.Unpack(body) err = fullReply.Unpack(body)
if err != nil && err != dns.ErrTruncated { if err != nil {
log.Println(err) log.Printf("unpacking error from upstream %s: %v\n", req.currentUpstream, err)
req.reply.Rcode = dns.RcodeServerFailure req.reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(req.reply) w.WriteMsg(req.reply)
return return
@@ -228,7 +233,7 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
buf, err := fullReply.Pack() buf, err := fullReply.Pack()
if err != nil { if err != nil {
log.Println(err) log.Printf("packing error with upstream %s: %v\n", req.currentUpstream, err)
req.reply.Rcode = dns.RcodeServerFailure req.reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(req.reply) w.WriteMsg(req.reply)
return return
@@ -237,12 +242,15 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
fullReply.Truncated = true fullReply.Truncated = true
buf, err = fullReply.Pack() buf, err = fullReply.Pack()
if err != nil { if err != nil {
log.Println(err) log.Printf("re-packing error with upstream %s: %v\n", req.currentUpstream, err)
return return
} }
buf = buf[:req.udpSize] buf = buf[:req.udpSize]
} }
w.Write(buf) _, err = w.Write(buf)
if err != nil {
log.Printf("failed to write to client: %v\n", err)
}
} }
func fixRecordTTL(rr dns.RR, delta time.Duration) dns.RR { func fixRecordTTL(rr dns.RR, delta time.Duration) dns.RR {

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-client %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))]
}
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.4.1" VERSION = "2.2.1"
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

@@ -25,21 +25,23 @@ package main
import ( import (
"fmt" "fmt"
"regexp"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
type config struct { type config struct {
Listen []string `toml:"listen"` Listen []string `toml:"listen"`
LocalAddr string `toml:"local_addr"`
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"`
Verbose bool `toml:"verbose"` Verbose bool `toml:"verbose"`
DebugHTTPHeaders []string `toml:"debug_http_headers"` 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) {
@@ -60,7 +62,7 @@ func loadConfig(path string) (*config, error) {
conf.Path = "/dns-query" conf.Path = "/dns-query"
} }
if len(conf.Upstream) == 0 { if len(conf.Upstream) == 0 {
conf.Upstream = []string{"8.8.8.8:53", "8.8.4.4:53"} conf.Upstream = []string{"udp:8.8.8.8:53", "udp:8.8.4.4:53"}
} }
if conf.Timeout == 0 { if conf.Timeout == 0 {
conf.Timeout = 10 conf.Timeout = 10
@@ -73,9 +75,35 @@ func loadConfig(path string) (*config, error) {
return nil, &configError{"You must specify both -cert and -key to enable TLS"} return nil, &configError{"You must specify both -cert and -key to enable TLS"}
} }
// validate all upstreams
for _, us := range conf.Upstream {
address, t := addressAndType(us)
if address == "" {
return nil, &configError{"One of the upstreams has not a (udp|tcp|tcp-tls) prefix e.g. udp:1.1.1.1:53"}
}
switch t {
case "tcp", "udp", "tcp-tls":
// OK
default:
return nil, &configError{"Invalid upstream prefix specified, choose one of: udp tcp tcp-tls"}
}
}
return conf, nil return conf, nil
} }
var rxUpstreamWithTypePrefix = regexp.MustCompile("^[a-z-]+(:)")
func addressAndType(us string) (string, string) {
p := rxUpstreamWithTypePrefix.FindStringSubmatchIndex(us)
if len(p) != 4 {
return "", ""
}
return us[p[2]+1:], us[:p[2]]
}
type configError struct { type configError struct {
err string err string
} }

View File

@@ -2,8 +2,15 @@
listen = [ listen = [
"127.0.0.1:8053", "127.0.0.1:8053",
"[::1]:8053", "[::1]:8053",
## To listen on both 0.0.0.0:8053 and [::]:8053, use the following line
# ":8053",
] ]
# Local address and port for upstream DNS
# If left empty, a local address is automatically chosen.
local_addr = ""
# TLS certification file # TLS certification file
# If left empty, plain-text HTTP will be used. # If left empty, plain-text HTTP will be used.
# You are recommended to leave empty and to use a server load balancer (e.g. # You are recommended to leave empty and to use a server load balancer (e.g.
@@ -20,11 +27,16 @@ 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.
# You can use "udp", "tcp" or "tcp-tls" for the type prefix.
# For "udp", UDP will first be used, and switch to TCP when the server asks to
# or the response is too large.
# For "tcp", only TCP will be used.
# For "tcp-tls", DNS-over-TLS (RFC 7858) will be used to secure the upstream connection.
upstream = [ upstream = [
"1.1.1.1:53", "udp:1.1.1.1:53",
"1.0.0.1:53", "udp:1.0.0.1:53",
"8.8.8.8:53", "udp:8.8.8.8:53",
"8.8.4.4:53", "udp:8.8.4.4:53",
] ]
# Upstream timeout # Upstream timeout
@@ -33,8 +45,9 @@ timeout = 10
# Number of tries if upstream DNS fails # Number of tries if upstream DNS fails
tries = 3 tries = 3
# Only use TCP for DNS query
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

@@ -30,12 +30,13 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/m13253/dns-over-https/json-dns" jsonDNS "github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@@ -94,8 +95,16 @@ func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r
} else { } else {
questionType = strconv.FormatUint(uint64(question.Qtype), 10) 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
msg.Id = dns.Id() msg.Id = dns.Id()
@@ -151,7 +160,7 @@ func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter
req.response.Id = req.transactionID req.response.Id = req.transactionID
respBytes, err := req.response.Pack() respBytes, err := req.response.Pack()
if err != nil { if err != nil {
log.Println(err) log.Printf("DNS packet construct failure with upstream %s: %v\n", req.currentUpstream, err)
jsonDNS.FormatError(w, fmt.Sprintf("DNS packet construct failure (%s)", err.Error()), 500) jsonDNS.FormatError(w, fmt.Sprintf("DNS packet construct failure (%s)", err.Error()), 500)
return return
} }
@@ -174,9 +183,13 @@ func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter
} }
if respJSON.Status == dns.RcodeServerFailure { if respJSON.Status == dns.RcodeServerFailure {
log.Printf("received server failure from upstream %s: %v\n", req.currentUpstream, req.response)
w.WriteHeader(503) w.WriteHeader(503)
} }
w.Write(respBytes) _, err = w.Write(respBytes)
if err != nil {
log.Printf("failed to write to client: %v\n", err)
}
} }
// Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe // Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe

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)
@@ -42,6 +110,9 @@ func main() {
conf.Verbose = true conf.Verbose = true
} }
server := NewServer(conf) server, err := NewServer(conf)
if err != nil {
log.Fatalln(err)
}
_ = server.Start() _ = server.Start()
} }

View File

@@ -35,7 +35,7 @@ import (
"time" "time"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/m13253/dns-over-https/json-dns" jsonDNS "github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@@ -43,6 +43,7 @@ type Server struct {
conf *config conf *config
udpClient *dns.Client udpClient *dns.Client
tcpClient *dns.Client tcpClient *dns.Client
tcpClientTLS *dns.Client
servemux *http.ServeMux servemux *http.ServeMux
} }
@@ -56,22 +57,49 @@ type DNSRequest struct {
errtext string errtext string
} }
func NewServer(conf *config) (s *Server) { func NewServer(conf *config) (*Server, error) {
s = &Server{ timeout := time.Duration(conf.Timeout) * time.Second
s := &Server{
conf: conf, conf: conf,
udpClient: &dns.Client{ udpClient: &dns.Client{
Net: "udp", Net: "udp",
UDPSize: dns.DefaultMsgSize, UDPSize: dns.DefaultMsgSize,
Timeout: time.Duration(conf.Timeout) * time.Second, Timeout: timeout,
}, },
tcpClient: &dns.Client{ tcpClient: &dns.Client{
Net: "tcp", Net: "tcp",
Timeout: time.Duration(conf.Timeout) * time.Second, Timeout: timeout,
},
tcpClientTLS: &dns.Client{
Net: "tcp-tls",
Timeout: timeout,
}, },
servemux: http.NewServeMux(), servemux: http.NewServeMux(),
} }
if conf.LocalAddr != "" {
udpLocalAddr, err := net.ResolveUDPAddr("udp", conf.LocalAddr)
if err != nil {
return nil, err
}
tcpLocalAddr, err := net.ResolveTCPAddr("tcp", conf.LocalAddr)
if err != nil {
return nil, err
}
s.udpClient.Dialer = &net.Dialer{
Timeout: timeout,
LocalAddr: udpLocalAddr,
}
s.tcpClient.Dialer = &net.Dialer{
Timeout: timeout,
LocalAddr: tcpLocalAddr,
}
s.tcpClientTLS.Dialer = &net.Dialer{
Timeout: timeout,
LocalAddr: tcpLocalAddr,
}
}
s.servemux.HandleFunc(conf.Path, s.handlerFunc) s.servemux.HandleFunc(conf.Path, s.handlerFunc)
return return s, nil
} }
func (s *Server) Start() error { func (s *Server) Start() error {
@@ -244,21 +272,52 @@ func (s *Server) patchRootRD(req *DNSRequest) *DNSRequest {
return req return req
} }
// Return the position index for the question of qtype from a DNS msg, otherwise return -1
func (s *Server) indexQuestionType(msg *dns.Msg, qtype uint16) int {
for i, question := range msg.Question {
if question.Qtype == qtype {
return i
}
}
return -1
}
func (s *Server) doDNSQuery(ctx context.Context, 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. // 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 {
req.response, _, err = s.udpClient.Exchange(req.request, req.currentUpstream) upstream, t := addressAndType(req.currentUpstream)
if err == dns.ErrTruncated {
log.Println(err) switch t {
req.response, _, err = s.tcpClient.Exchange(req.request, req.currentUpstream) default:
} log.Printf("invalid DNS type %q in upstream %q", t, upstream)
return nil, &configError{"invalid DNS type"}
// Use DNS-over-TLS (DoT) if configured to do so
case "tcp-tls":
req.response, _, err = s.tcpClientTLS.Exchange(req.request, upstream)
case "tcp", "udp":
// Use TCP if always configured to or if the Query type dictates it (AXFR)
if t == "tcp" || (s.indexQuestionType(req.request, dns.TypeAXFR) > -1) {
req.response, _, err = s.tcpClient.Exchange(req.request, upstream)
} else { } else {
req.response, _, err = s.tcpClient.Exchange(req.request, req.currentUpstream) req.response, _, err = s.udpClient.Exchange(req.request, upstream)
if err == nil && req.response != nil && req.response.Truncated {
log.Println(err)
req.response, _, err = s.tcpClient.Exchange(req.request, upstream)
} }
if err == nil || err == dns.ErrTruncated {
// Retry with TCP if this was an IXFR request and we only received an SOA
if (s.indexQuestionType(req.request, dns.TypeIXFR) > -1) &&
(len(req.response.Answer) == 1) &&
(req.response.Answer[0].Header().Rrtype == dns.TypeSOA) {
req.response, _, err = s.tcpClient.Exchange(req.request, upstream)
}
}
}
if err == nil {
return req, nil return req, nil
} }
log.Printf("DNS error from upstream %s: %s\n", req.currentUpstream, err.Error()) log.Printf("DNS error from upstream %s: %s\n", req.currentUpstream, err.Error())

View File

@@ -24,6 +24,6 @@
package main package main
const ( const (
VERSION = "1.4.1" VERSION = "2.2.1"
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)"
) )

12
go.mod Normal file
View File

@@ -0,0 +1,12 @@
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.22
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
)

36
go.sum Normal file
View File

@@ -0,0 +1,36 @@
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.14 h1:wkQWn9wIp4mZbwW8XV6Km6owkvRPbOiV004ZM2CkGvA=
github.com/miekg/dns v1.1.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc=
github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a h1:Yu34BogBivvmu7SAzHHaB9nZWH5D1C+z3F1jyIaYZSQ=
golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190621203818-d432491b9138 h1:t8BZD9RDjkm9/h7yYN6kE8oaeov5r9aztkB7zKA5Tkg=
golang.org/x/sys v0.0.0-20190621203818-d432491b9138/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -38,7 +38,7 @@ func PrepareReply(req *dns.Msg) *dns.Msg {
reply := new(dns.Msg) reply := new(dns.Msg)
reply.Id = req.Id reply.Id = req.Id
reply.Response = true reply.Response = true
reply.Opcode = reply.Opcode reply.Opcode = req.Opcode
reply.RecursionDesired = req.RecursionDesired reply.RecursionDesired = req.RecursionDesired
reply.RecursionAvailable = req.RecursionDesired reply.RecursionAvailable = req.RecursionDesired
reply.CheckingDisabled = req.CheckingDisabled reply.CheckingDisabled = req.CheckingDisabled