Compare commits

...

101 Commits

Author SHA1 Message Date
Star Brilliant
82c5f0d327 Merge pull request #185 from shahradelahi/feature/bind-to-interface
Some checks failed
Docker / docker (client) (push) Failing after 33s
Docker / docker (server) (push) Failing after 31s
Go build for Linux / Build (push) Failing after 2m40s
2026-03-17 14:06:24 +00:00
Star Brilliant
602b3d6322 Merge pull request #186 from vinnyperella/patch-2 2026-01-23 18:43:00 +00:00
Vinny
db0bd43256 chore: upgrade dependencies 2026-01-23 17:26:28 +00:00
Shahrad Elahi
06e3d67f79 feat: support dual-stack for interface binding 2026-01-23 16:04:57 +00:00
Shahrad Elahi
d27aef852d feat: add option to bind outgoing connections to a specific interface
This adds a new `interface` configuration option to `doh-client` that allows users to specify a network interface for all outgoing DNS queries (including bootstrap and passthrough traffic).
2026-01-23 01:41:28 +00:00
Star Brilliant
6c561eb412 Merge pull request #181 from vinnyperella/patch-1 2025-11-18 20:38:06 +00:00
Vinny
381bf28a69 chore: upgrade dependencies 2025-11-18 16:19:55 +00:00
Star Brilliant
0b0651a015 Merge pull request #178 from vinnyperella/patch-1 2025-09-16 16:12:40 +00:00
Vinny
3130a747f8 chore: upgrade dependencies 2025-09-16 13:17:20 +00:00
Star Brilliant
fe9f9f9ad2 Merge pull request #176 from vinnyperella/patch-1 2025-06-17 17:18:03 +00:00
Vinny
00c6af00ed chore: upgrade dependencies 2025-06-17 17:15:43 +00:00
Star Brilliant
04f3e029ac Merge pull request #172 from bfahrenfort/patch-1
config: Add captive portal domains
2025-05-28 21:10:00 +00:00
bfahrenfort
87b3eedded doh-client: lint 2025-05-28 15:26:28 -05:00
Star Brilliant
c57a45deaa Merge pull request #174 from m13253/m13253/restart-backoff
Move StartLimitIntervalSec=0 from [Service] to [Unit]
2025-05-28 00:37:56 +00:00
Star Brilliant
8bc06acc6e Move StartLimitIntervalSec=0 from [Service] to [Unit]
This solves the warning message:
> systemd[1]: /usr/lib/systemd/system/doh-client.service:16: Unknown key 'StartLimitIntervalSec' in section [Service], ignoring.
2025-05-24 12:45:26 +00:00
Star Brilliant
0263a32c22 Merge pull request #173 from vinnyperella/patch-1 2025-05-18 00:10:28 +00:00
Vinny
354d0377b3 chore: upgrade dependencies 2025-05-14 19:26:12 +00:00
Star Brilliant
59a47c881b Merge pull request #171 from m13253/m13253/restart-backoff
When systemd service fail to start, use an exponential backoff delay to restart it
2025-05-13 01:50:24 +00:00
bfahrenfort
dfba0c36c5 config: Add captive portal domains 2025-05-12 15:48:41 -05:00
Star Brilliant
03da3a801f When systemd service fail to start, use an exponential backoff delay to restart it
This solves an issue that on (at least) Fedora and if NetworkManager starts too slow, systemd may stop trying to start it.
2025-05-09 00:12:11 +00:00
GreyXor
f13dea391f chore: v2.3.10 2025-03-29 01:02:43 +01:00
GreyXor
45edaad516 chore: upgrade dependencies 2025-03-29 01:00:16 +01:00
Star Brilliant
2179ee0054 Merge pull request #169 from vinnyperella/patch-4
Update go.yml
2025-03-28 14:07:47 +00:00
Vinny
6ddb1ad401 Update go.yml
Updated actions checkout/cache to v4.
2025-03-28 14:04:53 +00:00
GreyXor
35e0835949 chore: v2.3.9 2025-03-28 12:20:42 +01:00
GreyXor
5c744889be chore: upgrade dependencies 2025-03-28 11:47:07 +01:00
GreyXor
cb1c336217 chore: upgrade dependencies 2025-02-20 10:41:30 +01:00
GreyXor
f82ac1118e feat: new version 2.3.8 2025-01-28 09:09:13 +01:00
GreyXor
e5fef5690e chore: upgrade package dependencies 2025-01-28 09:07:28 +01:00
GreyXor
9c997f1491 Merge pull request #166 from vinnyperella/patch-3
chore: update dependencies
2025-01-27 22:51:34 +01:00
Vinny
d33fc60182 chore: update dependencies 2025-01-27 15:01:08 -05:00
Vinny
89bb7e95a6 chore: update dependencies 2025-01-27 14:58:41 -05:00
GreyXor
c5a06988d5 chore: upgrade dependencies 2024-12-28 18:17:23 +01:00
Satish Gaikwad
ea57996685 Merge pull request #162 from m13253/add-multi-upstream-dns-servers 2024-12-20 08:41:18 -08:00
Satish Gaikwad
0a1aa98a01 Updated comment 2024-12-19 22:27:26 +00:00
Satish Gaikwad
7ca84d162f Updated comment 2024-12-19 22:24:23 +00:00
Satish Gaikwad
0ea5c5015f Added a note for multiple DNS server support in the container image 2024-12-17 18:56:10 +00:00
Satish Gaikwad
65424da23f Update example to use multiple upstream DNS servers
Update example to use multiple upstream DNS servers
2024-12-17 10:41:51 -08:00
GreyXor
d35e6a6117 chore: upgrade package dependencies 2024-11-14 12:47:00 +01:00
Star Brilliant
4a98f0997b Update to actions/upload-artifact@v4 2024-11-13 18:58:47 +00:00
GreyXor
b70babf43e chore: upgrade package dependencies 2024-11-09 09:00:22 +01:00
GreyXor
967c43016c chore: upgrade package dependencies 2024-08-10 08:39:30 +02:00
GreyXor
a10da0ed10 Pre-bump to next version 2.3.7 2024-07-05 18:14:09 +02:00
GreyXor
379aa917f9 chore: upgrade package dependencies 2024-07-05 18:10:37 +02:00
GreyXor
1a90868fe9 Merge pull request #159 from testwill/fmt
chore: unnecessary use of fmt.Sprintf
2024-06-06 15:32:25 +02:00
guoguangwu
fa6e999d54 chore: unnecessary use of fmt.Sprintf
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
2024-05-10 17:18:49 +08:00
GreyXor
72be4857a3 chore: upgrade package dependencies 2024-05-06 16:18:48 +02:00
GreyXor
a5e8a7e089 chore: upgrade package dependencies 2024-04-11 11:56:15 +02:00
Satish Gaikwad
6b7f3927db Merge pull request #157 from m13253/Pre-bump-doh-client-to-next-version-2-3-6
Pre-bump doh-client to next version 2.3.6
2024-03-12 13:14:16 -07:00
Satish Gaikwad
2e2b012c9d Pre-bump to next version 2.3.6 2024-03-12 13:13:03 -07:00
Satish Gaikwad
9e67d1b8bd Merge pull request #156 from m13253/pre-bump-next-version-to-2-3-6
Pre-bump to next version 2.3.6
2024-03-12 13:09:49 -07:00
Satish Gaikwad
b8c3122f54 Pre-bump to next version 2.3.6 2024-03-12 13:07:28 -07:00
GreyXor
65622feb5f chore: upgrade package dependencies 2024-03-05 11:29:59 +01:00
GreyXor
057797fed3 chore: upgrade package dependencies 2024-02-08 11:58:30 +01:00
GreyXor
e171ec0da2 chore: upgrade package dependencies 2024-01-08 21:19:31 +01:00
GreyXor
a2b984f816 chore: upgrade package dependencies 2023-12-21 14:41:57 +01:00
GreyXor
2662d9f35b chores: upgrade deps 2023-11-13 12:40:37 +01:00
GreyXor
7d9ea18607 upgrade dependencies 2023-10-12 16:31:32 +02:00
GreyXor
967ca0103e upgrade dependencies 2023-10-06 10:58:27 +02:00
GreyXor
6fb0df257e upgrade dependencies 2023-09-15 14:25:53 +02:00
Jamesits
d00be8b698 Merge pull request #152 from Gontier-Julien/ldflags
Add ldflags
2023-08-24 08:43:38 -07:00
Gontier Julien
f08eb16d0b Add ldflags
Signed-off-by: Gontier Julien <gontierjulien68@gmail.com>
2023-08-24 17:34:20 +02:00
James Swineson
2bda149686 revert last commit 2023-08-23 10:38:03 +08:00
James Swineson
299e8f9245 CI: parallelize multiarch build 2023-08-23 10:32:22 +08:00
James Swineson
f3e529865a enable multiarch Docker builds 2023-08-23 10:20:08 +08:00
James Swineson
94bc8c0587 remove unused logout action 2023-08-23 10:14:03 +08:00
James Swineson
f5f03513f8 fix CI config grammar 2023-08-23 10:12:52 +08:00
James Swineson
9e1f2c70cc update environment tag 2023-08-23 10:11:12 +08:00
James Swineson
979780c8d5 add CI job for Docker Hub build and push 2023-08-23 10:08:23 +08:00
James Swineson
f697ba1e1e bump Golang version in CI to 1.21 2023-08-23 10:06:23 +08:00
GreyXor
32f3ad71f7 upgrade dependencies 2023-07-06 10:08:37 +02:00
GreyXor
83924dd76b Fix rand usage, typos and 30 bytes saved by fields alignment 2023-07-04 16:11:18 +02:00
GreyXor
e72363306b cleanup code style 2023-07-04 16:09:12 +02:00
GreyXor
e4d309bbc0 upgrade dependencies 2023-07-04 16:06:26 +02:00
GreyXor
0690c41610 Upgrade dependencies 2023-05-25 20:44:19 +02:00
GreyXor
244e4b5b8f Upgrade dependencies 2023-03-12 12:39:53 +01:00
Star Brilliant
a7708f71b3 Pre-bump to next version 2.3.4 2023-01-26 10:13:43 +00:00
Star Brilliant
d6a5c91f67 Release 2.3.3 2023-01-26 10:13:10 +00:00
Star Brilliant
fdc1b81e42 Properly truncate DNS packets
This should fix issue #144.
2023-01-25 06:47:13 +00:00
GreyXor
70fc8578c7 feat: upgrade deps 2023-01-11 09:33:57 +01:00
GreyXor
94c223df4b feat: upgrade deps 2022-12-16 13:52:17 +01:00
GreyXor
b9bf6e80f4 fix: io and os instead of deprecated ioutil
Deprecated: As of Go 1.16, the same functionality is now provided by package io or package os, and those implementations should be preferred in new code. See the specific function documentation for details.
https://pkg.go.dev/io/ioutil
2022-10-29 11:35:22 +02:00
GreyXor
1b7eed5afe chore: upgrade deps 2022-10-29 11:19:04 +02:00
Star Brilliant
393019fc14 Merge pull request #140 from m13253/fix-dynamic-user 2022-10-07 16:24:08 +00:00
Star Brilliant
604daa663f systemd: Use DynamicUser=yes instead of User=nobody
Fix issue #139
2022-10-06 16:07:19 +00:00
Star Brilliant
8bedfc4bab Pre-bump to next version 2.3.3 2022-09-19 00:31:26 +00:00
Star Brilliant
ae0333f1c2 Release 2.3.2 2022-09-19 00:30:59 +00:00
Star Brilliant
34014d847e Merge pull request #137 from Gontier-Julien/patch
Removing Firefox 61-62 workaround patch
2022-09-15 06:01:44 +00:00
Star Brilliant
71eecf7b8a Merge pull request #133 from m13253/docs/DoT
docs: explain how to use DNS-over-TLS with nginx/STunnel
2022-09-15 06:01:26 +00:00
Star Brilliant
6e74bbd061 Merge pull request #132 from m13253/docs/README-updates
docs: some README additions
2022-09-15 06:00:16 +00:00
Star Brilliant
f212286c4f Merge pull request #134 from m13253/fix/parse-ecs-golang 2022-09-14 15:23:42 +00:00
Gontier Julien
1fff629074 Removing Firefox 61-62 patch
Signed-off-by: Gontier Julien <gontierjulien68@gmail.com>
2022-09-14 11:15:23 +02:00
Star Brilliant
533ea58e67 Merge pull request #135 from m13253/upgrade-deps
chore: upgrade deps
2022-09-11 07:59:12 +00:00
GreyXor
b7b935767f chore: upgrade deps 2022-09-09 11:56:43 +02:00
gdm85
f1b3133982 fix: add unit tests for CIDR subnets parsing 2022-09-03 13:58:56 +02:00
gdm85
a519b5a9c4 docs: explain how to use DNS-over-TLS with nginx/STunnel 2022-09-03 10:54:06 +02:00
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
29 changed files with 585 additions and 219 deletions

36
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: "Docker"
on:
push:
branches:
- 'master'
jobs:
docker:
runs-on: ubuntu-latest
environment: "Docker Hub"
strategy:
matrix:
variant: ["client", "server"]
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
logout: true
- # TODO: port https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
name: Build and push
uses: docker/build-push-action@v4
with:
file: ./Dockerfile.${{ matrix.variant }}
tags: m13253/dns-over-https-${{ matrix.variant }}:latest
push: true
platforms: "linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8"

View File

@@ -4,24 +4,24 @@ jobs:
build:
name: Build
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
go-version: 1.25.6
id: go
- name: Check out repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Linux build
run: |
make
- name: Upload Linux build
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: linux-amd64
path: |
@@ -29,7 +29,7 @@ jobs:
doh-server/doh-server
- name: Cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
# A directory to store and save the cache
path: ~/go/pkg/mod

36
.golangci.yml Normal file
View File

@@ -0,0 +1,36 @@
---
issues:
fix: true
linters:
enable-all: true
disable:
- importas
- depguard
- lll
- exhaustruct
- perfsprint
- gochecknoinits
- wsl
- exportloopref
linters-settings:
revive:
enable-all-rules: true
rules:
- name: line-length-limit
disabled: true
gocritic:
enabled-tags:
- diagnostic
- style
- performance
- experimental
- opinionated
govet:
enable-all: true
gci:
sections:
- standard
- default
- prefix(github.com/m13253/dns-over-https/v2)
gofumpt:
extra-rules: true

View File

@@ -4,6 +4,21 @@ This Changelog records major changes between versions.
Not all changes are recorded. Please check git log for details.
## Version 2.3.3
- systemd: Use DynamicUser=yes instead of User=nobody (Fixed #139)
- Migrate deprecated Go packages `ioutil` to `io` and `os`
- Fix a bug that truncates the response improperly, causing malformed DNS responsed (Fixed #144)
## Version 2.3.2
- Documentation updates, including deploying recommenation alongside DoT, thanks @gdm85
- Add unit tests for CIDR subnets parsing, thanks @gdm85
- Removing Firefox 61-62 patch
Since this version, @gdm85, @GreyXor, @Jamesits will be able to maintain this repository alongside @m13253. Anyone who contributed to this project can also apply to be a maintainer.
This is because changes in life have delayed the development of this project. By constructing a community hopefully can we restore the pace of development.
## Version 2.3.1
- No new features in this release

View File

@@ -3,9 +3,9 @@
PREFIX = /usr/local
ifeq ($(GOROOT),)
GOBUILD = go build
GOBUILD = go build -ldflags "-s -w"
else
GOBUILD = $(GOROOT)/bin/go build
GOBUILD = $(GOROOT)/bin/go build -ldflags "-s -w"
endif
ifeq ($(shell uname),Darwin)

View File

@@ -11,8 +11,8 @@ and [IETF DNS-over-HTTPS (RFC 8484)](https://www.rfc-editor.org/rfc/rfc8484.txt)
## 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.)
- Install [Go](https://golang.org), at least version 1.20. 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
@@ -58,7 +58,7 @@ sudo make uninstall
```bash
docker run -d --name doh-server \
-p 8053:8053 \
-e UPSTREAM_DNS_SERVER="udp:8.8.8.8:53" \
-e UPSTREAM_DNS_SERVER="udp:208.67.222.222:53,udp:208.67.220.220:53" \
-e DOH_HTTP_PREFIX="/dns-query" \
-e DOH_SERVER_LISTEN=":8053" \
-e DOH_SERVER_TIMEOUT="10" \
@@ -66,6 +66,16 @@ docker run -d --name doh-server \
-e DOH_SERVER_VERBOSE="false" \
satishweb/doh-server
```
Note: Multiple Upstream DNS server support was added in the container image on 2024-12-19.
Feeling adventurous? Try the latest build:
- `m13253/dns-over-https-server:latest`
- `m13253/dns-over-https-client:latest`
## 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
@@ -84,7 +94,7 @@ The following is a typical DNS-over-HTTPS architecture:
| doh-client +--+ Content Delivery Network +--+ (Apache, Nginx, Caddy) |
+--------------+ +--------------------------+ +------------------------+
Although DNS-over-HTTPS can work alone, a HTTP service muxer would be useful as
Although DNS-over-HTTPS can work alone, an HTTP service muxer would be useful as
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,
@@ -127,7 +137,7 @@ server {
server_tokens off;
ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.3 requires nginx >= 1.13.0
ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.3 requires nginx >= 1.20.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;
@@ -269,10 +279,28 @@ services:
> IPV6 Support for Docker Compose based configuration TBA
### Example configuration: DNS-over-TLS
There is no native [DNS-over-TLS](https://en.wikipedia.org/wiki/DNS_over_TLS) support, but you can easily add it via nginx:
```
stream {
server {
listen *:853 ssl;
proxy_pass ipofyourdnsresolver:port #127.0.0.1:53
}
ssl_certificate /etc/letsencrypt/live/site.yourdomain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/site.yourdomain/privkey.pem;
}
```
The DoT service can also be provided by running a [STunnel](https://www.stunnel.org/) instance to wrap dnsmasq (or any other resolver of your choice, listening on a TCP port);
this approach does not need a stand-alone daemon to provide the DoT service.
## DNSSEC
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
records. An instance of [Pi Hole](https://pi-hole.net) could also be used to validate DNS signatures as well as provide other capabilities.
@@ -315,6 +343,10 @@ Currently supported features are:
- [X] EDNS0 large UDP packet (4 KiB 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.

View File

@@ -38,39 +38,40 @@ import (
"sync"
"time"
"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"
"golang.org/x/net/http2"
"golang.org/x/net/idna"
"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"
)
type Client struct {
conf *config.Config
bootstrap []string
passthrough []string
udpClient *dns.Client
tcpClient *dns.Client
udpServers []*dns.Server
tcpServers []*dns.Server
bootstrapResolver *net.Resolver
httpClientLastCreate time.Time
cookieJar http.CookieJar
selector selector.Selector
httpClientMux *sync.RWMutex
tcpClient *dns.Client
bootstrapResolver *net.Resolver
udpClient *dns.Client
conf *config.Config
httpTransport *http.Transport
httpClient *http.Client
httpClientLastCreate time.Time
selector selector.Selector
udpServers []*dns.Server
tcpServers []*dns.Server
passthrough []string
bootstrap []string
}
type DNSRequest struct {
err error
response *http.Response
reply *dns.Msg
udpSize uint16
ednsClientAddress net.IP
ednsClientNetmask uint8
currentUpstream string
err error
ednsClientAddress net.IP
udpSize uint16
ednsClientNetmask uint8
}
func NewClient(conf *config.Config) (c *Client, err error) {
@@ -89,6 +90,29 @@ func NewClient(conf *config.Config) (c *Client, err error) {
Net: "tcp",
Timeout: time.Duration(conf.Other.Timeout) * time.Second,
}
if c.conf.Other.Interface != "" {
localV4, localV6, err := c.getInterfaceIPs()
if err != nil {
return nil, fmt.Errorf("failed to get interface IPs for %s: %v", c.conf.Other.Interface, err)
}
var localAddr net.IP
if localV4 != nil {
localAddr = localV4
} else {
localAddr = localV6
}
c.udpClient.Dialer = &net.Dialer{
Timeout: time.Duration(conf.Other.Timeout) * time.Second,
LocalAddr: &net.UDPAddr{IP: localAddr},
}
c.tcpClient.Dialer = &net.Dialer{
Timeout: time.Duration(conf.Other.Timeout) * time.Second,
LocalAddr: &net.TCPAddr{IP: localAddr},
}
}
for _, addr := range conf.Listen {
c.udpServers = append(c.udpServers, &dns.Server{
Addr: addr,
@@ -119,6 +143,38 @@ func NewClient(conf *config.Config) (c *Client, err error) {
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
var d net.Dialer
if c.conf.Other.Interface != "" {
localV4, localV6, err := c.getInterfaceIPs()
if err != nil {
log.Printf("Bootstrap dial warning: %v", err)
} else {
numServers := len(c.bootstrap)
bootstrap := c.bootstrap[rand.Intn(numServers)]
host, _, _ := net.SplitHostPort(bootstrap)
ip := net.ParseIP(host)
if ip != nil {
if ip.To4() != nil {
if localV4 != nil {
if strings.HasPrefix(network, "udp") {
d.LocalAddr = &net.UDPAddr{IP: localV4}
} else {
d.LocalAddr = &net.TCPAddr{IP: localV4}
}
}
} else {
if localV6 != nil {
if strings.HasPrefix(network, "udp") {
d.LocalAddr = &net.UDPAddr{IP: localV6}
} else {
d.LocalAddr = &net.TCPAddr{IP: localV6}
}
}
}
}
conn, err := d.DialContext(ctx, network, bootstrap)
return conn, err
}
}
numServers := len(c.bootstrap)
bootstrap := c.bootstrap[rand.Intn(numServers)]
conn, err := d.DialContext(ctx, network, bootstrap)
@@ -234,14 +290,72 @@ func (c *Client) newHTTPClient() error {
if c.httpTransport != nil {
c.httpTransport.CloseIdleConnections()
}
dialer := &net.Dialer{
localV4, localV6, err := c.getInterfaceIPs()
if err != nil {
log.Printf("Interface binding error: %v", err)
return err
}
baseDialer := &net.Dialer{
Timeout: time.Duration(c.conf.Other.Timeout) * time.Second,
KeepAlive: 30 * time.Second,
// DualStack: true,
Resolver: c.bootstrapResolver,
Resolver: c.bootstrapResolver,
}
c.httpTransport = &http.Transport{
DialContext: dialer.DialContext,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
if c.conf.Other.Interface == "" {
return baseDialer.DialContext(ctx, network, addr)
}
if network == "tcp4" && localV4 != nil {
d := *baseDialer
d.LocalAddr = &net.TCPAddr{IP: localV4}
return d.DialContext(ctx, network, addr)
}
if network == "tcp6" && localV6 != nil {
d := *baseDialer
d.LocalAddr = &net.TCPAddr{IP: localV6}
return d.DialContext(ctx, network, addr)
}
// Manual Dual-Stack: Resolve host and try compatible families sequentially
host, port, _ := net.SplitHostPort(addr)
ips, err := c.bootstrapResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
var lastErr error
for _, ip := range ips {
d := *baseDialer
targetAddr := net.JoinHostPort(ip.String(), port)
if ip.IP.To4() != nil {
if localV4 == nil {
continue
}
d.LocalAddr = &net.TCPAddr{IP: localV4}
} else {
if localV6 == nil {
continue
}
d.LocalAddr = &net.TCPAddr{IP: localV6}
}
conn, err := d.DialContext(ctx, "tcp", targetAddr)
if err == nil {
return conn, nil
}
lastErr = err
}
if lastErr != nil {
return nil, lastErr
}
return nil, fmt.Errorf("connection to %s failed: no matching local/remote IP families on interface %s", addr, c.conf.Other.Interface)
},
ExpectContinueTimeout: 1 * time.Second,
IdleConnTimeout: 90 * time.Second,
MaxIdleConns: 100,
@@ -250,15 +364,18 @@ func (c *Client) newHTTPClient() error {
TLSHandshakeTimeout: time.Duration(c.conf.Other.Timeout) * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.conf.Other.TLSInsecureSkipVerify},
}
if c.conf.Other.NoIPv6 {
originalDial := c.httpTransport.DialContext
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)
return originalDial(ctx, network, address)
}
}
err := http2.ConfigureTransport(c.httpTransport)
err = http2.ConfigureTransport(c.httpTransport)
if err != nil {
return err
}
@@ -427,7 +544,7 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
// 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
// I think if status code is 5xx, upstream must have some problems
/*if req.response.StatusCode/100 == 5 {
c.selector.ReportUpstreamStatus(upstream, selector.Medium)
}*/
@@ -484,3 +601,38 @@ func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddre
}
return
}
// getInterfaceIPs returns the first valid IPv4 and IPv6 addresses found on the interface
func (c *Client) getInterfaceIPs() (v4, v6 net.IP, err error) {
if c.conf.Other.Interface == "" {
return nil, nil, nil
}
ifi, err := net.InterfaceByName(c.conf.Other.Interface)
if err != nil {
return nil, nil, err
}
addrs, err := ifi.Addrs()
if err != nil {
return nil, nil, err
}
for _, addr := range addrs {
ip, _, err := net.ParseCIDR(addr.String())
if err != nil {
continue
}
if ip4 := ip.To4(); ip4 != nil {
if v4 == nil {
v4 = ip4
}
} else {
if v6 == nil && !c.conf.Other.NoIPv6 {
v6 = ip
}
}
}
if v4 == nil && v6 == nil {
return nil, nil, fmt.Errorf("no valid IP addresses found on interface %s", c.conf.Other.Interface)
}
return v4, v6, nil
}

View File

@@ -50,6 +50,7 @@ type others struct {
Bootstrap []string `toml:"bootstrap"`
Passthrough []string `toml:"passthrough"`
Timeout uint `toml:"timeout"`
Interface string `toml:"interface"`
NoCookies bool `toml:"no_cookies"`
NoECS bool `toml:"no_ecs"`
NoIPv6 bool `toml:"no_ipv6"`

View File

@@ -81,7 +81,9 @@ passthrough = [
"captive.apple.com",
"connectivitycheck.gstatic.com",
"detectportal.firefox.com",
"globalreachtech.com",
"msftconnecttest.com",
"network-auth.com",
"nmcheck.gnome.org",
"pool.ntp.org",
@@ -95,6 +97,11 @@ passthrough = [
# Timeout for upstream request in seconds
timeout = 30
# Interface to bind to for outgoing connections.
# If empty, the system default route is used (usually eth0 or wlan0).
# Example: "eth1", "wlan0"
interface = ""
# Disable HTTP Cookies
#
# Cookies may be useful if your upstream resolver is protected by some

View File

@@ -27,16 +27,17 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/miekg/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"
)
func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool, upstream *selector.Upstream) *DNSRequest {
@@ -77,7 +78,7 @@ func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter
requestURL += fmt.Sprintf("&edns_client_subnet=%s/%d", ednsClientAddress.String(), ednsClientNetmask)
}
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
req, err := http.NewRequest(http.MethodGet, requestURL, http.NoBody)
if err != nil {
log.Println(err)
reply := jsondns.PrepareReply(r)
@@ -140,7 +141,7 @@ func (c *Client) parseResponseGoogle(ctx context.Context, w dns.ResponseWriter,
}
}
body, err := ioutil.ReadAll(req.response.Body)
body, err := io.ReadAll(req.response.Body)
if err != nil {
log.Println(err)
req.reply.Rcode = dns.RcodeServerFailure
@@ -163,6 +164,11 @@ func (c *Client) parseResponseGoogle(ctx context.Context, w dns.ResponseWriter,
fixEmptyNames(&respJSON)
fullReply := jsondns.Unmarshal(req.reply, &respJSON, req.udpSize, req.ednsClientNetmask)
if isTCP {
fullReply.Truncate(dns.MaxMsgSize)
} else {
fullReply.Truncate(int(req.udpSize))
}
buf, err := fullReply.Pack()
if err != nil {
log.Println(err)
@@ -170,15 +176,6 @@ func (c *Client) parseResponseGoogle(ctx context.Context, w dns.ResponseWriter,
w.WriteMsg(req.reply)
return
}
if !isTCP && len(buf) > int(req.udpSize) {
fullReply.Truncated = true
buf, err = fullReply.Pack()
if err != nil {
log.Println(err)
return
}
buf = buf[:req.udpSize]
}
w.Write(buf)
}

View File

@@ -28,16 +28,17 @@ import (
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"io"
"log"
"net"
"net/http"
"strings"
"time"
"github.com/miekg/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"
)
func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, isTCP bool, upstream *selector.Upstream) *DNSRequest {
@@ -104,7 +105,7 @@ func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter,
var req *http.Request
if len(requestURL) < 2048 {
req, err = http.NewRequest(http.MethodGet, requestURL, nil)
req, err = http.NewRequest(http.MethodGet, requestURL, http.NoBody)
if err != nil {
log.Println(err)
reply := jsondns.PrepareReply(r)
@@ -178,7 +179,7 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
}
}
body, err := ioutil.ReadAll(req.response.Body)
body, err := io.ReadAll(req.response.Body)
if err != nil {
log.Printf("read error from upstream %s: %v\n", req.currentUpstream, err)
req.reply.Rcode = dns.RcodeServerFailure
@@ -231,6 +232,11 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
_ = fixRecordTTL(rr, timeDelta)
}
if isTCP {
fullReply.Truncate(dns.MaxMsgSize)
} else {
fullReply.Truncate(int(req.udpSize))
}
buf, err := fullReply.Pack()
if err != nil {
log.Printf("packing error with upstream %s: %v\n", req.currentUpstream, err)
@@ -238,15 +244,6 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
w.WriteMsg(req.reply)
return
}
if !isTCP && len(buf) > int(req.udpSize) {
fullReply.Truncated = true
buf, err = fullReply.Pack()
if err != nil {
log.Printf("re-packing error with upstream %s: %v\n", req.currentUpstream, err)
return
}
buf = buf[:req.udpSize]
}
_, err = w.Write(buf)
if err != nil {
log.Printf("failed to write to client: %v\n", err)

View File

@@ -26,8 +26,6 @@ package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"runtime"
@@ -38,9 +36,9 @@ import (
func checkPIDFile(pidFile string) (bool, error) {
retry:
f, err := os.OpenFile(pidFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
f, err := os.OpenFile(pidFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
if os.IsExist(err) {
pidStr, err := ioutil.ReadFile(pidFile)
pidStr, err := os.ReadFile(pidFile)
if err != nil {
return false, err
}
@@ -64,7 +62,7 @@ retry:
return false, err
}
defer f.Close()
_, err = io.WriteString(f, strconv.FormatInt(int64(os.Getpid()), 10))
_, err = f.WriteString(strconv.FormatInt(int64(os.Getpid()), 10))
if err != nil {
return false, err
}

View File

@@ -80,7 +80,7 @@ func (ls *LVSWRRSelector) StartEvaluate() {
acceptType = "application/dns-message"
}
req, err := http.NewRequest(http.MethodGet, upstreamURL, nil)
req, err := http.NewRequest(http.MethodGet, upstreamURL, http.NoBody)
if err != nil {
/*log.Println("upstream:", upstreamURL, "type:", typeMap[upstream.Type], "check failed:", err)
continue*/

View File

@@ -73,7 +73,7 @@ func (ws *NginxWRRSelector) StartEvaluate() {
acceptType = "application/dns-message"
}
req, err := http.NewRequest(http.MethodGet, upstreamURL, nil)
req, err := http.NewRequest(http.MethodGet, upstreamURL, http.NoBody)
if err != nil {
/*log.Println("upstream:", upstreamURL, "type:", typeMap[upstream.Type], "check failed:", err)
continue*/
@@ -110,7 +110,7 @@ func (ws *NginxWRRSelector) StartEvaluate() {
}()
}
// nginx wrr like
// nginx wrr like.
func (ws *NginxWRRSelector) Get() *Upstream {
var (
total int32

View File

@@ -7,7 +7,7 @@ import (
)
func init() {
rand.Seed(time.Now().UnixNano())
rand.NewSource(time.Now().UnixNano())
}
type RandomSelector struct {

View File

@@ -3,12 +3,12 @@ package selector
type upstreamStatus int
const (
// when query upstream timeout, usually upstream is unavailable for a long time
// 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
// 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
// when query upstream ok, means upstream is available.
OK
)

View File

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

View File

@@ -31,21 +31,21 @@ import (
)
type config struct {
Listen []string `toml:"listen"`
TLSClientAuthCA string `toml:"tls_client_auth_ca"`
LocalAddr string `toml:"local_addr"`
Cert string `toml:"cert"`
Key string `toml:"key"`
Path string `toml:"path"`
DebugHTTPHeaders []string `toml:"debug_http_headers"`
Listen []string `toml:"listen"`
Upstream []string `toml:"upstream"`
Timeout uint `toml:"timeout"`
Tries uint `toml:"tries"`
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) {

View File

@@ -34,9 +34,10 @@ import (
"strings"
"time"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns"
"golang.org/x/net/idna"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
)
func (s *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest {
@@ -90,45 +91,14 @@ func (s *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter,
if ednsClientSubnet == "0/0" {
ednsClientSubnet = "0.0.0.0/0"
}
slash := strings.IndexByte(ednsClientSubnet, '/')
if slash < 0 {
ednsClientAddress = net.ParseIP(ednsClientSubnet)
if ednsClientAddress == nil {
return &DNSRequest{
errcode: 400,
errtext: fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet),
}
var err error
ednsClientFamily, ednsClientAddress, ednsClientNetmask, err = parseSubnet(ednsClientSubnet)
if err != nil {
return &DNSRequest{
errcode: 400,
errtext: err.Error(),
}
if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1
ednsClientAddress = ipv4
ednsClientNetmask = 24
} else {
ednsClientFamily = 2
ednsClientNetmask = 56
}
} else {
ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash])
if ednsClientAddress == nil {
return &DNSRequest{
errcode: 400,
errtext: fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet),
}
}
if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1
ednsClientAddress = ipv4
} else {
ednsClientFamily = 2
}
netmask, err := strconv.ParseUint(ednsClientSubnet[slash+1:], 10, 8)
if err != nil {
return &DNSRequest{
errcode: 400,
errtext: fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet),
}
}
ednsClientNetmask = uint8(netmask)
}
} else {
ednsClientAddress = s.findClientIP(r)
@@ -169,6 +139,45 @@ func (s *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter,
}
}
func parseSubnet(ednsClientSubnet string) (ednsClientFamily uint16, ednsClientAddress net.IP, ednsClientNetmask uint8, err error) {
slash := strings.IndexByte(ednsClientSubnet, '/')
if slash < 0 {
ednsClientAddress = net.ParseIP(ednsClientSubnet)
if ednsClientAddress == nil {
err = fmt.Errorf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet)
return
}
if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1
ednsClientAddress = ipv4
ednsClientNetmask = 24
} else {
ednsClientFamily = 2
ednsClientNetmask = 56
}
} else {
ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash])
if ednsClientAddress == nil {
err = fmt.Errorf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet)
return
}
if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1
ednsClientAddress = ipv4
} else {
ednsClientFamily = 2
}
netmask, err1 := strconv.ParseUint(ednsClientSubnet[slash+1:], 10, 8)
if err1 != nil {
err = fmt.Errorf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet)
return
}
ednsClientNetmask = uint8(netmask)
}
return
}
func (s *Server) generateResponseGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) {
respJSON := jsondns.Marshal(req.response)
respStr, err := json.Marshal(respJSON)

View File

@@ -28,7 +28,7 @@ import (
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"io"
"log"
"net"
"net/http"
@@ -36,8 +36,9 @@ import (
"strings"
"time"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
)
func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest {
@@ -50,7 +51,7 @@ func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r
}
}
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 = io.ReadAll(r.Body)
if err != nil {
return &DNSRequest{
errcode: 400,
@@ -61,7 +62,7 @@ func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r
if len(requestBinary) == 0 {
return &DNSRequest{
errcode: 400,
errtext: fmt.Sprintf("Invalid argument value: \"dns\""),
errtext: "Invalid argument value: \"dns\"",
}
}
@@ -182,8 +183,6 @@ func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter
w.Header().Set("Last-Modified", now)
w.Header().Set("Vary", "Accept")
_ = s.patchFirefoxContentType(w, r, req)
if respJSON.HaveTTL {
if req.isTailored {
w.Header().Set("Cache-Control", "private, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10))
@@ -203,7 +202,7 @@ func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter
}
}
// Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe
// Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe.
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 {
@@ -218,17 +217,3 @@ func (s *Server) patchDNSCryptProxyReqID(w http.ResponseWriter, r *http.Request,
}
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

@@ -26,8 +26,6 @@ package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"runtime"
@@ -36,9 +34,9 @@ import (
func checkPIDFile(pidFile string) (bool, error) {
retry:
f, err := os.OpenFile(pidFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
f, err := os.OpenFile(pidFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666)
if os.IsExist(err) {
pidStr, err := ioutil.ReadFile(pidFile)
pidStr, err := os.ReadFile(pidFile)
if err != nil {
return false, err
}
@@ -62,7 +60,7 @@ retry:
return false, err
}
defer f.Close()
_, err = io.WriteString(f, strconv.FormatInt(int64(os.Getpid()), 10))
_, err = f.WriteString(strconv.FormatInt(int64(os.Getpid()), 10))
if err != nil {
return false, err
}

119
doh-server/parse_test.go Normal file
View File

@@ -0,0 +1,119 @@
/*
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
import (
"testing"
"github.com/miekg/dns"
)
func TestParseCIDR(t *testing.T) {
t.Parallel()
for _, ednsClientSubnet := range []string{
"2001:db8::/0",
"2001:db8::/56",
"2001:db8::/129",
"2001:db8::",
"127.0.0.1/0",
"127.0.0.1/24",
"127.0.0.1/33",
"127.0.0.1",
"::ffff:7f00:1/0",
"::ffff:7f00:1/120",
"::ffff:7f00:1",
"127.0.0.1/0",
"127.0.0.1/24",
"127.0.0.1",
} {
_, ip, ipNet, err := parseSubnet(ednsClientSubnet)
if err != nil {
t.Errorf("ecs:%s ip:[%v] ipNet:[%v] err:[%v]", ednsClientSubnet, ip, ipNet, err)
}
}
}
func TestParseInvalidCIDR(t *testing.T) {
t.Parallel()
for _, ip := range []string{
"test",
"test/0",
"test/24",
"test/34",
"test/56",
"test/129",
} {
_, _, _, err := parseSubnet(ip)
if err == nil {
t.Errorf("expected error for %q", ip)
}
}
}
func TestEdns0SubnetParseCIDR(t *testing.T) {
t.Parallel()
// init dns Msg
msg := new(dns.Msg)
msg.Id = dns.Id()
msg.SetQuestion(dns.Fqdn("example.com"), 1)
// init edns0Subnet
edns0Subnet := new(dns.EDNS0_SUBNET)
edns0Subnet.Code = dns.EDNS0SUBNET
edns0Subnet.SourceScope = 0
// init opt
opt := new(dns.OPT)
opt.Hdr.Name = "."
opt.Hdr.Rrtype = dns.TypeOPT
opt.SetUDPSize(dns.DefaultMsgSize)
opt.Option = append(opt.Option, edns0Subnet)
msg.Extra = append(msg.Extra, opt)
for _, subnet := range []string{"::ffff:7f00:1/120", "127.0.0.1/24"} {
var err error
edns0Subnet.Family, edns0Subnet.Address, edns0Subnet.SourceNetmask, err = parseSubnet(subnet)
if err != nil {
t.Error(err)
continue
}
t.Log(msg.Pack())
}
// ------127.0.0.1/24-----
// [143 29 1 0 0 1 0 0 0 0 0 1 7 101 120 97 109 112 108 101 3 99 111 109 0 0 1 0 1 0
// opt start 0 41 16 0 0 0 0 0 0 11
// subnet start 0 8 0 7 0 1 24 0
// client subnet start 127 0 0]
// -----::ffff:7f00:1/120----
// [111 113 1 0 0 1 0 0 0 0 0 1 7 101 120 97 109 112 108 101 3 99 111 109 0 0 1 0 1 0
// opt start 0 41 16 0 0 0 0 0 0 23
// subnet start 0 8 0 19 0 2 120 0
// client subnet start 0 0 0 0 0 0 0 0 0 0 255 255 127 0 0]
}

View File

@@ -28,7 +28,6 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
@@ -38,8 +37,9 @@ import (
"time"
"github.com/gorilla/handlers"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
)
type Server struct {
@@ -53,11 +53,11 @@ type Server struct {
type DNSRequest struct {
request *dns.Msg
response *dns.Msg
transactionID uint16
currentUpstream string
isTailored bool
errcode int
errtext string
errcode int
transactionID uint16
isTailored bool
}
func NewServer(conf *config) (*Server, error) {
@@ -114,7 +114,7 @@ func (s *Server) Start() error {
var clientCAPool *x509.CertPool
if s.conf.TLSClientAuth {
if s.conf.TLSClientAuthCA != "" {
clientCA, err := ioutil.ReadFile(s.conf.TLSClientAuthCA)
clientCA, err := os.ReadFile(s.conf.TLSClientAuthCA)
if err != nil {
log.Fatalf("Reading certificate for client authentication has failed: %v", err)
}
@@ -285,7 +285,7 @@ func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
func (s *Server) findClientIP(r *http.Request) net.IP {
noEcs := r.URL.Query().Get("no_ecs")
if strings.ToLower(noEcs) == "true" {
if strings.EqualFold(noEcs, "true") {
return nil
}
@@ -319,7 +319,7 @@ func (s *Server) findClientIP(r *http.Request) net.IP {
return nil
}
// Workaround a bug causing Unbound to refuse returning anything about the root
// 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 == "." {
@@ -329,7 +329,7 @@ func (s *Server) patchRootRD(req *DNSRequest) *DNSRequest {
return req
}
// Return the position index for the question of qtype from a DNS msg, otherwise return -1
// 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 {
@@ -364,7 +364,7 @@ func (s *Server) doDNSQuery(ctx context.Context, req *DNSRequest) (err error) {
req.response, _, err = s.tcpClient.ExchangeContext(ctx, req.request, upstream)
}
// Retry with TCP if this was an IXFR request and we only received an SOA
// 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) {

View File

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

24
go.mod
View File

@@ -1,20 +1,20 @@
module github.com/m13253/dns-over-https/v2
go 1.18
go 1.24.0
require (
github.com/BurntSushi/toml v1.1.0
github.com/gorilla/handlers v1.5.1
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9
github.com/miekg/dns v1.1.49
golang.org/x/net v0.0.0-20220526153639-5463443f8c37
github.com/BurntSushi/toml v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc
github.com/miekg/dns v1.1.70
golang.org/x/net v0.49.0
)
require (
github.com/felixge/httpsnoop v1.0.1 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // 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
github.com/felixge/httpsnoop v1.0.4 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.41.0 // indirect
)

73
go.sum
View File

@@ -1,51 +1,30 @@
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.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.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8=
github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc h1:RhT2pjLo3EVRmldbEcBdeRA7CGPWsNEJC+Y/N1aXQbg=
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc/go.mod h1:BaIJzjD2ZnHmx2acPF6XfGLPzNCMiBbMRqJr+8/8uRI=
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
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-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -119,7 +119,6 @@ func init() {
IP: net.IP{0xfe, 0x80, 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 {

View File

@@ -4,15 +4,18 @@ Documentation=https://github.com/m13253/dns-over-https
After=network.target
Before=nss-lookup.target
Wants=nss-lookup.target
StartLimitIntervalSec=0
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
ExecStart=/usr/local/bin/doh-client -conf /etc/dns-over-https/doh-client.conf
LimitNOFILE=1048576
Restart=always
RestartSec=3
RestartSec=1s
RestartMaxDelaySec=76s
RestartSteps=9
Type=simple
User=nobody
DynamicUser=yes
[Install]
WantedBy=multi-user.target

View File

@@ -2,15 +2,18 @@
Description=DNS-over-HTTPS Server
Documentation=https://github.com/m13253/dns-over-https
After=network.target
StartLimitIntervalSec=0
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
ExecStart=/usr/local/bin/doh-server -conf /etc/dns-over-https/doh-server.conf
LimitNOFILE=1048576
Restart=always
RestartSec=3
RestartSec=1s
RestartMaxDelaySec=76s
RestartSteps=9
Type=simple
User=nobody
DynamicUser=yes
[Install]
WantedBy=multi-user.target