Compare commits

...

228 Commits

Author SHA1 Message Date
gdm85
80e95cd028 docs: mention poor compatibility with dnscrypt-proxy 2022-09-03 10:32:29 +02:00
gdm85
1d59772fad docs: mention where to find logs 2022-09-03 10:31:14 +02:00
Star Brilliant
a375bea95d Merge pull request #131 from GreyXor/master
chore: upgrade deps
2022-08-17 16:07:35 +00:00
GreyXor
b98b01cc4e chore: upgrade deps 2022-08-17 10:32:42 +02:00
Star Brilliant
6276bed46f Pre-bump version to 2.3.2 2022-06-01 02:57:07 +00:00
Star Brilliant
19737361ad Release 2.3.1 2022-06-01 02:56:35 +00:00
Star Brilliant
791d2d43dd Merge pull request #128 from GreyXor/master 2022-05-31 11:31:46 +00:00
GreyXor
90753c3910 ci: update setup go version 2022-05-31 12:07:30 +02:00
GreyXor
221240a840 chore: update dependencies 2022-05-31 12:00:18 +02:00
StarBrilliant
b338c7ae52 Bump to version 2.3.1 2021-09-13 10:31:04 +00:00
StarBrilliant
9fd69439c4 Release 2.3.0 2021-09-13 10:30:38 +00:00
Star Brilliant
10eb8f5c87 Merge pull request #116 from leiless/gh-115-go-import-v2
GH#115: Fix Go module semver import
2021-09-13 05:27:45 +00:00
Fishbone
8cd4c4205d gh-115-go-import-v2: Suffix Go module path with /v2 2021-09-12 17:12:39 +08:00
Fishbone
63c6c1de91 gh-115-go-import-v2: Run go mod tidy 2021-09-12 17:09:53 +08:00
Star Brilliant
f25e9a706d Merge pull request #112 from 1574242600/patch-1
Fix not working example docker command
2021-07-15 19:04:41 -04:00
Nworm
a2e3b0cd4b readme: fix not working example docker command 2021-07-15 22:40:57 +08:00
Star Brilliant
f172a7b7fb Merge pull request #110 from gdm85/fix/simplify
Simplify doDNSQuery call
2021-05-15 12:16:35 +00:00
Star Brilliant
56a01679ad Merge pull request #108 from gdm85/fix/add-gh-action
Add GitHub actions to build master and each PR
2021-05-14 16:39:28 +00:00
Star Brilliant
05c3b1676d Merge pull request #107 from gdm85/fix/verbose-logging
Log for response patching only when verbose is enabled
2021-05-13 21:17:46 +00:00
gdm85
5af0d538ca Remove deps download, modern Go does it automatically 2021-05-13 19:20:46 +02:00
gdm85
0bbd26c1b5 Use Go 1.13 2021-05-13 19:19:44 +02:00
gdm85
8a13f085a6 Simplify doDNSQuery call 2021-05-13 19:15:54 +02:00
gdm85
849bc584cc Add GitHub actions to build master and each PR 2021-05-13 19:00:58 +02:00
gdm85
5f8371817b Log for response patching only when verbose is enabled 2021-05-13 18:54:09 +02:00
Star Brilliant
2e36b4ebcd New minimum Go version 2021-04-02 22:23:50 +00:00
Star Brilliant
02dbd9d954 Bump to version 2.2.6 2021-03-25 14:31:03 +00:00
Star Brilliant
0a76416f8e Release 2.2.5 2021-03-25 14:30:44 +00:00
Star Brilliant
82c50163c1 Merge pull request #99 from amincheloh/patch-1
Fix not working example docker command
2021-01-26 19:24:34 +08:00
Amin Cheloh
d5c1c592f6 Fix not working example docker command 2021-01-25 14:30:26 +07:00
Alex Chauvin
1cf98e87c9 add client certificate authentication (#98)
* add client certificate authentication
* fix #97 for ECS forward local addresses
2021-01-08 08:34:25 +00:00
Satish Gaikwad
e7461f2d85 Documentation update: Docker example update (#96)
* Set traefik container version to 2.3 in docker-compose example. This supports recent lets encrypt changes.
* Remove docker swarm related references. Docker swarm example is no more valid. Docker compose is the best example available atm.
2020-12-27 13:25:25 +00:00
Star Brilliant
608394e2d2 Bump to version 2.2.5 2020-12-06 22:53:33 +00:00
Star Brilliant
eb166ececa Release 2.2.4 2020-12-06 22:52:35 +00:00
Star Brilliant
f557e4aa29 Reformat the code 2020-11-24 12:38:16 +00:00
Alex Chauvin
967faec56c add options for ECS full subnet mask in server & TLS verification bypass in client (#92)
* add ECS full size & limit filtering

* add tls certification bypass in configuration

* flush log lines

* changes following pull request comments

* with fmt and reorg of libs in client.go
2020-11-24 12:35:23 +00:00
Star Brilliant
2aa7370aaf Bump to version 2.2.4 2020-11-22 13:27:30 +00:00
Star Brilliant
b63e86bab3 Release 2.2.3 2020-11-22 13:26:41 +00:00
Star Brilliant
7c96cd4436 Merge pull request #91 from dwoffinden/patch-1
Fix an inconsistency in the example doh-client.conf
2020-11-22 13:17:44 +00:00
Daniel Woffinden
f5f1a8f3f4 Fix an inconsistency in the example doh-client.conf
Above, it was said that 8.8.8.8 had good ECS, so don't contradict that further down.

This confused a reviewer of https://github.com/NixOS/nixpkgs/pull/104530 :)
2020-11-22 12:18:12 +00:00
Star Brilliant
4f46b89feb Resolve (some) linter warnings 2020-08-02 05:58:24 +08:00
Star Brilliant
2c7e70466e Rewrite globalip_test 2020-08-02 05:53:21 +08:00
Star Brilliant
88f9ef84d1 Merge pull request #83 from sanyo0714/globalip_use_iptree
Use ipTree to determine the global IP
2020-08-02 05:02:15 +08:00
Star Brilliant
63bceea638 Merge branch 'master' into globalip_use_iptree 2020-08-02 05:01:56 +08:00
Star Brilliant
16120fdc11 Bump to version 2.2.3 2020-08-02 04:44:31 +08:00
Star Brilliant
b2fcfb706c Release 2.2.2 2020-08-02 04:43:22 +08:00
Star Brilliant
64e9375e3b Merge pull request #85 from leiless/alidns-json-Question
json-dns/response.go: Fix variant question response in Response.Question
2020-08-02 02:46:52 +08:00
leixiang
34feec9f5d json-dns/response.go: Fix variant question response in Response.Question
Known affected DoH server:
    https://www.alidns.com/faqs/#dns-safe
2020-08-01 13:26:35 +08:00
Star Brilliant
6d30a12d5f Pass X-Real-IP to handlers.CombinedLoggingHandler
Note that X-Forwarded-For or guessed client IP are not used due to security concerns.
This should fix issue #71.
2020-07-30 20:44:18 +08:00
sanyo
0c878a6ad7 change git ignore 2020-07-29 10:50:38 +08:00
Star Brilliant
a8aed7e09a Use ExchangeClient for DNS request 2020-07-26 22:09:24 +08:00
sanyo
31ea7c520d Use ipTree to determine the global IP 2020-07-16 17:11:34 +08:00
Star Brilliant
6e99d8153a Merge pull request #78 from leiless/DNSSEC-OK
[JSON-DOH] Honor DNSSEC OK flag for incoming DNS requests
2020-04-19 14:49:55 +08:00
Star Brilliant
2d4495a0dd Merge pull request #79 from leiless/json-fix-empty-rr-names
[JSON-DOH] Fix DNS response empty []RR.Name
2020-04-19 14:49:07 +08:00
leixiang
b30056a590 doh-client/google.go: [JSON-DOH] Fix DNS response empty []RR.Name
Cloudflare JSON DOH may return empty RR names if r.Question[0].Name is "."
Which causes malformed DNS response
2020-04-19 11:28:28 +08:00
leixiang
b92da52539 .gitignore: Ignore make output binaries 2020-04-19 11:02:16 +08:00
leixiang
f43d2c69e0 doh-client/google.go: [JSON-DOH] Honor DNSSEC OK flag for incoming DNS requests 2020-04-19 10:57:03 +08:00
Star Brilliant
5f1f418664 Merge pull request #77 from Henrocker/patch-1
Upgrade Caddyfile config to v2 and drastically reduce size.
2020-04-10 23:30:01 +08:00
Henrik
9a316a56a8 Upgrade Caddyfile config to v2 and drastically reduce size.
Since Caddy v2 is in RC state, config should be updates to v2. Also config has been simplified a lot.

Running example with this config: https://dns.hnrk.io/dns

Cheers and happy easter 😊!
2020-04-10 17:05:37 +02:00
Star Brilliant
81b977ca11 Merge pull request #76 from satishweb/master
Docker compose based doh-server setup example
2020-04-05 20:42:36 +08:00
Satish Gaikwad
b7d252de7b Added arch types 2020-04-05 02:45:01 -07:00
Satish Gaikwad
e12b87b48d Enhanced documentation. Added Docker compose based doh-server deployment example. 2020-04-05 02:33:20 -07:00
Star Brilliant
09bdfe2b14 Merge pull request #75 from buckaroogeek/pihole
Added pi-hole as potential dnssec validator
2020-04-04 15:21:35 +08:00
Bradley G Smith
a84b65dd56 expose container to port 53/upd 2020-04-03 10:58:57 -07:00
Bradley G Smith
540f6e3043 Added pi-hole as potential dnssec validator 2020-04-03 08:07:34 -07:00
Star Brilliant
7db67db247 Merge pull request #74 from satishweb/master
Added example configuration for Docker Flow Proxy + Docker
2020-04-03 10:49:39 +08:00
Satish Gaikwad
026d89ac8d Added info on ipv6 support. Added simple docker run example in installation section 2020-04-02 10:42:33 -07:00
Satish Gaikwad
8228ea6299 Added example configuration for Docker Flow Proxy + Docker 2020-04-01 14:54:09 -07:00
Star Brilliant
59f79fb666 Merge pull request #67 from monperrus/patch-1
doc: document upstream_selector
2020-02-05 00:41:24 +08:00
Martin Monperrus
502caabd15 doc: document upstream_selector
fix #66
2020-02-04 15:39:29 +00:00
Star Brilliant
f151c90e9d Merge pull request #61 from m13253/feature/no_ecs_arg
Allow client opt-out of EDNS0 Client Subnet
2019-11-11 12:21:41 +08:00
James Swineson
d8e3969640 add no_ecs argument in query string support 2019-11-11 10:11:25 +08:00
Star Brilliant
475ef65f57 Bump version to 2.2.2 2019-10-29 06:07:30 +08:00
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
Star Brilliant
3affb2c227 Release 1.4.1 2018-11-10 22:59:37 +08:00
Star Brilliant
7c7b7d969d Add detectportal.firefox.com to default passthrough list 2018-11-08 23:58:28 +08:00
Star Brilliant
4754aa0414 Enable CORS by default, which is necessary for AJAX resolver to run 2018-11-07 20:13:51 +08:00
Star Brilliant
2d9c9eba50 Detect context.DeadlineExceeded 2018-11-07 19:41:55 +08:00
Star Brilliant
c51be0e69c Use context for more functions 2018-11-07 19:25:46 +08:00
Star Brilliant
95ec839409 Put cancel() earlier 2018-11-07 19:10:06 +08:00
Star Brilliant
502fe6b048 Use RCODE_REFUSED for unsupported Qclass 2018-11-07 18:56:22 +08:00
Star Brilliant
f8b40c4bfc Try to use context.WithTimeout to detect HTTP timeout. Hopefully it might work. 2018-11-07 18:47:01 +08:00
Star Brilliant
bb1e21778a Slightly change the log format 2018-11-07 18:11:12 +08:00
Star Brilliant
afa0d563d0 Add passthrough feature, tests are welcome 2018-11-07 17:10:39 +08:00
Star Brilliant
017a18f20c Fix HTTP stream leaking problem 2018-11-06 14:46:45 +08:00
Star Brilliant
0577ff6dca Merge pull request #28 from Chaz6/patch-1
doh-server: change to google.go
2018-11-02 10:48:33 +08:00
Chris Hills
ef2c6bbdc8 Update google.go
Make "cd" check case-insensitive.
2018-11-01 20:12:28 +00:00
Chris Hills
4d742bd15e doh-server: change to google.go
Allow the "cd" parameter to be case insensitive to work with some clients that send True/False instead of true/false such as gDNS.
2018-10-31 23:40:33 +00:00
Star Brilliant
3b112b946e Congratulations RFC 8484, remove the word "draft" from Readme 2018-10-20 13:59:37 +08:00
Star Brilliant
6d19cbb9ad Congratulations RFC 8484, remove the word "draft" from Readme 2018-10-20 13:58:26 +08:00
Star Brilliant
b094a8d4fd Update Readme, fix issue #27 2018-10-04 23:03:51 +08:00
Star Brilliant
c1f6fe1997 Update Readme 2018-10-04 02:12:55 +08:00
Star Brilliant
1fb3ed3513 Add a ink to a guide 2018-10-04 02:11:55 +08:00
Star Brilliant
c85ef45840 Fix panic with debug_http_headers 2018-09-27 16:46:36 +08:00
Star Brilliant
85d81d3d0b Merge pull request #22 from paulie-g/master
Fix segfault when no_cookies=true
2018-09-24 03:21:00 +10:00
Paul G
ab0eddb0ba Fix segfault when no_cookies=true 2018-09-23 08:25:15 -04:00
Star Brilliant
aa3389b1d0 Build doh-logger with static libswiftCore, fix #20 2018-09-22 04:28:12 +08:00
Star Brilliant
6eb7b29142 Add configuration option: debug_http_headers 2018-09-22 04:23:55 +08:00
Star Brilliant
ea0a769389 Bump version to 1.3.11 2018-08-21 01:44:56 +08:00
Star Brilliant
e480251e67 Release 1.3.10 2018-08-21 01:44:35 +08:00
Star Brilliant
027480afeb Enable application/dns-message (draft-13) by default, since Google has finally supported it 2018-08-21 01:43:46 +08:00
Star Brilliant
4839498ad5 Move linux-install.* to contrib/ 2018-08-14 09:11:09 +08:00
Star Brilliant
a303c21036 Bump version to 1.3.10 2018-08-14 09:08:46 +08:00
Star Brilliant
3586688aa6 Release 1.3.9 2018-08-14 09:08:27 +08:00
Star Brilliant
ffe5573552 Change the ECS prefix length from /48 to /56 for IPv6, per RFC 7871 2018-08-14 09:06:13 +08:00
Star Brilliant
f40116b1f8 Update Readme to instruct Debian users to set $GOROOT 2018-08-14 01:43:41 +08:00
Star Brilliant
58e6cdfb71 If $GOROOT is defined, Makefile should respect the value, fix #8 2018-08-14 01:37:19 +08:00
Star Brilliant
1491138f69 Add 5380 as an additional default doh-client port 2018-08-10 03:50:38 +08:00
Star Brilliant
83df8964d8 Fix #16: doh-client panics when connecting no_cookies = true 2018-07-04 22:43:08 +08:00
Star Brilliant
07f39088d4 Update example configuration 2018-07-02 20:42:11 +08:00
Star Brilliant
db007fbded Update example configuration 2018-07-02 20:40:56 +08:00
Star Brilliant
89d809d469 Bump version to 1.3.9 2018-07-02 20:12:04 +08:00
Star Brilliant
5ca6813801 Release 1.3.8 2018-07-02 20:11:41 +08:00
Star Brilliant
033865e508 Workaround a bug causing Firefox 61-62 to reject responses with Content-Type = application/dns-message 2018-07-02 19:42:57 +08:00
Star Brilliant
7e7cd1ee90 Workaround a bug causing Firefox 61-62 to reject responses with Content-Type = application/dns-message 2018-07-02 17:55:58 +08:00
Star Brilliant
e19250dc99 Workaround a bug causing Firefox 61-62 to reject responses with Content-Type = application/dns-message 2018-07-02 17:49:34 +08:00
Star Brilliant
a64df3f048 Preserve TransactionID 2018-07-02 13:49:59 +08:00
Star Brilliant
f54b49c090 Disable preventing capitalization scrambling 2018-07-02 13:48:05 +08:00
Star Brilliant
87436b7cbf Turn on no_cookies by default according to the IETF draft 2018-07-02 13:45:08 +08:00
Star Brilliant
11056bcad5 Construct a real DNS packet for DNSCrypt-Proxy 2018-06-26 15:20:43 +08:00
Star Brilliant
ca33027e04 Remove offensive words :-) 2018-06-26 14:39:05 +08:00
Star Brilliant
6ff0a6a9ab Update Readme 2018-06-26 13:28:07 +08:00
Star Brilliant
359c81a019 Update Readme 2018-06-26 13:26:42 +08:00
Star Brilliant
07143d5890 Update documentation about TLS
This is to avoid confusions like issue #12
2018-06-26 13:22:32 +08:00
Star Brilliant
1d367cb7e7 Bump version to 1.3.8 2018-06-26 04:10:58 +08:00
Star Brilliant
b132de608f Release 1.3.7 2018-06-26 04:10:31 +08:00
Star Brilliant
523b6120b9 Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe 2018-06-26 03:55:59 +08:00
Star Brilliant
82fe91395d Workaround a bug causing Unbound to refuse returning anything about the root 2018-06-26 03:04:34 +08:00
Star Brilliant
06ce104d2a Fix typo 2018-06-16 19:41:41 +08:00
Star Brilliant
71b8c093c0 Install default configuration files to *.conf.example 2018-06-16 19:40:52 +08:00
Star Brilliant
abdd033310 Print upstream information if error happens 2018-06-12 11:12:13 +08:00
Star Brilliant
461d546082 Add CloudFlare DNS resolver for Tor to the preset 2018-06-06 01:26:36 +08:00
Star Brilliant
51b065eed7 Release 1.3.6 2018-05-27 21:03:51 +08:00
Star Brilliant
a58607dc3d Add an option to disable IPv6, this option is available to client only 2018-05-27 21:00:13 +08:00
Star Brilliant
cf94354b08 Revert "Conflict with systemd-resolved.service"
This reverts commit 88b3c95710.
2018-05-15 19:46:04 +08:00
Star Brilliant
f2e3a642e3 Write a logger for macOS systems 2018-05-08 21:11:03 +08:00
Star Brilliant
88b3c95710 Conflict with systemd-resolved.service 2018-04-29 22:30:43 +08:00
Star Brilliant
93b70f3941 Release 1.3.5 2018-04-27 00:39:48 +08:00
Star Brilliant
76c40fb4dc Bump version to 1.3.5 2018-04-26 23:52:59 +08:00
Star Brilliant
174a465ac8 Limit the frequency of creating HTTP client 2018-04-26 23:52:35 +08:00
Star Brilliant
4cbe7c8c98 Release 1.3.4 2018-04-26 10:33:38 +08:00
Star Brilliant
7839d2c7b1 Bump version to 1.3.4 2018-04-26 10:18:36 +08:00
Star Brilliant
c4b2236cf9 Do not respond to network error, silently fail to prevent caching of SERVFAIL
Hopefully we can improve the availability of DoH under unstable network environments.
2018-04-26 10:17:14 +08:00
Star Brilliant
1b90731f20 Add version.go to Makefile 2018-04-25 18:36:06 +08:00
Star Brilliant
20624acf20 Update documents 2018-04-25 14:23:24 +08:00
Star Brilliant
01385b6d29 Update documents 2018-04-25 14:19:40 +08:00
Star Brilliant
5afdee6315 Put EDNS0 at the beginning of the OPT section 2018-04-25 03:05:06 +08:00
Star Brilliant
874a3613e4 Use dns.DefaultMsgSize instead of magic number 4096 2018-04-25 03:04:31 +08:00
Star Brilliant
dc14a70e9d Use dns.DefaultMsgSize instead of magic number 4096 2018-04-24 20:46:34 +08:00
Star Brilliant
58e4018ab2 Rename variables 2018-04-24 20:43:24 +08:00
Star Brilliant
f4516429ee Take User-Agent out of common library, that would be better for packaging 2018-04-24 14:25:33 +08:00
Star Brilliant
12df47f45f Release 1.3.2 2018-04-17 03:28:36 +08:00
Star Brilliant
450c10a594 Fix version number in User-Agent 2018-04-17 03:27:42 +08:00
Star Brilliant
e7c4450787 Fix build system 2018-04-17 03:27:21 +08:00
Star Brilliant
bd5ef5d61e Fix build 2018-04-16 21:36:26 +08:00
Star Brilliant
ff0e9529cb Update User-Agent 2018-04-16 21:35:33 +08:00
Star Brilliant
627e2d639d Release 1.3.1 2018-04-16 13:59:03 +08:00
Star Brilliant
7d5cf98d2b Fix the "address already in use" issue 2018-04-16 13:58:38 +08:00
Star Brilliant
34adf40b36 Release 1.3.0 2018-04-16 13:35:38 +08:00
Star Brilliant
b9c1bcaad2 Put [::1] into default listen addresses 2018-04-16 13:18:51 +08:00
Star Brilliant
47df06b6e2 Merge pull request #9 from gdm85/master
Add client/server support for multiple listen addresses
2018-04-16 03:36:30 +08:00
gdm85
1abba72898 Add server support for multiple listen addresses 2018-04-15 19:57:17 +02:00
gdm85
ce656ac3f7 Add client support for multiple listen addresses 2018-04-15 19:57:01 +02:00
Star Brilliant
83f20767ea Update to IETF draft-07 2018-04-13 02:46:03 +08:00
Star Brilliant
07db7ba200 Merge pull request #7 from joubin/master
Linux (Ubuntu) easy install
2018-04-13 02:32:03 +08:00
Joubin Jabbari
cdb8599c9f Made changes per pull request feedback #7. 2018-04-12 08:24:33 -10:00
Joubin Jabbari
196207631b Added instructions and script for easy installation under Ubuntu
Tested with:

  * Ubuntu 16.04 LTS
  * Raspbian (Jesse, Pi 3 B+)
2018-04-09 16:17:45 -10:00
49 changed files with 3388 additions and 525 deletions

37
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Go build for Linux
on: [push, pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-18.04
steps:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
id: go
- name: Check out repository
uses: actions/checkout@v2
- name: Linux build
run: |
make
- name: Upload Linux build
uses: actions/upload-artifact@v2
with:
name: linux-amd64
path: |
doh-client/doh-client
doh-server/doh-server
- name: Cache
uses: actions/cache@v2
with:
# A directory to store and save the cache
path: ~/go/pkg/mod
# An explicit key for restoring and saving the cache
key: ${{ runner.os }}-${{ hashFiles('**/go.sum') }}

6
.gitignore vendored
View File

@@ -3,6 +3,9 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
darwin-wrapper/doh-logger
doh-client/doh-client
doh-server/doh-server
# Test binary, build with `go test -c` # Test binary, build with `go test -c`
*.test *.test
@@ -12,3 +15,6 @@
# 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/
vendor/

209
Changelog.md Normal file
View File

@@ -0,0 +1,209 @@
# Changelog
This Changelog records major changes between versions.
Not all changes are recorded. Please check git log for details.
## Version 2.3.1
- No new features in this release
- Bumped versions of Go toolchain and third-party dependencies, requested by #128
## Version 2.3.0
- The repository now conforms to the Go semvar standard (Fixed #115, thanks to @leiless)
## Version 2.2.5
- Add client certificate authentication
- Fixing documentation related to Docker
## Version 2.2.4
- Add options to configure ECS netmask length
- Add an option to disable TLS verification (Note: dangerous)
## Version 2.2.3
- Use the library ipTree to determine whether an IP is global routable, improving the performance
- Google's 8.8.8.8 resolver is now marked as "Good ECS" in the example configuration file
## Version 2.2.2
- Allow client to opt-out EDNS0 Client Support
- [JSON-DoH] Honor DNSSEC OK flag for incoming DNS requests
- [JSON-DoH] Add support for non-standard response formats
- `X-Real-IP` is now used in logging if set by frontend load balancer
- Fix documentation
## 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
- Add a configuration option: `debug_http_headers` (e.g. Add `CF-Ray` to diagnose Cloudflare's resolver)
- Add a configuration option: `passrthrough`
- macOS logger is rebuilt with static libswiftCore
- Fix HTTP stream leaking problem, which may cause massive half-open connections if HTTP/1 is in use
- Utilize Go's cancelable context to detect timeouts more reliably.
- Fix interoperation problems with gDNS
- CORS is enabled by default in doh-server
- Documentation updates
## Version 1.3.10
- Enable application/dns-message (draft-13) by default, since Google has finally supported it
## Version 1.3.9
- Fix client crash with `no_cookies = true`
- Add 5380 as an additional default doh-client port
- If `$GOROOT` is defined, Makefile now respects the value for the convenience of Debian/Ubuntu users
- Change the ECS prefix length from /48 to /56 for IPv6, per RFC 7871
## Version 1.3.8
- Workaround a bug causing Firefox 61-62 to reject responses with Content-Type = application/dns-message
- Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe
- TransactionID is now preserved to maintain compatibility with some clients
- Turn on `no_cookies` by default according to the IETF draft
- Update Documentation
## Version 1.3.7
- Add CloudFlare DNS resolver for Tor to the preset
- It is now able to print upstream information if error happens
- Updated default configuration files are now installed to `*.conf.example`
- Workaround a bug causing Unbound to refuse returning anything about the root
- Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe
## Version 1.3.6
- We have a logger for macOS platform now, so logs can be sent to Console.app
- Add an option to disable IPv6, this option is available to client only
## Version 1.3.5
- Limit the frequency of creating HTTP client on bad network condition
## Version 1.3.4
- doh-client now silently fails in case of network error to prevent caching of SERVFAIL
- EDNS0 is now inserted to the beginning of OPT section, to ensure DNSSEC signatures are at the end
- Improve building system
- Update documents
## Version 1.3.3
- Take User-Agent out of common library, that would be better for packaging
## Version 1.3.2
- Fix version string in HTTP User-Agent
## Version 1.3.1
- Fix the "address already in use" issue
## Version 1.3.0
- Breaking change: Add client / server support for multiple listen address
The `listen` option in the configuration file is a list now
## Version 1.2.1
- Update protocol to IETF draft-07
- Update installation documentations for Ubuntu / Debian
## Version 1.2.0
- Add installation documentations for Ubuntu / Debian
- Include CloudFlare DOH server (1.1.1.1, 1.0.0.1) in default configuration
- Fix a problem causing `go get` to fail due to relative paths
- Add documentation about `/etc/hosts` preloading
## Version 1.1.4
- Add `no_cookies` option
- Add documentation on privacy issues
- Adapt for CloudFlare DNS service
- Fix a problem causing a single network failure blocking future requests
- Add experimental macOS support
## Version 1.1.3
- Unsupported Content-Type now generates HTTP error code 415
## Version 1.1.2
- Adapt to IETF protocol
- Optimize for HTTP caches
## Version 1.1.1
- Adapt to IETF protocol
- Optimize for HTTP caches
- Add documentation for uninstallation instructions
- Fix build issues
## Version 1.1.0
- Adpat to IETF protocol
- Fix issues regarding to HTTP caching
- Require Go 1.9 to build now
- Fix systemd issue
## Version 1.0.1
- Fix build issues
## Version 1.0.0
- First release
- Relicense as MIT license

23
Dockerfile.client Normal file
View File

@@ -0,0 +1,23 @@
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/udp
EXPOSE 53/tcp
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

@@ -1,8 +1,13 @@
.PHONY: all clean install uninstall deps .PHONY: all clean install uninstall
GOBUILD=go build
GOGET=go get -d -v
PREFIX = /usr/local PREFIX = /usr/local
ifeq ($(GOROOT),)
GOBUILD = go build
else
GOBUILD = $(GOROOT)/bin/go build
endif
ifeq ($(shell uname),Darwin) ifeq ($(shell uname),Darwin)
CONFDIR = /usr/local/etc/dns-over-https CONFDIR = /usr/local/etc/dns-over-https
else else
@@ -10,9 +15,15 @@ else
endif endif
all: doh-client/doh-client doh-server/doh-server all: doh-client/doh-client doh-server/doh-server
if [ "`uname`" = "Darwin" ]; then \
$(MAKE) -C darwin-wrapper; \
fi
clean: clean:
rm -f doh-client/doh-client doh-server/doh-server rm -f doh-client/doh-client doh-server/doh-server
if [ "`uname`" = "Darwin" ]; then \
$(MAKE) -C darwin-wrapper clean; \
fi
install: install:
[ -e doh-client/doh-client ] || $(MAKE) doh-client/doh-client [ -e doh-client/doh-client ] || $(MAKE) doh-client/doh-client
@@ -21,17 +32,20 @@ install:
install -m0755 doh-client/doh-client "$(DESTDIR)$(PREFIX)/bin/doh-client" install -m0755 doh-client/doh-client "$(DESTDIR)$(PREFIX)/bin/doh-client"
install -m0755 doh-server/doh-server "$(DESTDIR)$(PREFIX)/bin/doh-server" install -m0755 doh-server/doh-server "$(DESTDIR)$(PREFIX)/bin/doh-server"
mkdir -p "$(DESTDIR)$(CONFDIR)/" mkdir -p "$(DESTDIR)$(CONFDIR)/"
install -m0644 doh-client/doh-client.conf "$(DESTDIR)$(CONFDIR)/doh-client.conf.example"
install -m0644 doh-server/doh-server.conf "$(DESTDIR)$(CONFDIR)/doh-server.conf.example"
[ -e "$(DESTDIR)$(CONFDIR)/doh-client.conf" ] || install -m0644 doh-client/doh-client.conf "$(DESTDIR)$(CONFDIR)/doh-client.conf" [ -e "$(DESTDIR)$(CONFDIR)/doh-client.conf" ] || install -m0644 doh-client/doh-client.conf "$(DESTDIR)$(CONFDIR)/doh-client.conf"
[ -e "$(DESTDIR)$(CONFDIR)/doh-server.conf" ] || install -m0644 doh-server/doh-server.conf "$(DESTDIR)$(CONFDIR)/doh-server.conf" [ -e "$(DESTDIR)$(CONFDIR)/doh-server.conf" ] || install -m0644 doh-server/doh-server.conf "$(DESTDIR)$(CONFDIR)/doh-server.conf"
if [ "`uname`" = "Linux" ]; then \ if [ "`uname`" = "Linux" ]; then \
$(MAKE) -C systemd install "DESTDIR=$(DESTDIR)"; \ $(MAKE) -C systemd install "DESTDIR=$(DESTDIR)"; \
$(MAKE) -C NetworkManager install "DESTDIR=$(DESTDIR)"; \ $(MAKE) -C NetworkManager install "DESTDIR=$(DESTDIR)"; \
elif [ "`uname`" = "Darwin" ]; then \ elif [ "`uname`" = "Darwin" ]; then \
$(MAKE) -C darwin-wrapper install "DESTDIR=$(DESTDIR)" "PREFIX=$(PREFIX)"; \
$(MAKE) -C launchd install "DESTDIR=$(DESTDIR)"; \ $(MAKE) -C launchd install "DESTDIR=$(DESTDIR)"; \
fi fi
uninstall: uninstall:
rm -f "$(DESTDIR)$(PREFIX)/bin/doh-client" "$(DESTDIR)$(PREFIX)/bin/doh-server" rm -f "$(DESTDIR)$(PREFIX)/bin/doh-client" "$(DESTDIR)$(PREFIX)/bin/doh-server" "$(DESTDIR)$(CONFDIR)/doh-client.conf.example" "$(DESTDIR)$(CONFDIR)/doh-server.conf.example"
if [ "`uname`" = "Linux" ]; then \ if [ "`uname`" = "Linux" ]; then \
$(MAKE) -C systemd uninstall "DESTDIR=$(DESTDIR)"; \ $(MAKE) -C systemd uninstall "DESTDIR=$(DESTDIR)"; \
$(MAKE) -C NetworkManager uninstall "DESTDIR=$(DESTDIR)"; \ $(MAKE) -C NetworkManager uninstall "DESTDIR=$(DESTDIR)"; \
@@ -39,11 +53,8 @@ uninstall:
$(MAKE) -C launchd uninstall "DESTDIR=$(DESTDIR)"; \ $(MAKE) -C launchd uninstall "DESTDIR=$(DESTDIR)"; \
fi fi
deps: doh-client/doh-client: 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
$(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 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 json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go doh-server/doh-server: 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
cd doh-server && $(GOBUILD) cd doh-server && $(GOBUILD)

287
Readme.md
View File

@@ -2,53 +2,74 @@ DNS-over-HTTPS
============== ==============
Client and server software to query DNS over HTTPS, using [Google DNS-over-HTTPS protocol](https://developers.google.com/speed/public-dns/docs/dns-over-https) Client and server software to query DNS over HTTPS, using [Google DNS-over-HTTPS protocol](https://developers.google.com/speed/public-dns/docs/dns-over-https)
and [draft-ietf-doh-dns-over-https](https://github.com/dohwg/draft-ietf-doh-dns-over-https). and [IETF DNS-over-HTTPS (RFC 8484)](https://www.rfc-editor.org/rfc/rfc8484.txt).
## Easy start ## Guides
Install [Go](https://golang.org), at least version 1.9. - [Tutorial: Setup your own DNS-over-HTTPS (DoH) server](https://www.aaflalo.me/2018/10/tutorial-setup-dns-over-https-server/). (Thanks to Antoine Aflalo)
- [Tutorial: Setup your own Docker based DNS-over-HTTPS (DoH) server](https://github.com/satishweb/docker-doh/blob/master/README.md). (Thanks to Satish Gaikwad)
First create an empty directory, used for `$GOPATH`: ## Installing
### From Source
- Install [Go](https://golang.org), at least version 1.13. The newer the better.
> Note for Debian/Ubuntu users: You need to set `$GOROOT` if you could not get your new version of Go selected by the Makefile.)
- First create an empty directory, used for `$GOPATH`:
```bash
mkdir ~/gopath mkdir ~/gopath
export GOPATH=~/gopath export GOPATH=~/gopath
```
To build the program, type: - To build the program, type:
```bash
make make
```
To install DNS-over-HTTPS as Systemd services, type: - To install DNS-over-HTTPS as Systemd services, type:
```bash
sudo make install sudo make install
```
By default, [Google DNS over HTTPS](https://dns.google.com) is used. It should - By default, [Google DNS over HTTPS](https://dns.google.com) is used. It should
work for most users (except for People's Republic of China). If you need to work for most users (except for People's Republic of China). If you need to
modify the default settings, type: modify the default settings, type:
```bash
sudoedit /etc/dns-over-https/doh-client.conf sudoedit /etc/dns-over-https/doh-client.conf
```
To automatically start DNS-over-HTTPS client as a system service, type: - To automatically start DNS-over-HTTPS client as a system service, type:
```bash
sudo systemctl start doh-client.service sudo systemctl start doh-client.service
sudo systemctl enable doh-client.service sudo systemctl enable doh-client.service
```
- Then, modify your DNS settings (usually with NetworkManager) to 127.0.0.1.
Then, modify your DNS settings (usually with NetworkManager) to 127.0.0.1. - To test your configuration, type:
```bash
To test your configuration, type:
dig www.google.com dig www.google.com
Output:
If it is OK, you will wee:
;; SERVER: 127.0.0.1#53(127.0.0.1) ;; SERVER: 127.0.0.1#53(127.0.0.1)
```
#### Uninstall
### Uninstalling - To uninstall, type:
```bash
To uninstall, type:
sudo make uninstall sudo make uninstall
```
> Note: The configuration files are kept at `/etc/dns-over-https`. Remove them manually if you want.
The configuration files are kept at `/etc/dns-over-https`. Remove them manually if you want. ### Using docker image
```bash
docker run -d --name doh-server \
-p 8053:8053 \
-e UPSTREAM_DNS_SERVER="udp:8.8.8.8:53" \
-e DOH_HTTP_PREFIX="/dns-query" \
-e DOH_SERVER_LISTEN=":8053" \
-e DOH_SERVER_TIMEOUT="10" \
-e DOH_SERVER_TRIES="3" \
-e DOH_SERVER_VERBOSE="false" \
satishweb/doh-server
```
## Logging
All log lines (by either doh-client or doh-server) are written into `stderr`; you can view them using your OS tool of choice (`journalctl` when using systemd).
## Server Configuration ## Server Configuration
@@ -70,17 +91,199 @@ The following is a typical DNS-over-HTTPS architecture:
Although DNS-over-HTTPS can work alone, a HTTP service muxer would be useful as Although DNS-over-HTTPS can work alone, a HTTP service muxer would be useful as
you can host DNS-over-HTTPS along with other HTTPS services. 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,
otherwise DNS recursion may happen.
### Configuration file
The main configuration file is `doh-client.conf`.
**Server selectors.** If several upstream servers are set, one is selected according to `upstream_selector` for each request. With `upstream_selector = "random"`, a random upstream server will be chosen for each request.
```toml
# available selector: random (default) or weighted_round_robin or lvs_weighted_round_robin
upstream_selector = "random"
```
### Example configuration: Apache
```bash
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
```bash
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 (v2)
```bash
my.server.name {
reverse_proxy * localhost:8053
tls your@email.address
try_files {path} {path}/index.php /index.php?{query}
}
```
### Example configuration: Docker Compose + Traefik + Unbound (Raspberry Pi/Linux/Mac) [linux/amd64,linux/arm64,linux/arm/v7]
```yaml
version: '2.2'
networks:
default:
services:
proxy:
# The official v2 Traefik docker image
image: traefik:v2.3
hostname: proxy
networks:
- default
environment:
TRAEFIK_ACCESSLOG: "true"
TRAEFIK_API: "true"
TRAEFIK_PROVIDERS_DOCKER: "true"
TRAEFIK_API_INSECURE: "true"
TRAEFIK_PROVIDERS_DOCKER_NETWORK: "${STACK}_default"
# DNS provider specific environment variables for DNS Challenge using route53 (AWS)
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
AWS_REGION: ${AWS_REGION}
AWS_HOSTED_ZONE_ID: ${AWS_HOSTED_ZONE_ID}
ports:
# The HTTP port
- "80:80"
# The HTTPS port
- "443:443"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
command:
#- "--log.level=DEBUG"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
# Providers list:
# https://docs.traefik.io/https/acme/#providers
# https://go-acme.github.io/lego/dns/
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=route53"
# Enable below line to use staging letsencrypt server.
#- "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.letsencrypt.acme.email=${EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/certs/acme.json"
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
- ./data/proxy/certs:/certs
doh-server:
image: satishweb/doh-server:latest
hostname: doh-server
networks:
- default
environment:
# Enable below line to see more logs
# DEBUG: "1"
UPSTREAM_DNS_SERVER: "udp:unbound:53"
DOH_HTTP_PREFIX: "${DOH_HTTP_PREFIX}"
DOH_SERVER_LISTEN: ":${DOH_SERVER_LISTEN}"
DOH_SERVER_TIMEOUT: "10"
DOH_SERVER_TRIES: "3"
DOH_SERVER_VERBOSE: "false"
#volumes:
# - ./doh-server.conf:/server/doh-server.conf
# - ./app-config:/app-config
depends_on:
- unbound
labels:
- "traefik.enable=true"
- "traefik.http.routers.doh-server.rule=Host(`${SUBDOMAIN}.${DOMAIN}`) && Path(`${DOH_HTTP_PREFIX}`)"
- "traefik.http.services.doh-server.loadbalancer.server.port=${DOH_SERVER_LISTEN}"
- "traefik.http.middlewares.mw-doh-compression.compress=true"
- "traefik.http.routers.doh-server.tls=true"
- "traefik.http.middlewares.mw-doh-tls.headers.sslredirect=true"
- "traefik.http.middlewares.mw-doh-tls.headers.sslforcehost=true"
- "traefik.http.routers.doh-server.tls.certresolver=letsencrypt"
- "traefik.http.routers.doh-server.tls.domains[0].main=${DOMAIN}"
- "traefik.http.routers.doh-server.tls.domains[0].sans=${SUBDOMAIN}.${DOMAIN}"
# Protection from requests flood
- "traefik.http.middlewares.mw-doh-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.mw-doh-ratelimit.ratelimit.burst=50"
- "traefik.http.middlewares.mw-doh-ratelimit.ratelimit.period=10s"
unbound:
image: satishweb/unbound:latest
hostname: unbound
networks:
- default
ports:
# Disable these ports if DOH server is the only client
- 53:53/tcp
- 53:53/udp
volumes:
- ./unbound.sample.conf:/templates/unbound.sample.conf
- ./data/unbound/custom:/etc/unbound/custom
# Keep your custom.hosts file inside custom folder
#environment:
# DEBUG: "1"
````
> Complete Guide available at: https://github.com/satishweb/docker-doh
> IPV6 Support for Docker Compose based configuration TBA
## 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
default. However signature validation is not built-in. It is highly recommended default. However signature validation is not built-in. It is highly recommended
that you install `unbound` or `bind` and pass results for them to validate DNS that you install `unbound` or `bind` and pass results for them to validate DNS
records. records. An instance of [Pi Hole](https://pi-hole.net) could also be used to validate DNS signatures as well as provide other capabilities.
## EDNS0-Client-Subnet (GeoDNS) ## EDNS0-Client-Subnet (GeoDNS)
DNS-over-HTTPS supports EDNS0-Client-Subnet protocol, which submits part of the DNS-over-HTTPS supports EDNS0-Client-Subnet protocol, which submits part of the
client's IP address (/24 for IPv4, /48 for IPv6 by default) to the upstream client's IP address (/24 for IPv4, /56 for IPv6 by default) to the upstream
server. This is useful for GeoDNS and CDNs to work, and is exactly the same server. This is useful for GeoDNS and CDNs to work, and is exactly the same
configuration as most public DNS servers. configuration as most public DNS servers.
@@ -90,8 +293,10 @@ EDNS0-Client-Subnet is affecting your privacy, you can set `no_ecs = true` in
`/etc/dns-over-https/doh-client.conf`, with the cost of slower video streaming `/etc/dns-over-https/doh-client.conf`, with the cost of slower video streaming
or software downloading speed. or software downloading speed.
If your server is backed by `unbound` or `bind`, you probably want to enable To ultilize ECS, `X-Forwarded-For` or `X-Real-IP` should be enabled on your
the EDNS0-Client-Subnet feature in their configuration files as well. HTTP service muxer. If your server is backed by `unbound` or `bind`, you
probably want to configure it to enable the EDNS0-Client-Subnet feature as
well.
## Protocol compatibility ## Protocol compatibility
@@ -102,11 +307,9 @@ except for absolute expire time is preferred to relative TTL value. Refer to
[json-dns/response.go](json-dns/response.go) for a complete description of the [json-dns/response.go](json-dns/response.go) for a complete description of the
API. API.
### IETF DNS-over-HTTPS Protocol (Draft) ### IETF DNS-over-HTTPS Protocol
DNS-over-HTTPS uses a protocol compatible to [draft-ietf-doh-dns-over-https](https://github.com/dohwg/draft-ietf-doh-dns-over-https). DNS-over-HTTPS uses a protocol compatible to [IETF DNS-over-HTTPS (RFC 8484)](https://www.rfc-editor.org/rfc/rfc8484.txt).
This protocol is in draft stage. Any incompatibility may be introduced before
it is finished.
### Supported features ### Supported features
@@ -114,7 +317,17 @@ Currently supported features are:
- [X] IPv4 / IPv6 - [X] IPv4 / IPv6
- [X] EDNS0 large UDP packet (4 KiB by default) - [X] EDNS0 large UDP packet (4 KiB by default)
- [X] EDNS0-Client-Subnet (/24 for IPv4, /48 for IPv6 by default) - [X] EDNS0-Client-Subnet (/24 for IPv4, /56 for IPv6 by default)
## Known issues
* it does not work well with [dnscrypt-proxy](https://github.com/DNSCrypt/dnscrypt-proxy), you might want to use either (or fix the compatibility bugs by submitting PRs)
## The name of the project
This project is named "DNS-over-HTTPS" because it was written before the IETF DoH project. Although this project is compatible with IETF DoH, the project is not affiliated with IETF.
To avoid confusion, you may also call this project "m13253/DNS-over-HTTPS" or anything you like.
## License ## License

51
contrib/linux-install.md Normal file
View File

@@ -0,0 +1,51 @@
# Ubuntu Install
> Tested on a clean install of `Ubuntu 16.04 LTS`
## Intalling go
Install `Go >= 1.9`
```bash
sudo apt update
sudo apt install golang-1.10 -y
```
Add the newly install `go` to the path
```bash
export PATH=$PATH:/usr/lib/go-1.10/bin
```
Test to make sure that you can execute `go`
```bash
go version
```
which should output something like
```bash
go version go1.10.1 linux/amd64
```
## Installing dns-over-https
Clone this repo
```bash
git clone https://github.com/m13253/dns-over-https.git
```
Change directory to the cloned repo
```bash
cd dns-over-https
```
make and install
```bash
make
sudo make install
```

13
contrib/linux-install.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
# See the linux-install.md (README) first.
set -e
sudo apt update
sudo apt install golang-1.10 git -y
export PATH=$PATH:/usr/lib/go-1.10/bin
cd /tmp
git clone https://github.com/m13253/dns-over-https.git
cd dns-over-https
make
sudo make install

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)

19
darwin-wrapper/Makefile Normal file
View File

@@ -0,0 +1,19 @@
.PHONY: all clean install uninstall
SWIFTC = swiftc
PREFIX = /usr/local
all: doh-logger
doh-logger: doh-logger.swift
$(SWIFTC) -o $@ -O $<
clean:
rm -f doh-logger
install: doh-logger
mkdir -p $(DESTDIR)$(PREFIX)/bin
install -m0755 doh-logger $(DESTDIR)$(PREFIX)/bin
uninstall:
rm -f $(DESTDIR)$(PREFIX)/bin/doh-logger

View File

@@ -0,0 +1,94 @@
#!/usr/bin/swift
/*
DNS-over-HTTPS
Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
import Foundation
import os.log
if CommandLine.arguments.count < 3 {
let programName = CommandLine.arguments[0]
print("Usage: \(programName) LOG_NAME PROGRAM [ARGUMENTS]\n")
exit(1)
}
let logSubsystem = CommandLine.arguments[1]
let logger = OSLog(subsystem: logSubsystem, category: "default")
let pipe = Pipe()
var buffer = Data()
NotificationCenter.default.addObserver(forName: FileHandle.readCompletionNotification, object: pipe.fileHandleForReading, queue: nil) { notification in
let data = notification.userInfo?["NSFileHandleNotificationDataItem"] as? Data ?? Data()
buffer.append(data)
var lastIndex = 0
for (i, byte) in buffer.enumerated() {
if byte == 0x0a {
let line = String(data: buffer.subdata(in: lastIndex..<i), encoding: .utf8) ?? ""
print(line)
os_log("%{public}@", log: logger, line)
lastIndex = i + 1
}
}
buffer = buffer.subdata(in: lastIndex..<buffer.count)
if data.count == 0 && buffer.count != 0 {
let line = String(data: buffer, encoding: .utf8) ?? ""
print(line, terminator: "")
os_log("%{public}@", log: logger, line)
}
pipe.fileHandleForReading.readInBackgroundAndNotify()
}
pipe.fileHandleForReading.readInBackgroundAndNotify()
let process = Process()
process.arguments = Array(CommandLine.arguments[3...])
process.executableURL = URL(fileURLWithPath: CommandLine.arguments[2])
process.standardError = pipe.fileHandleForWriting
process.standardInput = FileHandle.standardInput
process.standardOutput = pipe.fileHandleForWriting
NotificationCenter.default.addObserver(forName: Process.didTerminateNotification, object: process, queue: nil) { notification in
if buffer.count != 0 {
let line = String(data: buffer, encoding: .utf8) ?? ""
print(line, terminator: "")
os_log("%{public}@", log: logger, line)
}
exit(process.terminationStatus)
}
let SIGINTSource = DispatchSource.makeSignalSource(signal: SIGINT)
let SIGTERMSource = DispatchSource.makeSignalSource(signal: SIGTERM)
SIGINTSource.setEventHandler(handler: process.interrupt)
SIGTERMSource.setEventHandler(handler: process.terminate)
signal(SIGINT, SIG_IGN)
signal(SIGTERM, SIG_IGN)
SIGINTSource.resume()
SIGTERMSource.resume()
do {
try process.run()
} catch {
let errorMessage = error.localizedDescription
print(errorMessage)
os_log("%{public}@", log: logger, type: .fault, errorMessage)
exit(1)
}
RunLoop.current.run()

View File

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

101
doh-client/config/config.go Normal file
View File

@@ -0,0 +1,101 @@
/*
DNS-over-HTTPS
Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package config
import (
"fmt"
"github.com/BurntSushi/toml"
)
const (
Random = "random"
NginxWRR = "weighted_round_robin"
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"`
Passthrough []string `toml:"passthrough"`
Timeout uint `toml:"timeout"`
NoCookies bool `toml:"no_cookies"`
NoECS bool `toml:"no_ecs"`
NoIPv6 bool `toml:"no_ipv6"`
NoUserAgent bool `toml:"no_user_agent"`
Verbose bool `toml:"verbose"`
DebugHTTPHeaders []string `toml:"debug_http_headers"`
TLSInsecureSkipVerify bool `toml:"insecure_tls_skip_verify"`
}
type Config struct {
Listen []string `toml:"listen"`
Upstream upstream `toml:"upstream"`
Other others `toml:"others"`
}
func LoadConfig(path string) (*Config, error) {
conf := &Config{}
metaData, err := toml.DecodeFile(path, conf)
if err != nil {
return nil, err
}
for _, key := range metaData.Undecoded() {
return nil, &configError{fmt.Sprintf("unknown option %q", key.String())}
}
if len(conf.Listen) == 0 {
conf.Listen = []string{"127.0.0.1:53", "[::1]:53"}
}
if len(conf.Upstream.UpstreamGoogle) == 0 && len(conf.Upstream.UpstreamIETF) == 0 {
conf.Upstream.UpstreamGoogle = []upstreamDetail{{URL: "https://dns.google.com/resolve", Weight: 50}}
}
if conf.Other.Timeout == 0 {
conf.Other.Timeout = 10
}
if conf.Upstream.UpstreamSelector == "" {
conf.Upstream.UpstreamSelector = Random
}
return conf, nil
}
type configError struct {
err string
}
func (e *configError) Error() string {
return e.err
}

View File

@@ -1,31 +1,62 @@
# DNS listen port # DNS listen port
listen = "127.0.0.1:53" listen = [
"127.0.0.1:53",
"127.0.0.1:5380",
"[::1]:53",
"[::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",
] # weight should in (0, 100], if upstream_selector is random, weight will be ignored
upstream_ietf = [
# Google's experimental resolver, good ECS, good DNSSEC ## Google's resolver, good ECS, good DNSSEC
#"https://dns.google.com/experimental", #[[upstream.upstream_ietf]]
# url = "https://dns.google/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", [[upstream.upstream_ietf]]
#"https://1.0.0.1/dns-query", url = "https://cloudflare-dns.com/dns-query"
weight = 50
] ## CloudFlare's resolver, bad ECS, good DNSSEC
## ECS is disabled for privacy by design: https://developers.cloudflare.com/1.1.1.1/nitty-gritty-details/#edns-client-subnet
## Note that some ISPs have problems connecting to 1.1.1.1, try 1.0.0.1 if problems happen.
#[[upstream.upstream_ietf]]
# url = "https://1.1.1.1/dns-query"
# weight = 50
## DNS.SB's resolver, good ECS, good DNSSEC
## The provider claims no logging: https://dns.sb/doh/
#[[upstream.upstream_ietf]]
# 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.
@@ -33,7 +64,7 @@ upstream_ietf = [
# bootstrap server, please make this list empty. # bootstrap server, please make this list empty.
bootstrap = [ bootstrap = [
# Google's resolver, bad ECS, good DNSSEC # Google's resolver, good ECS, good DNSSEC
"8.8.8.8:53", "8.8.8.8:53",
"8.8.4.4:53", "8.8.4.4:53",
@@ -43,8 +74,26 @@ bootstrap = [
] ]
# Timeout for upstream request # The domain names here are directly passed to bootstrap servers listed above,
timeout = 10 # allowing captive portal detection and systems without RTC to work.
# Only effective if at least one bootstrap server is configured.
passthrough = [
"captive.apple.com",
"connectivitycheck.gstatic.com",
"detectportal.firefox.com",
"msftconnecttest.com",
"nmcheck.gnome.org",
"pool.ntp.org",
"time.apple.com",
"time.asia.apple.com",
"time.euro.apple.com",
"time.nist.gov",
"time.windows.com",
]
# Timeout for upstream request in seconds
timeout = 30
# Disable HTTP Cookies # Disable HTTP Cookies
# #
@@ -52,15 +101,40 @@ timeout = 10
# anti-DDoS services to identify clients. # anti-DDoS services to identify clients.
# Note that DNS Cookies (an DNS protocol extension to DNS) also has the ability # Note that DNS Cookies (an DNS protocol extension to DNS) also has the ability
# to track uesrs and is not controlled by doh-client. # to track uesrs and is not controlled by doh-client.
no_cookies = false no_cookies = true
# Disable EDNS0-Client-Subnet (ECS) # Disable EDNS0-Client-Subnet (ECS)
# #
# DNS-over-HTTPS supports EDNS0-Client-Subnet protocol, which submits part of # DNS-over-HTTPS supports EDNS0-Client-Subnet protocol, which submits part of
# the client's IP address (/24 for IPv4, /48 for IPv6 by default) to the # the client's IP address (/24 for IPv4, /56 for IPv6 by default) to the
# upstream server. This is useful for GeoDNS and CDNs to work, and is exactly # upstream server. This is useful for GeoDNS and CDNs to work, and is exactly
# the same configuration as most public DNS servers. # the same configuration as most public DNS servers.
no_ecs = false no_ecs = false
# Disable IPv6 when querying upstream
#
# Only enable this if you really have trouble connecting.
# Doh-client uses both IPv4 and IPv6 by default and should not have problems
# with an IPv4-only environment.
# Note that DNS listening and bootstrapping is not controlled by this option.
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
# insecure_tls_skip_verification will disable necessary TLS security verification.
# This option is designed for testing or development purposes,
# turning on this option on public Internet may cause your connection
# vulnerable to MITM attack.
insecure_tls_skip_verify = false

View File

@@ -24,49 +24,41 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"math/rand"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/m13253/dns-over-https/json-dns" "github.com/m13253/dns-over-https/v2/doh-client/selector"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func (c *Client) generateRequestGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP bool) *DNSRequest { func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool, upstream *selector.Upstream) *DNSRequest {
reply := jsonDNS.PrepareReply(r) question := &r.Question[0]
questionName := question.Name
if len(r.Question) != 1 { questionClass := question.Qclass
log.Println("Number of questions is not 1") if questionClass != dns.ClassINET {
reply.Rcode = dns.RcodeFormatError reply := jsondns.PrepareReply(r)
reply.Rcode = dns.RcodeRefused
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
err: &dns.Error{}, err: &dns.Error{},
} }
} }
question := &r.Question[0]
// knot-resolver scrambles capitalization, I think it is unfriendly to cache
questionName := strings.ToLower(question.Name)
questionType := "" questionType := ""
if qtype, ok := dns.TypeToString[question.Qtype]; ok { if qtype, ok := dns.TypeToString[question.Qtype]; ok {
questionType = qtype questionType = qtype
} else { } else {
questionType = strconv.Itoa(int(question.Qtype)) questionType = strconv.FormatUint(uint64(question.Qtype), 10)
} }
if c.conf.Verbose { requestURL := fmt.Sprintf("%s?ct=application/dns-json&name=%s&type=%s", upstream.URL, url.QueryEscape(questionName), url.QueryEscape(questionType))
fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType)
}
numServers := len(c.conf.UpstreamGoogle)
upstream := c.conf.UpstreamGoogle[rand.Intn(numServers)]
requestURL := fmt.Sprintf("%s?ct=application/dns-json&name=%s&type=%s", upstream, url.QueryEscape(questionName), url.QueryEscape(questionType))
if r.CheckingDisabled { if r.CheckingDisabled {
requestURL += "&cd=1" requestURL += "&cd=1"
@@ -75,6 +67,9 @@ func (c *Client) generateRequestGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP b
udpSize := uint16(512) udpSize := uint16(512)
if opt := r.IsEdns0(); opt != nil { if opt := r.IsEdns0(); opt != nil {
udpSize = opt.UDPSize() udpSize = opt.UDPSize()
if opt.Do() {
requestURL += "&do=1"
}
} }
ednsClientAddress, ednsClientNetmask := c.findClientIP(w, r) ednsClientAddress, ednsClientNetmask := c.findClientIP(w, r)
@@ -82,28 +77,43 @@ func (c *Client) generateRequestGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP b
requestURL += fmt.Sprintf("&edns_client_subnet=%s/%d", ednsClientAddress.String(), ednsClientNetmask) requestURL += fmt.Sprintf("&edns_client_subnet=%s/%d", ednsClientAddress.String(), ednsClientNetmask)
} }
req, err := http.NewRequest("GET", requestURL, nil) req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsondns.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
} }
req.Header.Set("Accept", "application/json, message/dns, application/dns-udpwireformat")
req.Header.Set("User-Agent", "DNS-over-HTTPS/1.1 (+https://github.com/m13253/dns-over-https)") req.Header.Set("Accept", "application/json, application/dns-message, application/dns-udpwireformat")
if !c.conf.Other.NoUserAgent {
req.Header.Set("User-Agent", USER_AGENT)
} else {
req.Header.Set("User-Agent", "")
}
req = req.WithContext(ctx)
c.httpClientMux.RLock() c.httpClientMux.RLock()
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
c.httpClientMux.RUnlock() c.httpClientMux.RUnlock()
// if http Client.Do returns non-nil error, it always *url.Error
/*if err == context.DeadlineExceeded {
// Do not respond, silently fail to prevent caching of SERVFAIL
log.Println(err)
return &DNSRequest{
err: err,
}
}*/
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsondns.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
err1 := c.newHTTPClient()
if err1 != nil {
log.Fatalln(err1)
}
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
@@ -111,16 +121,17 @@ func (c *Client) generateRequestGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP b
return &DNSRequest{ return &DNSRequest{
response: resp, response: resp,
reply: reply, reply: jsondns.PrepareReply(r),
udpSize: udpSize, udpSize: udpSize,
ednsClientAddress: ednsClientAddress, ednsClientAddress: ednsClientAddress,
ednsClientNetmask: ednsClientNetmask, ednsClientNetmask: ednsClientNetmask,
currentUpstream: upstream.URL,
} }
} }
func (c *Client) parseResponseGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP bool, req *DNSRequest) { func (c *Client) parseResponseGoogle(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool, req *DNSRequest) {
if req.response.StatusCode != 200 { if req.response.StatusCode != http.StatusOK {
log.Printf("HTTP error: %s\n", 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")
if contentType != "application/json" && !strings.HasPrefix(contentType, "application/json;") { if contentType != "application/json" && !strings.HasPrefix(contentType, "application/json;") {
@@ -137,7 +148,7 @@ func (c *Client) parseResponseGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP boo
return return
} }
var respJSON jsonDNS.Response var respJSON jsondns.Response
err = json.Unmarshal(body, &respJSON) err = json.Unmarshal(body, &respJSON)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@@ -149,8 +160,9 @@ func (c *Client) parseResponseGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP boo
if respJSON.Status != dns.RcodeSuccess && respJSON.Comment != "" { if respJSON.Status != dns.RcodeSuccess && respJSON.Comment != "" {
log.Printf("DNS error: %s\n", respJSON.Comment) log.Printf("DNS error: %s\n", respJSON.Comment)
} }
fixEmptyNames(&respJSON)
fullReply := jsonDNS.Unmarshal(req.reply, &respJSON, req.udpSize, req.ednsClientNetmask) fullReply := jsondns.Unmarshal(req.reply, &respJSON, req.udpSize, req.ednsClientNetmask)
buf, err := fullReply.Pack() buf, err := fullReply.Pack()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@@ -169,3 +181,19 @@ func (c *Client) parseResponseGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP boo
} }
w.Write(buf) w.Write(buf)
} }
// Fix DNS response empty []RR.Name
// Additional section won't be rectified
// see: https://stackoverflow.com/questions/52136176/what-is-additional-section-in-dns-and-how-it-works
func fixEmptyNames(respJSON *jsondns.Response) {
for i := range respJSON.Answer {
if respJSON.Answer[i].Name == "" {
respJSON.Answer[i].Name = "."
}
}
for i := range respJSON.Authority {
if respJSON.Authority[i].Name == "" {
respJSON.Authority[i].Name = "."
}
}
}

View File

@@ -25,57 +25,31 @@ package main
import ( import (
"bytes" "bytes"
"context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"math/rand"
"net" "net"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
"github.com/m13253/dns-over-https/json-dns" "github.com/m13253/dns-over-https/v2/doh-client/selector"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool) *DNSRequest { func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool, upstream *selector.Upstream) *DNSRequest {
reply := jsonDNS.PrepareReply(r)
if len(r.Question) != 1 {
log.Println("Number of questions is not 1")
reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply)
return &DNSRequest{
err: &dns.Error{},
}
}
question := &r.Question[0]
// knot-resolver scrambles capitalization, I think it is unfriendly to cache
questionName := strings.ToLower(question.Name)
questionType := ""
if qtype, ok := dns.TypeToString[question.Qtype]; ok {
questionType = qtype
} else {
questionType = strconv.Itoa(int(question.Qtype))
}
if c.conf.Verbose {
fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType)
}
question.Name = questionName
opt := r.IsEdns0() opt := r.IsEdns0()
udpSize := uint16(512) udpSize := uint16(512)
if opt == nil { if opt == nil {
opt = new(dns.OPT) opt = new(dns.OPT)
opt.Hdr.Name = "." opt.Hdr.Name = "."
opt.Hdr.Rrtype = dns.TypeOPT opt.Hdr.Rrtype = dns.TypeOPT
opt.SetUDPSize(4096) opt.SetUDPSize(dns.DefaultMsgSize)
opt.SetDo(false) opt.SetDo(false)
r.Extra = append(r.Extra, opt) r.Extra = append([]dns.RR{opt}, r.Extra...)
} else { } else {
udpSize = opt.UDPSize() udpSize = opt.UDPSize()
} }
@@ -97,7 +71,7 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
edns0Subnet = new(dns.EDNS0_SUBNET) edns0Subnet = new(dns.EDNS0_SUBNET)
edns0Subnet.Code = dns.EDNS0SUBNET edns0Subnet.Code = dns.EDNS0SUBNET
@@ -116,6 +90,7 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
requestBinary, err := r.Pack() requestBinary, err := r.Pack()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsondns.PrepareReply(r)
reply.Rcode = dns.RcodeFormatError reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
@@ -125,16 +100,14 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
r.Id = requestID r.Id = requestID
requestBase64 := base64.RawURLEncoding.EncodeToString(requestBinary) requestBase64 := base64.RawURLEncoding.EncodeToString(requestBinary)
numServers := len(c.conf.UpstreamIETF) requestURL := fmt.Sprintf("%s?ct=application/dns-message&dns=%s", upstream.URL, requestBase64)
upstream := c.conf.UpstreamIETF[rand.Intn(numServers)]
requestURL := fmt.Sprintf("%s?ct=application/dns-udpwireformat&dns=%s", upstream, requestBase64)
//requestURL := fmt.Sprintf("%s?ct=message/dns&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.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
@@ -142,30 +115,43 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
} }
} }
} 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.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
} }
req.Header.Set("Content-Type", "message/dns") req.Header.Set("Content-Type", "application/dns-message")
} }
req.Header.Set("Accept", "message/dns, application/dns-udpwireformat, application/json") req.Header.Set("Accept", "application/dns-message, application/dns-udpwireformat, application/json")
req.Header.Set("User-Agent", "DNS-over-HTTPS/1.1 (+https://github.com/m13253/dns-over-https)") if !c.conf.Other.NoUserAgent {
req.Header.Set("User-Agent", USER_AGENT)
} else {
req.Header.Set("User-Agent", "")
}
req = req.WithContext(ctx)
c.httpClientMux.RLock() c.httpClientMux.RLock()
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
c.httpClientMux.RUnlock() c.httpClientMux.RUnlock()
// if http Client.Do returns non-nil error, it always *url.Error
/*if err == context.DeadlineExceeded {
// Do not respond, silently fail to prevent caching of SERVFAIL
log.Println(err)
return &DNSRequest{
err: err,
}
}*/
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsondns.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
err1 := c.newHTTPClient()
if err1 != nil {
log.Fatalln(err1)
}
return &DNSRequest{ return &DNSRequest{
err: err, err: err,
} }
@@ -173,19 +159,20 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
return &DNSRequest{ return &DNSRequest{
response: resp, response: resp,
reply: reply, reply: jsondns.PrepareReply(r),
udpSize: udpSize, udpSize: udpSize,
ednsClientAddress: ednsClientAddress, ednsClientAddress: ednsClientAddress,
ednsClientNetmask: ednsClientNetmask, ednsClientNetmask: ednsClientNetmask,
currentUpstream: upstream.URL,
} }
} }
func (c *Client) parseResponseIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool, req *DNSRequest) { func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool, req *DNSRequest) {
if req.response.StatusCode != 200 { if req.response.StatusCode != http.StatusOK {
log.Printf("HTTP error: %s\n", 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")
if contentType != "message/dns" && !strings.HasPrefix(contentType, "message/dns;") { if contentType != "application/dns-message" && !strings.HasPrefix(contentType, "application/dns-message;") {
w.WriteMsg(req.reply) w.WriteMsg(req.reply)
return return
} }
@@ -193,7 +180,7 @@ func (c *Client) parseResponseIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool,
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
@@ -204,7 +191,7 @@ func (c *Client) parseResponseIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool,
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")
@@ -213,7 +200,7 @@ func (c *Client) parseResponseIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool,
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)
@@ -224,7 +211,7 @@ func (c *Client) parseResponseIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool,
fullReply := new(dns.Msg) fullReply := new(dns.Msg)
err = fullReply.Unpack(body) err = fullReply.Unpack(body)
if err != nil { 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
@@ -246,7 +233,7 @@ func (c *Client) parseResponseIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool,
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
@@ -255,12 +242,15 @@ func (c *Client) parseResponseIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool,
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/v2/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

@@ -23,50 +23,7 @@
package main package main
import ( const (
"fmt" VERSION = "2.3.2"
USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)"
"github.com/BurntSushi/toml"
) )
type config struct {
Listen string `toml:"listen"`
UpstreamGoogle []string `toml:"upstream_google"`
UpstreamIETF []string `toml:"upstream_ietf"`
Bootstrap []string `toml:"bootstrap"`
Timeout uint `toml:"timeout"`
NoCookies bool `toml:"no_cookies"`
NoECS bool `toml:"no_ecs"`
Verbose bool `toml:"verbose"`
}
func loadConfig(path string) (*config, error) {
conf := &config{}
metaData, err := toml.DecodeFile(path, conf)
if err != nil {
return nil, err
}
for _, key := range metaData.Undecoded() {
return nil, &configError{fmt.Sprintf("unknown option %q", key.String())}
}
if conf.Listen == "" {
conf.Listen = "127.0.0.1:53"
}
if len(conf.UpstreamGoogle) == 0 && len(conf.UpstreamIETF) == 0 {
conf.UpstreamGoogle = []string{"https://dns.google.com/resolve"}
}
if conf.Timeout == 0 {
conf.Timeout = 10
}
return conf, nil
}
type configError struct {
err string
}
func (e *configError) Error() string {
return e.err
}

View File

@@ -25,20 +25,27 @@ 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"`
LogGuessedIP bool `toml:"log_guessed_client_ip"`
ECSAllowNonGlobalIP bool `toml:"ecs_allow_non_global_ip"`
ECSUsePreciseIP bool `toml:"ecs_use_precise_ip"`
TLSClientAuth bool `toml:"tls_client_auth"`
TLSClientAuthCA string `toml:"tls_client_auth_ca"`
} }
func loadConfig(path string) (*config, error) { func loadConfig(path string) (*config, error) {
@@ -51,14 +58,15 @@ func loadConfig(path string) (*config, error) {
return nil, &configError{fmt.Sprintf("unknown option %q", key.String())} return nil, &configError{fmt.Sprintf("unknown option %q", key.String())}
} }
if conf.Listen == "" { if len(conf.Listen) == 0 {
conf.Listen = "127.0.0.1:8053" conf.Listen = []string{"127.0.0.1:8053", "[::1]:8053"}
} }
if conf.Path == "" { if conf.Path == "" {
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
@@ -71,9 +79,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

@@ -1,10 +1,25 @@
# HTTP listen port # HTTP listen port
listen = "127.0.0.1:8053" listen = [
"127.0.0.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.
# You are recommended to leave empty and to use a server load balancer (e.g.
# Caddy, Nginx) and set up TLS there, because this program does not do OCSP
# Stapling, which is necessary for client bootstrapping in a network
# environment with completely no traditional DNS service.
cert = "" cert = ""
# TLS key file # TLS private key file
key = "" key = ""
# HTTP path for resolve application # HTTP path for resolve application
@@ -12,9 +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 = [
"8.8.8.8:53", "udp:1.1.1.1:53",
"8.8.4.4:53", "udp:1.0.0.1:53",
"udp:8.8.8.8:53",
"udp:8.8.4.4:53",
] ]
# Upstream timeout # Upstream timeout
@@ -23,8 +45,34 @@ 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
# By default, non global IP addresses are never forwarded to upstream servers.
# This is to prevent two things from happening:
# 1. the upstream server knowing your private LAN addresses;
# 2. the upstream server unable to provide geographically near results,
# or even fail to provide any result.
# However, if you are deploying a split tunnel corporation network
# environment, or for any other reason you want to inhibit this
# behavior and allow local (eg RFC1918) address to be forwarded,
# change the following option to "true".
ecs_allow_non_global_ip = false
# If ECS is added to the request, let the full IP address or
# cap it to 24 or 128 mask. This option is to be used only on private
# networks where knwoledge of the terminal endpoint may be required for
# security purposes (eg. DNS Firewalling). Not a good option on the
# internet where IP address may be used to identify the user and
# not only the approximate location.
ecs_use_precise_ip = false
# If DOH is used for a controlled network, it is possible to enable
# the client TLS certificate validation with a specific certificate
# authority used to sign any client one. Disabled by default.
# tls_client_auth = true
# tls_client_auth_ca = "root-ca-public.crt"

View File

@@ -24,6 +24,7 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
@@ -33,12 +34,12 @@ import (
"strings" "strings"
"time" "time"
"github.com/m13253/dns-over-https/json-dns" jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/idna" "golang.org/x/net/idna"
) )
func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNSRequest { func (s *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest {
name := r.FormValue("name") name := r.FormValue("name")
if name == "" { if name == "" {
return &DNSRequest{ return &DNSRequest{
@@ -46,7 +47,6 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
errtext: "Invalid argument value: \"name\"", errtext: "Invalid argument value: \"name\"",
} }
} }
name = strings.ToLower(name)
if punycode, err := idna.ToASCII(name); err == nil { if punycode, err := idna.ToASCII(name); err == nil {
name = punycode name = punycode
} else { } else {
@@ -72,9 +72,9 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
cdStr := r.FormValue("cd") cdStr := r.FormValue("cd")
cd := false cd := false
if cdStr == "1" || cdStr == "true" { if cdStr == "1" || strings.EqualFold(cdStr, "true") {
cd = true cd = true
} else if cdStr == "0" || cdStr == "false" || cdStr == "" { } else if cdStr == "0" || strings.EqualFold(cdStr, "false") || cdStr == "" {
} else { } else {
return &DNSRequest{ return &DNSRequest{
errcode: 400, errcode: 400,
@@ -105,7 +105,7 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
} else { } else {
ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash]) ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash])
@@ -140,7 +140,7 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
} }
@@ -150,7 +150,7 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
opt := new(dns.OPT) opt := new(dns.OPT)
opt.Hdr.Name = "." opt.Hdr.Name = "."
opt.Hdr.Rrtype = dns.TypeOPT opt.Hdr.Rrtype = dns.TypeOPT
opt.SetUDPSize(4096) opt.SetUDPSize(dns.DefaultMsgSize)
opt.SetDo(true) opt.SetDo(true)
if ednsClientAddress != nil { if ednsClientAddress != nil {
edns0Subnet := new(dns.EDNS0_SUBNET) edns0Subnet := new(dns.EDNS0_SUBNET)
@@ -169,12 +169,12 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
} }
} }
func (s *Server) generateResponseGoogle(w http.ResponseWriter, r *http.Request, req *DNSRequest) { func (s *Server) generateResponseGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) {
respJSON := jsonDNS.Marshal(req.response) respJSON := jsondns.Marshal(req.response)
respStr, err := json.Marshal(respJSON) respStr, err := json.Marshal(respJSON)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
jsonDNS.FormatError(w, fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), 500) jsondns.FormatError(w, fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), 500)
return return
} }
@@ -182,11 +182,12 @@ func (s *Server) generateResponseGoogle(w http.ResponseWriter, r *http.Request,
now := time.Now().UTC().Format(http.TimeFormat) now := time.Now().UTC().Format(http.TimeFormat)
w.Header().Set("Date", now) w.Header().Set("Date", now)
w.Header().Set("Last-Modified", now) w.Header().Set("Last-Modified", now)
w.Header().Set("Vary", "Accept")
if respJSON.HaveTTL { if respJSON.HaveTTL {
if req.isTailored { if req.isTailored {
w.Header().Set("Cache-Control", "private, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) w.Header().Set("Cache-Control", "private, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10))
} else { } else {
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) w.Header().Set("Cache-Control", "public, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10))
} }
w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat)) w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat))
} }

View File

@@ -24,19 +24,23 @@
package main package main
import ( import (
"bytes"
"context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/m13253/dns-over-https/json-dns" jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRequest { func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest {
requestBase64 := r.FormValue("dns") requestBase64 := r.FormValue("dns")
requestBinary, err := base64.RawURLEncoding.DecodeString(requestBase64) requestBinary, err := base64.RawURLEncoding.DecodeString(requestBase64)
if err != nil { if err != nil {
@@ -45,7 +49,7 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
errtext: fmt.Sprintf("Invalid argument value: \"dns\" = %q", requestBase64), errtext: fmt.Sprintf("Invalid argument value: \"dns\" = %q", requestBase64),
} }
} }
if len(requestBinary) == 0 && (r.Header.Get("Content-Type") == "message/dns" || r.Header.Get("Content-Type") == "application/dns-udpwireformat") { if len(requestBinary) == 0 && (r.Header.Get("Content-Type") == "application/dns-message" || r.Header.Get("Content-Type") == "application/dns-udpwireformat") {
requestBinary, err = ioutil.ReadAll(r.Body) requestBinary, err = ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
return &DNSRequest{ return &DNSRequest{
@@ -60,6 +64,13 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
errtext: fmt.Sprintf("Invalid argument value: \"dns\""), errtext: fmt.Sprintf("Invalid argument value: \"dns\""),
} }
} }
if s.patchDNSCryptProxyReqID(w, r, requestBinary) {
return &DNSRequest{
errcode: 444,
}
}
msg := new(dns.Msg) msg := new(dns.Msg)
err = msg.Unpack(requestBinary) err = msg.Unpack(requestBinary)
if err != nil { if err != nil {
@@ -76,26 +87,35 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
if qclass, ok := dns.ClassToString[question.Qclass]; ok { if qclass, ok := dns.ClassToString[question.Qclass]; ok {
questionClass = qclass questionClass = qclass
} else { } else {
questionClass = strconv.Itoa(int(question.Qclass)) questionClass = strconv.FormatUint(uint64(question.Qclass), 10)
} }
questionType := "" questionType := ""
if qtype, ok := dns.TypeToString[question.Qtype]; ok { if qtype, ok := dns.TypeToString[question.Qtype]; ok {
questionType = qtype questionType = qtype
} else { } else {
questionType = strconv.Itoa(int(question.Qtype)) questionType = strconv.FormatUint(uint64(question.Qtype), 10)
} }
var clientip net.IP = nil
if s.conf.LogGuessedIP {
clientip = s.findClientIP(r)
}
if clientip != nil {
fmt.Printf("%s - - [%s] \"%s %s %s\"\n", clientip, time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType)
} else {
fmt.Printf("%s - - [%s] \"%s %s %s\"\n", r.RemoteAddr, time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType) fmt.Printf("%s - - [%s] \"%s %s %s\"\n", r.RemoteAddr, time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType)
} }
}
transactionID := msg.Id
msg.Id = dns.Id() msg.Id = dns.Id()
opt := msg.IsEdns0() opt := msg.IsEdns0()
if opt == nil { if opt == nil {
opt = new(dns.OPT) opt = new(dns.OPT)
opt.Hdr.Name = "." opt.Hdr.Name = "."
opt.Hdr.Rrtype = dns.TypeOPT opt.Hdr.Rrtype = dns.TypeOPT
opt.SetUDPSize(4096) opt.SetUDPSize(dns.DefaultMsgSize)
opt.SetDo(false) opt.SetDo(false)
msg.Extra = append(msg.Extra, opt) msg.Extra = append([]dns.RR{opt}, msg.Extra...)
} }
var edns0Subnet *dns.EDNS0_SUBNET var edns0Subnet *dns.EDNS0_SUBNET
for _, option := range opt.Option { for _, option := range opt.Option {
@@ -105,6 +125,7 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
} }
} }
isTailored := edns0Subnet == nil isTailored := edns0Subnet == nil
if edns0Subnet == nil { if edns0Subnet == nil {
ednsClientFamily := uint16(0) ednsClientFamily := uint16(0)
ednsClientAddress := s.findClientIP(r) ednsClientAddress := s.findClientIP(r)
@@ -113,10 +134,20 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
if ipv4 := ednsClientAddress.To4(); ipv4 != nil { if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1 ednsClientFamily = 1
ednsClientAddress = ipv4 ednsClientAddress = ipv4
if s.conf.ECSUsePreciseIP {
ednsClientNetmask = 32
} else {
ednsClientNetmask = 24 ednsClientNetmask = 24
ednsClientAddress = ednsClientAddress.Mask(net.CIDRMask(24, 32))
}
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
ednsClientNetmask = 48 if s.conf.ECSUsePreciseIP {
ednsClientNetmask = 128
} else {
ednsClientNetmask = 56
ednsClientAddress = ednsClientAddress.Mask(net.CIDRMask(56, 128))
}
} }
edns0Subnet = new(dns.EDNS0_SUBNET) edns0Subnet = new(dns.EDNS0_SUBNET)
edns0Subnet.Code = dns.EDNS0SUBNET edns0Subnet.Code = dns.EDNS0SUBNET
@@ -130,34 +161,74 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
return &DNSRequest{ return &DNSRequest{
request: msg, request: msg,
transactionID: transactionID,
isTailored: isTailored, isTailored: isTailored,
} }
} }
func (s *Server) generateResponseIETF(w http.ResponseWriter, r *http.Request, req *DNSRequest) { func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) {
respJSON := jsonDNS.Marshal(req.response) respJSON := jsondns.Marshal(req.response)
req.response.Id = 0 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
} }
w.Header().Set("Content-Type", "message/dns") w.Header().Set("Content-Type", "application/dns-message")
now := time.Now().UTC().Format(http.TimeFormat) now := time.Now().UTC().Format(http.TimeFormat)
w.Header().Set("Date", now) w.Header().Set("Date", now)
w.Header().Set("Last-Modified", now) w.Header().Set("Last-Modified", now)
w.Header().Set("Vary", "Accept")
_ = s.patchFirefoxContentType(w, r, req)
if respJSON.HaveTTL { if respJSON.HaveTTL {
if req.isTailored { if req.isTailored {
w.Header().Set("Cache-Control", "private, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) w.Header().Set("Cache-Control", "private, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10))
} else { } else {
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(int(respJSON.LeastTTL))) w.Header().Set("Cache-Control", "public, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10))
} }
w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat)) w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat))
} }
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
func (s *Server) patchDNSCryptProxyReqID(w http.ResponseWriter, r *http.Request, requestBinary []byte) bool {
if strings.Contains(r.UserAgent(), "dnscrypt-proxy") && bytes.Equal(requestBinary, []byte("\xca\xfe\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01\x00\x00\x02\x00\x01\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x00")) {
if s.conf.Verbose {
log.Println("DNSCrypt-Proxy detected. Patching response.")
}
w.Header().Set("Content-Type", "application/dns-message")
w.Header().Set("Vary", "Accept, User-Agent")
now := time.Now().UTC().Format(http.TimeFormat)
w.Header().Set("Date", now)
w.Write([]byte("\xca\xfe\x81\x05\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x10\x00\x01\x00\x00\x00\x00\x00\xa8\xa7\r\nWorkaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe\r\nRefer to https://github.com/jedisct1/dnscrypt-proxy/issues/526 for details."))
return true
}
return false
}
// Workaround a bug causing Firefox 61-62 to reject responses with Content-Type = application/dns-message
func (s *Server) patchFirefoxContentType(w http.ResponseWriter, r *http.Request, req *DNSRequest) bool {
if strings.Contains(r.UserAgent(), "Firefox") && strings.Contains(r.Header.Get("Accept"), "application/dns-udpwireformat") && !strings.Contains(r.Header.Get("Accept"), "application/dns-message") {
if s.conf.Verbose {
log.Println("Firefox 61-62 detected. Patching response.")
}
w.Header().Set("Content-Type", "application/dns-udpwireformat")
w.Header().Set("Vary", "Accept, User-Agent")
req.isTailored = true
return true
}
return false
} }

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

View File

@@ -24,7 +24,11 @@
package main package main
import ( import (
"context"
"crypto/tls"
"crypto/x509"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"math/rand" "math/rand"
"net" "net"
@@ -34,7 +38,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/v2/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@@ -42,32 +46,63 @@ 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
} }
type DNSRequest struct { type DNSRequest struct {
request *dns.Msg request *dns.Msg
response *dns.Msg response *dns.Msg
transactionID uint16
currentUpstream string
isTailored bool isTailored bool
errcode int errcode int
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",
Timeout: time.Duration(conf.Timeout) * time.Second, UDPSize: dns.DefaultMsgSize,
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 {
@@ -75,20 +110,106 @@ func (s *Server) Start() error {
if s.conf.Verbose { if s.conf.Verbose {
servemux = handlers.CombinedLoggingHandler(os.Stdout, servemux) servemux = handlers.CombinedLoggingHandler(os.Stdout, servemux)
} }
if s.conf.Cert != "" || s.conf.Key != "" {
return http.ListenAndServeTLS(s.conf.Listen, s.conf.Cert, s.conf.Key, servemux) var clientCAPool *x509.CertPool
if s.conf.TLSClientAuth {
if s.conf.TLSClientAuthCA != "" {
clientCA, err := ioutil.ReadFile(s.conf.TLSClientAuthCA)
if err != nil {
log.Fatalf("Reading certificate for client authentication has failed: %v", err)
} }
return http.ListenAndServe(s.conf.Listen, servemux) clientCAPool = x509.NewCertPool()
clientCAPool.AppendCertsFromPEM(clientCA)
log.Println("Certificate loaded for client TLS authentication")
} else {
log.Fatalln("TLS client authentication requires both tls_client_auth and tls_client_auth_ca, exiting.")
}
}
results := make(chan error, len(s.conf.Listen))
for _, addr := range s.conf.Listen {
go func(addr string) {
var err error
if s.conf.Cert != "" || s.conf.Key != "" {
if clientCAPool != nil {
srvtls := &http.Server{
Handler: servemux,
Addr: addr,
TLSConfig: &tls.Config{
ClientCAs: clientCAPool,
ClientAuth: tls.RequireAndVerifyClientCert,
GetCertificate: func(info *tls.ClientHelloInfo) (certificate *tls.Certificate, e error) {
c, err := tls.LoadX509KeyPair(s.conf.Cert, s.conf.Key)
if err != nil {
fmt.Printf("Error loading server certificate key pair: %v\n", err)
return nil, err
}
return &c, nil
},
},
}
err = srvtls.ListenAndServeTLS("", "")
} else {
err = http.ListenAndServeTLS(addr, s.conf.Cert, s.conf.Key, servemux)
}
} else {
err = http.ListenAndServe(addr, servemux)
}
if err != nil {
log.Println(err)
}
results <- err
}(addr)
}
// wait for all handlers
for i := 0; i < cap(results); i++ {
err := <-results
if err != nil {
return err
}
}
close(results)
return nil
} }
func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) { func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "DNS-over-HTTPS/1.1 (+https://github.com/m13253/dns-over-https)") ctx := r.Context()
w.Header().Set("X-Powered-By", "DNS-over-HTTPS/1.1 (+https://github.com/m13253/dns-over-https)")
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
if strings.ContainsRune(realIP, ':') {
r.RemoteAddr = "[" + realIP + "]:0"
} else {
r.RemoteAddr = realIP + ":0"
}
_, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
r.RemoteAddr = realIP
}
}
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS, POST")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Max-Age", "3600")
w.Header().Set("Server", USER_AGENT)
w.Header().Set("X-Powered-By", USER_AGENT)
if r.Method == "OPTIONS" {
w.Header().Set("Content-Length", "0")
return
}
if r.Form == nil { if r.Form == nil {
const maxMemory = 32 << 20 // 32 MB const maxMemory = 32 << 20 // 32 MB
r.ParseMultipartForm(maxMemory) r.ParseMultipartForm(maxMemory)
} }
for _, header := range s.conf.DebugHTTPHeaders {
if value := r.Header.Get(header); value != "" {
log.Printf("%s: %s\n", header, value)
}
}
contentType := r.Header.Get("Content-Type") contentType := r.Header.Get("Content-Type")
if ct := r.FormValue("ct"); ct != "" { if ct := r.FormValue("ct"); ct != "" {
contentType = ct contentType = ct
@@ -98,7 +219,7 @@ func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
if r.FormValue("name") != "" { if r.FormValue("name") != "" {
contentType = "application/dns-json" contentType = "application/dns-json"
} else if r.FormValue("dns") != "" { } else if r.FormValue("dns") != "" {
contentType = "message/dns" contentType = "application/dns-message"
} }
} }
var responseType string var responseType string
@@ -108,10 +229,10 @@ func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
responseType = "application/json" responseType = "application/json"
break break
} else if responseCandidate == "application/dns-udpwireformat" { } else if responseCandidate == "application/dns-udpwireformat" {
responseType = "message/dns" responseType = "application/dns-message"
break break
} else if responseCandidate == "message/dns" { } else if responseCandidate == "application/dns-message" {
responseType = "message/dns" responseType = "application/dns-message"
break break
} }
} }
@@ -119,52 +240,61 @@ func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
// Guess response Content-Type based on request Content-Type // Guess response Content-Type based on request Content-Type
if contentType == "application/dns-json" { if contentType == "application/dns-json" {
responseType = "application/json" responseType = "application/json"
} else if contentType == "message/dns" { } else if contentType == "application/dns-message" {
responseType = "message/dns" responseType = "application/dns-message"
} else if contentType == "application/dns-udpwireformat" { } else if contentType == "application/dns-udpwireformat" {
responseType = "message/dns" responseType = "application/dns-message"
} }
} }
var req *DNSRequest var req *DNSRequest
if contentType == "application/dns-json" { if contentType == "application/dns-json" {
req = s.parseRequestGoogle(w, r) req = s.parseRequestGoogle(ctx, w, r)
} else if contentType == "message/dns" { } else if contentType == "application/dns-message" {
req = s.parseRequestIETF(w, r) req = s.parseRequestIETF(ctx, w, r)
} else if contentType == "application/dns-udpwireformat" { } else if contentType == "application/dns-udpwireformat" {
req = s.parseRequestIETF(w, r) req = s.parseRequestIETF(ctx, w, r)
} else { } else {
jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 415) jsondns.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 415)
return
}
if req.errcode == 444 {
return return
} }
if req.errcode != 0 { if req.errcode != 0 {
jsonDNS.FormatError(w, req.errtext, req.errcode) jsondns.FormatError(w, req.errtext, req.errcode)
return return
} }
var err error req = s.patchRootRD(req)
req.response, err = s.doDNSQuery(req.request)
err := s.doDNSQuery(ctx, req)
if err != nil { if err != nil {
jsonDNS.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503) jsondns.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503)
return return
} }
if responseType == "application/json" { if responseType == "application/json" {
s.generateResponseGoogle(w, r, req) s.generateResponseGoogle(ctx, w, r, req)
} else if responseType == "message/dns" { } else if responseType == "application/dns-message" {
s.generateResponseIETF(w, r, req) s.generateResponseIETF(ctx, w, r, req)
} else { } else {
panic("Unknown response Content-Type") panic("Unknown response Content-Type")
} }
} }
func (s *Server) findClientIP(r *http.Request) net.IP { func (s *Server) findClientIP(r *http.Request) net.IP {
noEcs := r.URL.Query().Get("no_ecs")
if strings.ToLower(noEcs) == "true" {
return nil
}
XForwardedFor := r.Header.Get("X-Forwarded-For") XForwardedFor := r.Header.Get("X-Forwarded-For")
if XForwardedFor != "" { if XForwardedFor != "" {
for _, addr := range strings.Split(XForwardedFor, ",") { for _, addr := range strings.Split(XForwardedFor, ",") {
addr = strings.TrimSpace(addr) addr = strings.TrimSpace(addr)
ip := net.ParseIP(addr) ip := net.ParseIP(addr)
if jsonDNS.IsGlobalIP(ip) { if jsondns.IsGlobalIP(ip) {
return ip return ip
} }
} }
@@ -173,37 +303,80 @@ func (s *Server) findClientIP(r *http.Request) net.IP {
if XRealIP != "" { if XRealIP != "" {
addr := strings.TrimSpace(XRealIP) addr := strings.TrimSpace(XRealIP)
ip := net.ParseIP(addr) ip := net.ParseIP(addr)
if jsonDNS.IsGlobalIP(ip) { if s.conf.ECSAllowNonGlobalIP || jsondns.IsGlobalIP(ip) {
return ip return ip
} }
} }
remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr)
if err != nil { if err != nil {
return nil return nil
} }
if ip := remoteAddr.IP; jsonDNS.IsGlobalIP(ip) { ip := remoteAddr.IP
if s.conf.ECSAllowNonGlobalIP || jsondns.IsGlobalIP(ip) {
return ip return ip
} }
return nil return nil
} }
func (s *Server) doDNSQuery(msg *dns.Msg) (resp *dns.Msg, err error) { // Workaround a bug causing Unbound to refuse returning anything about the root
func (s *Server) patchRootRD(req *DNSRequest) *DNSRequest {
for _, question := range req.request.Question {
if question.Name == "." {
req.request.RecursionDesired = true
}
}
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) (err error) {
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++ {
server := s.conf.Upstream[rand.Intn(numServers)] req.currentUpstream = s.conf.Upstream[rand.Intn(numServers)]
if !s.conf.TCPOnly {
resp, _, err = s.udpClient.Exchange(msg, server) upstream, t := addressAndType(req.currentUpstream)
if err == dns.ErrTruncated {
log.Println(err) switch t {
resp, _, err = s.tcpClient.Exchange(msg, server) default:
} log.Printf("invalid DNS type %q in upstream %q", t, upstream)
return &configError{"invalid DNS type"}
// Use DNS-over-TLS (DoT) if configured to do so
case "tcp-tls":
req.response, _, err = s.tcpClientTLS.ExchangeContext(ctx, 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.ExchangeContext(ctx, req.request, upstream)
} else { } else {
resp, _, err = s.tcpClient.Exchange(msg, server) req.response, _, err = s.udpClient.ExchangeContext(ctx, req.request, upstream)
} if err == nil && req.response != nil && req.response.Truncated {
if err == nil {
return
}
log.Println(err) log.Println(err)
req.response, _, err = s.tcpClient.ExchangeContext(ctx, req.request, upstream)
} }
return
// 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.ExchangeContext(ctx, req.request, upstream)
}
}
}
if err == nil {
return nil
}
log.Printf("DNS error from upstream %s: %s\n", req.currentUpstream, err.Error())
}
return err
} }

29
doh-server/version.go Normal file
View File

@@ -0,0 +1,29 @@
/*
DNS-over-HTTPS
Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package main
const (
VERSION = "2.3.2"
USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)"
)

20
go.mod Normal file
View File

@@ -0,0 +1,20 @@
module github.com/m13253/dns-over-https/v2
go 1.19
require (
github.com/BurntSushi/toml v1.2.0
github.com/gorilla/handlers v1.5.1
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9
github.com/miekg/dns v1.1.50
golang.org/x/net v0.0.0-20220812174116-3211cb980234
)
require (
github.com/felixge/httpsnoop v1.0.1 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)

51
go.sum Normal file
View File

@@ -0,0 +1,51 @@
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 h1:w66aaP3c6SIQ0pi3QH1Tb4AMO3aWoEPxd1CNvLphbkA=
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9/go.mod h1:BaIJzjD2ZnHmx2acPF6XfGLPzNCMiBbMRqJr+8/8uRI=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -21,7 +21,7 @@
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
*/ */
package jsonDNS package jsondns
import ( import (
"encoding/json" "encoding/json"
@@ -38,11 +38,11 @@ type dnsError struct {
func FormatError(w http.ResponseWriter, comment string, errcode int) { func FormatError(w http.ResponseWriter, comment string, errcode int) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.Header().Set("Content-Type", "application/json; charset=UTF-8")
errJson := dnsError{ errJSON := dnsError{
Status: dns.RcodeServerFailure, Status: dns.RcodeServerFailure,
Comment: comment, Comment: comment,
} }
errStr, err := json.Marshal(errJson) errStr, err := json.Marshal(errJSON)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }

View File

@@ -21,109 +21,111 @@
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
*/ */
package jsonDNS package jsondns
import ( import (
"net" "net"
"github.com/infobloxopen/go-trees/iptree"
) )
// RFC6890 var defaultFilter *iptree.Tree
var localIPv4Nets = []net.IPNet{
// This host on this network func init() {
net.IPNet{ defaultFilter = iptree.NewTree()
net.IP{0, 0, 0, 0},
net.IPMask{255, 0, 0, 0}, // RFC6890
}, // This host on this network
// Private-Use Networks defaultFilter.InplaceInsertNet(&net.IPNet{
net.IPNet{ IP: net.IP{0, 0, 0, 0},
net.IP{10, 0, 0, 0}, Mask: net.IPMask{255, 0, 0, 0},
net.IPMask{255, 0, 0, 0}, }, struct{}{})
},
// Shared Address Space // Private-Use Networks
net.IPNet{ defaultFilter.InplaceInsertNet(&net.IPNet{
net.IP{100, 64, 0, 0}, IP: net.IP{10, 0, 0, 0},
net.IPMask{255, 192, 0, 0}, Mask: net.IPMask{255, 0, 0, 0},
}, }, struct{}{})
// Loopback
net.IPNet{ // Shared Address Space
net.IP{127, 0, 0, 0}, defaultFilter.InplaceInsertNet(&net.IPNet{
net.IPMask{255, 0, 0, 0}, IP: net.IP{100, 64, 0, 0},
}, Mask: net.IPMask{255, 192, 0, 0},
// Link Local }, struct{}{})
net.IPNet{
net.IP{169, 254, 0, 0}, // Loopback
net.IPMask{255, 255, 0, 0}, defaultFilter.InplaceInsertNet(&net.IPNet{
}, IP: net.IP{127, 0, 0, 0},
// Private-Use Networks Mask: net.IPMask{255, 0, 0, 0},
net.IPNet{ }, struct{}{})
net.IP{172, 16, 0, 0},
net.IPMask{255, 240, 0, 0}, // Link Local
}, defaultFilter.InplaceInsertNet(&net.IPNet{
// DS-Lite IP: net.IP{169, 254, 0, 0},
net.IPNet{ Mask: net.IPMask{255, 255, 0, 0},
net.IP{192, 0, 0, 0}, }, struct{}{})
net.IPMask{255, 255, 255, 248},
}, // Private-Use Networks
// 6to4 Relay Anycast defaultFilter.InplaceInsertNet(&net.IPNet{
net.IPNet{ IP: net.IP{172, 16, 0, 0},
net.IP{192, 88, 99, 0}, Mask: net.IPMask{255, 240, 0, 0},
net.IPMask{255, 255, 255, 0}, }, struct{}{})
},
// Private-Use Networks // DS-Lite
net.IPNet{ defaultFilter.InplaceInsertNet(&net.IPNet{
net.IP{192, 168, 0, 0}, IP: net.IP{192, 0, 0, 0},
net.IPMask{255, 255, 0, 0}, Mask: net.IPMask{255, 255, 255, 248},
}, }, struct{}{})
// Reserved for Future Use & Limited Broadcast
net.IPNet{ // 6to4 Relay Anycast
net.IP{240, 0, 0, 0}, defaultFilter.InplaceInsertNet(&net.IPNet{
net.IPMask{240, 0, 0, 0}, IP: net.IP{192, 88, 99, 0},
}, Mask: net.IPMask{255, 255, 255, 0},
} }, struct{}{})
// Private-Use Networks
defaultFilter.InplaceInsertNet(&net.IPNet{
IP: net.IP{192, 168, 0, 0},
Mask: net.IPMask{255, 255, 0, 0},
}, struct{}{})
// Reserved for Future Use & Limited Broadcast
defaultFilter.InplaceInsertNet(&net.IPNet{
IP: net.IP{240, 0, 0, 0},
Mask: net.IPMask{240, 0, 0, 0},
}, struct{}{})
// RFC6890 // RFC6890
var localIPv6Nets = []net.IPNet{
// Unspecified & Loopback Address // Unspecified & Loopback Address
net.IPNet{ defaultFilter.InplaceInsertNet(&net.IPNet{
net.IP{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, IP: net.IP{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe}, Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe},
}, }, struct{}{})
// Discard-Only Prefix // Discard-Only Prefix
net.IPNet{ defaultFilter.InplaceInsertNet(&net.IPNet{
net.IP{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, IP: net.IP{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}, }, struct{}{})
// Unique-Local // Unique-Local
net.IPNet{ defaultFilter.InplaceInsertNet(&net.IPNet{
net.IP{0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, IP: net.IP{0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
net.IPMask{0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, Mask: net.IPMask{0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}, }, struct{}{})
// Linked-Scoped Unicast // Linked-Scoped Unicast
net.IPNet{ defaultFilter.InplaceInsertNet(&net.IPNet{
net.IP{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, IP: net.IP{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
net.IPMask{0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, Mask: net.IPMask{0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}, }, struct{}{})
} }
func IsGlobalIP(ip net.IP) bool { func IsGlobalIP(ip net.IP) bool {
if ip == nil { if ip == nil {
return false return false
} }
if ipv4 := ip.To4(); len(ipv4) == net.IPv4len { _, contained := defaultFilter.GetByIP(ip)
for _, ipnet := range localIPv4Nets { return !contained
if ipnet.Contains(ip) {
return false
}
}
return true
}
if len(ip) == net.IPv6len {
for _, ipnet := range localIPv6Nets {
if ipnet.Contains(ip) {
return false
}
}
return true
}
return true
} }

34
json-dns/globalip_test.go Normal file
View File

@@ -0,0 +1,34 @@
package jsondns
import (
"fmt"
"net"
)
func ExampleIsGlobalIP() {
fmt.Println(IsGlobalIP(net.ParseIP("127.0.0.1")))
fmt.Println(IsGlobalIP(net.IP{192, 168, 1, 1}))
fmt.Println(IsGlobalIP(net.ParseIP("8.8.8.8")))
fmt.Println(IsGlobalIP(net.IP{8, 8, 4, 4}))
fmt.Println(IsGlobalIP(net.ParseIP("::1")))
fmt.Println(IsGlobalIP(net.IP{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}))
fmt.Println(IsGlobalIP(net.ParseIP("2001:4860:4860::8888")))
fmt.Println(IsGlobalIP(net.IP{0x20, 0x01, 0x48, 0x60, 0x48, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x44}))
fmt.Println(IsGlobalIP(net.ParseIP("::ffff:127.0.0.1")))
fmt.Println(IsGlobalIP(net.IP{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 192, 168, 1, 1}))
fmt.Println(IsGlobalIP(net.ParseIP("::ffff:808:808")))
fmt.Println(IsGlobalIP(net.IP{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 8, 8, 4, 4}))
// Output:
// false
// false
// true
// true
// false
// false
// true
// true
// false
// false
// true
// true
}

View File

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

View File

@@ -21,12 +21,32 @@
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
*/ */
package jsonDNS package jsondns
import ( import (
"encoding/json"
"time" "time"
) )
type QuestionList []Question
func (ql *QuestionList) UnmarshalJSON(b []byte) error {
// Fix variant question response in Response.Question
//
// Solution taken from:
// https://engineering.bitnami.com/articles/dealing-with-json-with-non-homogeneous-types-in-go.html
// https://archive.is/NU4zR
if len(b) > 0 && b[0] == '[' {
return json.Unmarshal(b, (*[]Question)(ql))
}
var q Question
if err := json.Unmarshal(b, &q); err != nil {
return err
}
*ql = []Question{q}
return nil
}
type Response struct { type Response struct {
// Standard DNS response code (32 bit integer) // Standard DNS response code (32 bit integer)
Status uint32 `json:"Status"` Status uint32 `json:"Status"`
@@ -41,7 +61,7 @@ type Response struct {
AD bool `json:"AD"` AD bool `json:"AD"`
// Whether the client asked to disable DNSSEC // Whether the client asked to disable DNSSEC
CD bool `json:"CD"` CD bool `json:"CD"`
Question []Question `json:"Question"` Question QuestionList `json:"Question"`
Answer []RR `json:"Answer,omitempty"` Answer []RR `json:"Answer,omitempty"`
Authority []RR `json:"Authority,omitempty"` Authority []RR `json:"Authority,omitempty"`
Additional []RR `json:"Additional,omitempty"` Additional []RR `json:"Additional,omitempty"`

View File

@@ -21,7 +21,7 @@
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
*/ */
package jsonDNS package jsondns
import ( import (
"fmt" "fmt"
@@ -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
@@ -119,7 +119,7 @@ func Unmarshal(msg *dns.Msg, resp *Response, udpSize uint16, ednsClientNetmask u
if ednsClientFamily == 1 { if ednsClientFamily == 1 {
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
} }
edns0Subnet := new(dns.EDNS0_SUBNET) edns0Subnet := new(dns.EDNS0_SUBNET)

View File

@@ -6,6 +6,8 @@
<string>org.eu.starlab.doh.client</string> <string>org.eu.starlab.doh.client</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>/usr/local/bin/doh-logger</string>
<string>doh-client</string>
<string>/usr/local/bin/doh-client</string> <string>/usr/local/bin/doh-client</string>
<string>-conf</string> <string>-conf</string>
<string>/usr/local/etc/dns-over-https/doh-client.conf</string> <string>/usr/local/etc/dns-over-https/doh-client.conf</string>

View File

@@ -6,6 +6,8 @@
<string>org.eu.starlab.doh.server</string> <string>org.eu.starlab.doh.server</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>/usr/local/bin/doh-logger</string>
<string>doh-server</string>
<string>/usr/local/bin/doh-server</string> <string>/usr/local/bin/doh-server</string>
<string>-conf</string> <string>-conf</string>
<string>/usr/local/etc/dns-over-https/doh-server.conf</string> <string>/usr/local/etc/dns-over-https/doh-server.conf</string>