Compare commits

..

86 Commits

Author SHA1 Message Date
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
25 changed files with 788 additions and 184 deletions

View File

@@ -4,6 +4,69 @@ This Changelog records major changes between versions.
Not all changes are recorded. Please check git log for details. Not all changes are recorded. Please check git log for details.
## Version 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 ## Version 1.3.2
- Fix version string in HTTP User-Agent - Fix version string in HTTP User-Agent

View File

@@ -1,19 +1,33 @@
.PHONY: all clean install uninstall deps .PHONY: all clean install uninstall deps
GOBUILD=go build PREFIX = /usr/local
GOGET=go get -d -v
GOGET_UPDATE=go get -d -u -v ifeq ($(GOROOT),)
PREFIX=/usr/local GOBUILD = go build
ifeq ($(shell uname),Darwin) GOGET = go get -d -v
CONFDIR=/usr/local/etc/dns-over-https GOGET_UPDATE = go get -d -u -v
else else
CONFDIR=/etc/dns-over-https GOBUILD = $(GOROOT)/bin/go build
GOGET = $(GOROOT)/bin/go get -d -v
GOGET_UPDATE = $(GOROOT)/bin/go get -d -u -v
endif
ifeq ($(shell uname),Darwin)
CONFDIR = /usr/local/etc/dns-over-https
else
CONFDIR = /etc/dns-over-https
endif endif
all: doh-client/doh-client doh-server/doh-server all: doh-client/doh-client doh-server/doh-server
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
@@ -22,17 +36,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)"; \
@@ -45,8 +62,8 @@ deps:
$(GOGET_UPDATE) github.com/m13253/dns-over-https/json-dns $(GOGET_UPDATE) github.com/m13253/dns-over-https/json-dns
$(GOGET) ./doh-client ./doh-server $(GOGET) ./doh-client ./doh-server
doh-client/doh-client: deps doh-client/client.go doh-client/config.go doh-client/google.go doh-client/ietf.go doh-client/main.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go doh-client/doh-client: deps doh-client/client.go doh-client/config.go doh-client/google.go doh-client/ietf.go doh-client/main.go doh-client/version.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go
cd doh-client && $(GOBUILD) cd doh-client && $(GOBUILD)
doh-server/doh-server: deps doh-server/config.go doh-server/google.go doh-server/ietf.go doh-server/main.go doh-server/server.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go doh-server/doh-server: deps doh-server/config.go doh-server/google.go doh-server/ietf.go doh-server/main.go doh-server/server.go doh-server/version.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go
cd doh-server && $(GOBUILD) cd doh-server && $(GOBUILD)

View File

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

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 -static-stdlib $<
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,11 +25,13 @@ package main
import ( import (
"context" "context"
"fmt"
"log" "log"
"math/rand" "math/rand"
"net" "net"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -37,18 +39,23 @@ import (
"github.com/m13253/dns-over-https/json-dns" "github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/idna"
) )
type Client struct { type Client struct {
conf *config conf *config
bootstrap []string bootstrap []string
udpServers []*dns.Server passthrough []string
tcpServers []*dns.Server udpClient *dns.Client
bootstrapResolver *net.Resolver tcpClient *dns.Client
cookieJar *cookiejar.Jar udpServers []*dns.Server
httpClientMux *sync.RWMutex tcpServers []*dns.Server
httpTransport *http.Transport bootstrapResolver *net.Resolver
httpClient *http.Client cookieJar http.CookieJar
httpClientMux *sync.RWMutex
httpTransport *http.Transport
httpClient *http.Client
httpClientLastCreate time.Time
} }
type DNSRequest struct { type DNSRequest struct {
@@ -57,6 +64,7 @@ type DNSRequest struct {
udpSize uint16 udpSize uint16
ednsClientAddress net.IP ednsClientAddress net.IP
ednsClientNetmask uint8 ednsClientNetmask uint8
currentUpstream string
err error err error
} }
@@ -65,19 +73,28 @@ func NewClient(conf *config) (c *Client, err error) {
conf: conf, conf: conf,
} }
udpH := dns.HandlerFunc(c.udpHandlerFunc) udpHandler := dns.HandlerFunc(c.udpHandlerFunc)
tcpH := dns.HandlerFunc(c.tcpHandlerFunc) tcpHandler := dns.HandlerFunc(c.tcpHandlerFunc)
c.udpClient = &dns.Client{
Net: "udp",
UDPSize: dns.DefaultMsgSize,
Timeout: time.Duration(conf.Timeout) * time.Second,
}
c.tcpClient = &dns.Client{
Net: "tcp",
Timeout: time.Duration(conf.Timeout) * time.Second,
}
for _, addr := range conf.Listen { for _, addr := range conf.Listen {
c.udpServers = append(c.udpServers, &dns.Server{ c.udpServers = append(c.udpServers, &dns.Server{
Addr: addr, Addr: addr,
Net: "udp", Net: "udp",
Handler: udpH, Handler: udpHandler,
UDPSize: 4096, UDPSize: dns.DefaultMsgSize,
}) })
c.tcpServers = append(c.tcpServers, &dns.Server{ c.tcpServers = append(c.tcpServers, &dns.Server{
Addr: addr, Addr: addr,
Net: "tcp", Net: "tcp",
Handler: tcpH, Handler: tcpHandler,
}) })
} }
c.bootstrapResolver = net.DefaultResolver c.bootstrapResolver = net.DefaultResolver
@@ -103,6 +120,15 @@ func NewClient(conf *config) (c *Client, err error) {
return conn, err return conn, err
}, },
} }
if len(conf.Passthrough) != 0 {
c.passthrough = make([]string, len(conf.Passthrough))
for i, passthrough := range conf.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,
@@ -112,7 +138,10 @@ func NewClient(conf *config) (c *Client, err error) {
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 {
@@ -124,24 +153,35 @@ func NewClient(conf *config) (c *Client, err error) {
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.Timeout)*time.Second {
return nil
}
if c.httpTransport != nil { if c.httpTransport != nil {
c.httpTransport.CloseIdleConnections() c.httpTransport.CloseIdleConnections()
} }
dialer := &net.Dialer{
Timeout: time.Duration(c.conf.Timeout) * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
Resolver: c.bootstrapResolver,
}
c.httpTransport = &http.Transport{ c.httpTransport = &http.Transport{
DialContext: (&net.Dialer{ DialContext: dialer.DialContext,
Timeout: time.Duration(c.conf.Timeout) * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
Resolver: c.bootstrapResolver,
}).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.Timeout) * time.Second, TLSHandshakeTimeout: time.Duration(c.conf.Timeout) * time.Second,
} }
if c.conf.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 {
return err return err
@@ -150,6 +190,7 @@ 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
} }
@@ -176,11 +217,73 @@ func (c *Client) Start() error {
} }
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.Timeout)*time.Second)
defer cancel()
if r.Response {
log.Println("Received a response packet") log.Println("Received a response packet")
return return
} }
if len(r.Question) != 1 {
log.Println("Number of questions is not 1")
reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply)
return
}
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.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
}
requestType := "" requestType := ""
if len(c.conf.UpstreamIETF) == 0 { if len(c.conf.UpstreamIETF) == 0 {
requestType = "application/dns-json" requestType = "application/dns-json"
@@ -198,13 +301,21 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
var req *DNSRequest var req *DNSRequest
if requestType == "application/dns-json" { if requestType == "application/dns-json" {
req = c.generateRequestGoogle(w, r, isTCP) req = c.generateRequestGoogle(ctx, w, r, isTCP)
} else if requestType == "application/dns-message" { } else if requestType == "application/dns-message" {
req = c.generateRequestIETF(w, r, isTCP) req = c.generateRequestIETF(ctx, w, r, isTCP)
} else { } else {
panic("Unknown request Content-Type") panic("Unknown request Content-Type")
} }
if req.response != nil {
defer req.response.Body.Close()
for _, header := range c.conf.DebugHTTPHeaders {
if value := req.response.Header.Get(header); value != "" {
log.Printf("%s: %s\n", header, value)
}
}
}
if req.err != nil { if req.err != nil {
return return
} }
@@ -226,9 +337,9 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
} }
if contentType == "application/json" { if contentType == "application/json" {
c.parseResponseGoogle(w, r, isTCP, req) c.parseResponseGoogle(ctx, w, r, isTCP, req)
} else if contentType == "application/dns-message" { } else if contentType == "application/dns-message" {
c.parseResponseIETF(w, r, isTCP, req) c.parseResponseIETF(ctx, w, r, isTCP, req)
} else { } else {
panic("Unknown response Content-Type") panic("Unknown response Content-Type")
} }
@@ -244,7 +355,7 @@ 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) {
@@ -271,8 +382,8 @@ func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddre
ednsClientAddress = ipv4.Mask(ipv4Mask24) ednsClientAddress = ipv4.Mask(ipv4Mask24)
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientAddress = ip.Mask(ipv6Mask48) ednsClientAddress = ip.Mask(ipv6Mask56)
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
} }
return return

View File

@@ -30,14 +30,17 @@ import (
) )
type config struct { type config struct {
Listen []string `toml:"listen"` Listen []string `toml:"listen"`
UpstreamGoogle []string `toml:"upstream_google"` UpstreamGoogle []string `toml:"upstream_google"`
UpstreamIETF []string `toml:"upstream_ietf"` UpstreamIETF []string `toml:"upstream_ietf"`
Bootstrap []string `toml:"bootstrap"` Bootstrap []string `toml:"bootstrap"`
Timeout uint `toml:"timeout"` Passthrough []string `toml:"passthrough"`
NoCookies bool `toml:"no_cookies"` Timeout uint `toml:"timeout"`
NoECS bool `toml:"no_ecs"` NoCookies bool `toml:"no_cookies"`
Verbose bool `toml:"verbose"` NoECS bool `toml:"no_ecs"`
NoIPv6 bool `toml:"no_ipv6"`
Verbose bool `toml:"verbose"`
DebugHTTPHeaders []string `toml:"debug_http_headers"`
} }
func loadConfig(path string) (*config, error) { func loadConfig(path string) (*config, error) {

View File

@@ -1,7 +1,9 @@
# DNS listen port # DNS listen port
listen = [ listen = [
"127.0.0.1:53", "127.0.0.1:53",
"127.0.0.1:5380",
"[::1]:53", "[::1]:53",
"[::1]:5380",
] ]
# HTTP path for upstream resolver # HTTP path for upstream resolver
@@ -16,6 +18,11 @@ upstream_google = [
#"https://1.1.1.1/dns-query", #"https://1.1.1.1/dns-query",
#"https://1.0.0.1/dns-query", #"https://1.0.0.1/dns-query",
# 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/
#"https://dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion/dns-query",
] ]
upstream_ietf = [ upstream_ietf = [
@@ -27,6 +34,11 @@ upstream_ietf = [
#"https://1.1.1.1/dns-query", #"https://1.1.1.1/dns-query",
#"https://1.0.0.1/dns-query", #"https://1.0.0.1/dns-query",
# 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/
#"https://dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion/dns-query",
] ]
# Bootstrap DNS server to resolve the address of the upstream resolver # Bootstrap DNS server to resolve the address of the upstream resolver
@@ -46,8 +58,26 @@ bootstrap = [
] ]
# The domain names here are directly passed to bootstrap servers listed above,
# allowing captive portal detection and systems without RTC to work.
# Only effective if at least one bootstrap server is configured.
passthrough = [
"captive.apple.com",
"connectivitycheck.gstatic.com",
"detectportal.firefox.com",
"msftconnecttest.com",
"nmcheck.gnome.org",
"pool.ntp.org",
"time.apple.com",
"time.asia.apple.com",
"time.euro.apple.com",
"time.nist.gov",
"time.windows.com",
]
# Timeout for upstream request # Timeout for upstream request
timeout = 10 timeout = 30
# Disable HTTP Cookies # Disable HTTP Cookies
# #
@@ -55,15 +85,23 @@ 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
# Enable logging # Enable logging
verbose = false verbose = false

View File

@@ -24,6 +24,7 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@@ -33,35 +34,28 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/m13253/dns-over-https/json-dns" "github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func (c *Client) generateRequestGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP bool) *DNSRequest { func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool) *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 {
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) numServers := len(c.conf.UpstreamGoogle)
@@ -85,6 +79,7 @@ func (c *Client) generateRequestGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP b
req, err := http.NewRequest("GET", requestURL, nil) req, err := http.NewRequest("GET", 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{
@@ -92,18 +87,23 @@ func (c *Client) generateRequestGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP b
} }
} }
req.Header.Set("Accept", "application/json, application/dns-message, application/dns-udpwireformat") req.Header.Set("Accept", "application/json, application/dns-message, application/dns-udpwireformat")
req.Header.Set("User-Agent", jsonDNS.USER_AGENT) req.Header.Set("User-Agent", USER_AGENT)
req = req.WithContext(ctx)
c.httpClientMux.RLock() c.httpClientMux.RLock()
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
c.httpClientMux.RUnlock() c.httpClientMux.RUnlock()
if 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 +111,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,
} }
} }
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 != 200 {
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;") {

View File

@@ -25,6 +25,7 @@ package main
import ( import (
"bytes" "bytes"
"context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@@ -32,7 +33,6 @@ import (
"math/rand" "math/rand"
"net" "net"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
@@ -40,42 +40,16 @@ import (
"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) *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{
@@ -127,14 +102,14 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
numServers := len(c.conf.UpstreamIETF) numServers := len(c.conf.UpstreamIETF)
upstream := c.conf.UpstreamIETF[rand.Intn(numServers)] upstream := c.conf.UpstreamIETF[rand.Intn(numServers)]
requestURL := fmt.Sprintf("%s?ct=application/dns-udpwireformat&dns=%s", upstream, requestBase64) requestURL := fmt.Sprintf("%s?ct=application/dns-message&dns=%s", upstream, requestBase64)
//requestURL := fmt.Sprintf("%s?ct=application/dns-message&dns=%s", upstream, requestBase64)
var req *http.Request var req *http.Request
if len(requestURL) < 2048 { if len(requestURL) < 2048 {
req, err = http.NewRequest("GET", requestURL, nil) req, err = http.NewRequest("GET", 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{
@@ -145,6 +120,7 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
req, err = http.NewRequest("POST", upstream, bytes.NewReader(requestBinary)) req, err = http.NewRequest("POST", upstream, 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{
@@ -154,18 +130,23 @@ func (c *Client) generateRequestIETF(w dns.ResponseWriter, r *dns.Msg, isTCP boo
req.Header.Set("Content-Type", "application/dns-message") req.Header.Set("Content-Type", "application/dns-message")
} }
req.Header.Set("Accept", "application/dns-message, application/dns-udpwireformat, application/json") req.Header.Set("Accept", "application/dns-message, application/dns-udpwireformat, application/json")
req.Header.Set("User-Agent", jsonDNS.USER_AGENT) req.Header.Set("User-Agent", USER_AGENT)
req = req.WithContext(ctx)
c.httpClientMux.RLock() c.httpClientMux.RLock()
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
c.httpClientMux.RUnlock() c.httpClientMux.RUnlock()
if 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,16 +154,17 @@ 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,
} }
} }
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 != 200 {
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/dns-message" && !strings.HasPrefix(contentType, "application/dns-message;") { if contentType != "application/dns-message" && !strings.HasPrefix(contentType, "application/dns-message;") {

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-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()
if *showVersion {
fmt.Printf("doh-server %s\nHomepage: https://github.com/m13253/dns-over-https\n", VERSION)
return
}
if pidFile != nil && *pidFile != "" {
ok, err := checkPIDFile(*pidFile)
if err != nil {
log.Printf("Error checking PID file: %v\n", err)
}
if !ok {
return
}
}
conf, err := loadConfig(*confPath) conf, err := loadConfig(*confPath)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)

View File

@@ -21,7 +21,9 @@
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
*/ */
package jsonDNS package main
const VERSION = "1.3.2" const (
const USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)" VERSION = "1.4.2"
USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)"
)

View File

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

View File

@@ -5,9 +5,14 @@ listen = [
] ]
# 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
@@ -16,6 +21,8 @@ path = "/dns-query"
# Upstream DNS resolver # Upstream DNS resolver
# If multiple servers are specified, a random one will be chosen each time. # If multiple servers are specified, a random one will be chosen each time.
upstream = [ upstream = [
"1.1.1.1:53",
"1.0.0.1:53",
"8.8.8.8:53", "8.8.8.8:53",
"8.8.4.4:53", "8.8.4.4:53",
] ]

View File

@@ -24,6 +24,7 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
@@ -38,7 +39,7 @@ import (
"golang.org/x/net/idna" "golang.org/x/net/idna"
) )
func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNSRequest { func (s *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest {
name := r.FormValue("name") name := r.FormValue("name")
if name == "" { if name == "" {
return &DNSRequest{ return &DNSRequest{
@@ -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,7 +169,7 @@ func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNS
} }
} }
func (s *Server) generateResponseGoogle(w http.ResponseWriter, r *http.Request, req *DNSRequest) { func (s *Server) generateResponseGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) {
respJSON := jsonDNS.Marshal(req.response) respJSON := jsonDNS.Marshal(req.response)
respStr, err := json.Marshal(respJSON) respStr, err := json.Marshal(respJSON)
if err != nil { if err != nil {
@@ -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,22 @@
package main package main
import ( import (
"bytes"
"context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/m13253/dns-over-https/json-dns" "github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func (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 {
@@ -60,6 +63,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 +86,27 @@ 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)
} }
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 {
@@ -116,7 +127,7 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
ednsClientNetmask = 24 ednsClientNetmask = 24
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
ednsClientNetmask = 48 ednsClientNetmask = 56
} }
edns0Subnet = new(dns.EDNS0_SUBNET) edns0Subnet = new(dns.EDNS0_SUBNET)
edns0Subnet.Code = dns.EDNS0SUBNET edns0Subnet.Code = dns.EDNS0SUBNET
@@ -129,14 +140,15 @@ func (s *Server) parseRequestIETF(w http.ResponseWriter, r *http.Request) *DNSRe
} }
return &DNSRequest{ return &DNSRequest{
request: msg, request: msg,
isTailored: isTailored, transactionID: transactionID,
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.Println(err)
@@ -148,16 +160,47 @@ func (s *Server) generateResponseIETF(w http.ResponseWriter, r *http.Request, re
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 {
w.WriteHeader(503) w.WriteHeader(503)
} }
w.Write(respBytes) w.Write(respBytes)
} }
// 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")) {
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") {
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)

View File

@@ -24,6 +24,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"math/rand" "math/rand"
@@ -46,11 +47,13 @@ type Server struct {
} }
type DNSRequest struct { type DNSRequest struct {
request *dns.Msg request *dns.Msg
response *dns.Msg response *dns.Msg
isTailored bool transactionID uint16
errcode int currentUpstream string
errtext string isTailored bool
errcode int
errtext string
} }
func NewServer(conf *config) (s *Server) { func NewServer(conf *config) (s *Server) {
@@ -58,6 +61,7 @@ func NewServer(conf *config) (s *Server) {
conf: conf, conf: conf,
udpClient: &dns.Client{ udpClient: &dns.Client{
Net: "udp", Net: "udp",
UDPSize: dns.DefaultMsgSize,
Timeout: time.Duration(conf.Timeout) * time.Second, Timeout: time.Duration(conf.Timeout) * time.Second,
}, },
tcpClient: &dns.Client{ tcpClient: &dns.Client{
@@ -102,13 +106,31 @@ func (s *Server) Start() error {
} }
func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) { func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", jsonDNS.USER_AGENT) ctx := r.Context()
w.Header().Set("X-Powered-By", jsonDNS.USER_AGENT)
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
@@ -148,31 +170,36 @@ func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
var req *DNSRequest var req *DNSRequest
if contentType == "application/dns-json" { if contentType == "application/dns-json" {
req = s.parseRequestGoogle(w, r) req = s.parseRequestGoogle(ctx, w, r)
} else if contentType == "application/dns-message" { } else if contentType == "application/dns-message" {
req = s.parseRequestIETF(w, r) req = s.parseRequestIETF(ctx, w, r)
} else if contentType == "application/dns-udpwireformat" { } else if contentType == "application/dns-udpwireformat" {
req = s.parseRequestIETF(w, r) req = s.parseRequestIETF(ctx, w, r)
} else { } else {
jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 415) jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 415)
return return
} }
if req.errcode == 444 {
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
} }
req = s.patchRootRD(req)
var err error var err error
req.response, err = s.doDNSQuery(req.request) req, err = s.doDNSQuery(ctx, req)
if err != nil { if err != nil {
jsonDNS.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503) jsonDNS.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503)
return return
} }
if responseType == "application/json" { if responseType == "application/json" {
s.generateResponseGoogle(w, r, req) s.generateResponseGoogle(ctx, w, r, req)
} else if responseType == "application/dns-message" { } else if responseType == "application/dns-message" {
s.generateResponseIETF(w, r, req) s.generateResponseIETF(ctx, w, r, req)
} else { } else {
panic("Unknown response Content-Type") panic("Unknown response Content-Type")
} }
@@ -207,23 +234,34 @@ func (s *Server) findClientIP(r *http.Request) net.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
}
func (s *Server) doDNSQuery(ctx context.Context, req *DNSRequest) (resp *DNSRequest, err error) {
// TODO(m13253): Make ctx work. Waiting for a patch for ExchangeContext from miekg/dns.
numServers := len(s.conf.Upstream) numServers := len(s.conf.Upstream)
for i := uint(0); i < s.conf.Tries; i++ { for i := uint(0); i < s.conf.Tries; i++ {
server := s.conf.Upstream[rand.Intn(numServers)] req.currentUpstream = s.conf.Upstream[rand.Intn(numServers)]
if !s.conf.TCPOnly { if !s.conf.TCPOnly {
resp, _, err = s.udpClient.Exchange(msg, server) req.response, _, err = s.udpClient.Exchange(req.request, req.currentUpstream)
if err == dns.ErrTruncated { if err == nil && req.response != nil && req.response.Truncated {
log.Println(err) log.Println(err)
resp, _, err = s.tcpClient.Exchange(msg, server) req.response, _, err = s.tcpClient.Exchange(req.request, req.currentUpstream)
} }
} else { } else {
resp, _, err = s.tcpClient.Exchange(msg, server) req.response, _, err = s.tcpClient.Exchange(req.request, req.currentUpstream)
} }
if err == nil { if err == nil {
return return req, nil
} }
log.Println(err) log.Printf("DNS error from upstream %s: %s\n", req.currentUpstream, err.Error())
} }
return return req, 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 = "1.4.2"
USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)"
)

View File

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

View File

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

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>