Compare commits

..

2 Commits

Author SHA1 Message Date
Star Brilliant
fdf49c9065 Merge pull request #148 from vinnyperella/patch-1
Update go.yml to use ubuntu-latest for runs-on
2023-06-11 23:37:45 +00:00
Vinny
9bc797e6a4 Update go.yml
Updated workflow to use ubuntu-latest as ubuntu-18.04 has been deprecated.
2023-06-11 21:19:39 +00:00
29 changed files with 218 additions and 584 deletions

View File

@@ -1,36 +0,0 @@
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

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

View File

@@ -1,36 +0,0 @@
---
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,21 +4,6 @@ This Changelog records major changes between versions.
Not all changes are recorded. Please check git log for details. Not all changes are recorded. Please check git log for details.
## Version 2.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 ## Version 2.3.1
- No new features in this release - No new features in this release

View File

@@ -3,9 +3,9 @@
PREFIX = /usr/local PREFIX = /usr/local
ifeq ($(GOROOT),) ifeq ($(GOROOT),)
GOBUILD = go build -ldflags "-s -w" GOBUILD = go build
else else
GOBUILD = $(GOROOT)/bin/go build -ldflags "-s -w" GOBUILD = $(GOROOT)/bin/go build
endif endif
ifeq ($(shell uname),Darwin) 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 ## Installing
### From Source ### From Source
- Install [Go](https://golang.org), at least version 1.20. The newer, the better. - 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. > 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`:
```bash ```bash
@@ -58,7 +58,7 @@ sudo make uninstall
```bash ```bash
docker run -d --name doh-server \ docker run -d --name doh-server \
-p 8053:8053 \ -p 8053:8053 \
-e UPSTREAM_DNS_SERVER="udp:208.67.222.222:53,udp:208.67.220.220:53" \ -e UPSTREAM_DNS_SERVER="udp:8.8.8.8:53" \
-e DOH_HTTP_PREFIX="/dns-query" \ -e DOH_HTTP_PREFIX="/dns-query" \
-e DOH_SERVER_LISTEN=":8053" \ -e DOH_SERVER_LISTEN=":8053" \
-e DOH_SERVER_TIMEOUT="10" \ -e DOH_SERVER_TIMEOUT="10" \
@@ -66,16 +66,6 @@ docker run -d --name doh-server \
-e DOH_SERVER_VERBOSE="false" \ -e DOH_SERVER_VERBOSE="false" \
satishweb/doh-server 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 ## Server Configuration
@@ -94,7 +84,7 @@ The following is a typical DNS-over-HTTPS architecture:
| doh-client +--+ Content Delivery Network +--+ (Apache, Nginx, Caddy) | | doh-client +--+ Content Delivery Network +--+ (Apache, Nginx, Caddy) |
+--------------+ +--------------------------+ +------------------------+ +--------------+ +--------------------------+ +------------------------+
Although DNS-over-HTTPS can work alone, an 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, HTTP/2 with at least TLS v1.3 is recommended. OCSP stapling must be enabled,
@@ -137,7 +127,7 @@ server {
server_tokens off; server_tokens off;
ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.3 requires nginx >= 1.20.0 ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.3 requires nginx >= 1.13.0
ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -dsaparam -out /etc/nginx/dhparam.pem 4096 ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -dsaparam -out /etc/nginx/dhparam.pem 4096
ssl_ciphers EECDH+AESGCM:EDH+AESGCM; ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
@@ -279,28 +269,10 @@ services:
> IPV6 Support for Docker Compose based configuration TBA > 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 ## DNSSEC
DNS-over-HTTPS is compatible with DNSSEC, and requests DNSSEC signatures by DNS-over-HTTPS is compatible with DNSSEC, and requests DNSSEC signatures by
default. However, signature validation is not built-in. It is highly recommended default. However signature validation is not built-in. It is highly recommended
that you install `unbound` or `bind` and pass results for them to validate DNS that you install `unbound` or `bind` and pass results for them to validate DNS
records. An instance of [Pi Hole](https://pi-hole.net) could also be used to validate DNS signatures as well as provide other capabilities. records. An instance of [Pi Hole](https://pi-hole.net) could also be used to validate DNS signatures as well as provide other capabilities.
@@ -343,10 +315,6 @@ Currently supported features are:
- [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, /56 for IPv6 by default) - [X] EDNS0-Client-Subnet (/24 for IPv4, /56 for IPv6 by default)
## Known issues
* it does not work well with [dnscrypt-proxy](https://github.com/DNSCrypt/dnscrypt-proxy), you might want to use either (or fix the compatibility bugs by submitting PRs)
## The name of the project ## 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. 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,40 +38,39 @@ import (
"sync" "sync"
"time" "time"
"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/config"
"github.com/m13253/dns-over-https/v2/doh-client/selector" "github.com/m13253/dns-over-https/v2/doh-client/selector"
jsondns "github.com/m13253/dns-over-https/v2/json-dns" jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns"
"golang.org/x/net/http2"
"golang.org/x/net/idna"
) )
type Client struct { type Client struct {
httpClientLastCreate time.Time
cookieJar http.CookieJar
selector selector.Selector
httpClientMux *sync.RWMutex
tcpClient *dns.Client
bootstrapResolver *net.Resolver
udpClient *dns.Client
conf *config.Config conf *config.Config
httpTransport *http.Transport bootstrap []string
httpClient *http.Client passthrough []string
udpClient *dns.Client
tcpClient *dns.Client
udpServers []*dns.Server udpServers []*dns.Server
tcpServers []*dns.Server tcpServers []*dns.Server
passthrough []string bootstrapResolver *net.Resolver
bootstrap []string cookieJar http.CookieJar
httpClientMux *sync.RWMutex
httpTransport *http.Transport
httpClient *http.Client
httpClientLastCreate time.Time
selector selector.Selector
} }
type DNSRequest struct { type DNSRequest struct {
err error
response *http.Response response *http.Response
reply *dns.Msg reply *dns.Msg
currentUpstream string
ednsClientAddress net.IP
udpSize uint16 udpSize uint16
ednsClientAddress net.IP
ednsClientNetmask uint8 ednsClientNetmask uint8
currentUpstream string
err error
} }
func NewClient(conf *config.Config) (c *Client, err error) { func NewClient(conf *config.Config) (c *Client, err error) {
@@ -90,29 +89,6 @@ func NewClient(conf *config.Config) (c *Client, err error) {
Net: "tcp", Net: "tcp",
Timeout: time.Duration(conf.Other.Timeout) * time.Second, 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 { for _, addr := range conf.Listen {
c.udpServers = append(c.udpServers, &dns.Server{ c.udpServers = append(c.udpServers, &dns.Server{
Addr: addr, Addr: addr,
@@ -143,38 +119,6 @@ func NewClient(conf *config.Config) (c *Client, err error) {
PreferGo: true, PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) { Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
var d net.Dialer 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) numServers := len(c.bootstrap)
bootstrap := c.bootstrap[rand.Intn(numServers)] bootstrap := c.bootstrap[rand.Intn(numServers)]
conn, err := d.DialContext(ctx, network, bootstrap) conn, err := d.DialContext(ctx, network, bootstrap)
@@ -290,72 +234,14 @@ func (c *Client) newHTTPClient() error {
if c.httpTransport != nil { if c.httpTransport != nil {
c.httpTransport.CloseIdleConnections() 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, Timeout: time.Duration(c.conf.Other.Timeout) * time.Second,
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
Resolver: c.bootstrapResolver, // DualStack: true,
Resolver: c.bootstrapResolver,
} }
c.httpTransport = &http.Transport{ c.httpTransport = &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: dialer.DialContext,
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, ExpectContinueTimeout: 1 * time.Second,
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
MaxIdleConns: 100, MaxIdleConns: 100,
@@ -364,18 +250,15 @@ func (c *Client) newHTTPClient() error {
TLSHandshakeTimeout: time.Duration(c.conf.Other.Timeout) * time.Second, TLSHandshakeTimeout: time.Duration(c.conf.Other.Timeout) * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.conf.Other.TLSInsecureSkipVerify}, TLSClientConfig: &tls.Config{InsecureSkipVerify: c.conf.Other.TLSInsecureSkipVerify},
} }
if c.conf.Other.NoIPv6 { if c.conf.Other.NoIPv6 {
originalDial := c.httpTransport.DialContext
c.httpTransport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { c.httpTransport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
if strings.HasPrefix(network, "tcp") { if strings.HasPrefix(network, "tcp") {
network = "tcp4" network = "tcp4"
} }
return originalDial(ctx, network, address) 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
} }
@@ -544,7 +427,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 // 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 // returns code will be 200 / 400 / 413 / 415 / 504, some server will return 503, so
// I think if status code is 5xx, upstream must have some problems // I think if status code is 5xx, upstream must has some problems
/*if req.response.StatusCode/100 == 5 { /*if req.response.StatusCode/100 == 5 {
c.selector.ReportUpstreamStatus(upstream, selector.Medium) c.selector.ReportUpstreamStatus(upstream, selector.Medium)
}*/ }*/
@@ -601,38 +484,3 @@ func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddre
} }
return 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,7 +50,6 @@ type others struct {
Bootstrap []string `toml:"bootstrap"` Bootstrap []string `toml:"bootstrap"`
Passthrough []string `toml:"passthrough"` Passthrough []string `toml:"passthrough"`
Timeout uint `toml:"timeout"` Timeout uint `toml:"timeout"`
Interface string `toml:"interface"`
NoCookies bool `toml:"no_cookies"` NoCookies bool `toml:"no_cookies"`
NoECS bool `toml:"no_ecs"` NoECS bool `toml:"no_ecs"`
NoIPv6 bool `toml:"no_ipv6"` NoIPv6 bool `toml:"no_ipv6"`

View File

@@ -81,9 +81,7 @@ passthrough = [
"captive.apple.com", "captive.apple.com",
"connectivitycheck.gstatic.com", "connectivitycheck.gstatic.com",
"detectportal.firefox.com", "detectportal.firefox.com",
"globalreachtech.com",
"msftconnecttest.com", "msftconnecttest.com",
"network-auth.com",
"nmcheck.gnome.org", "nmcheck.gnome.org",
"pool.ntp.org", "pool.ntp.org",
@@ -97,11 +95,6 @@ passthrough = [
# Timeout for upstream request in seconds # Timeout for upstream request in seconds
timeout = 30 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 # Disable HTTP Cookies
# #
# Cookies may be useful if your upstream resolver is protected by some # Cookies may be useful if your upstream resolver is protected by some

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,12 @@ package selector
type upstreamStatus int type upstreamStatus int
const ( 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 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 Error
// when query upstream ok, means upstream is available. // when query upstream ok, means upstream is available
OK OK
) )

View File

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

View File

@@ -31,21 +31,21 @@ import (
) )
type config struct { type config struct {
TLSClientAuthCA string `toml:"tls_client_auth_ca"` Listen []string `toml:"listen"`
LocalAddr string `toml:"local_addr"` LocalAddr string `toml:"local_addr"`
Cert string `toml:"cert"` Cert string `toml:"cert"`
Key string `toml:"key"` Key string `toml:"key"`
Path string `toml:"path"` Path string `toml:"path"`
DebugHTTPHeaders []string `toml:"debug_http_headers"`
Listen []string `toml:"listen"`
Upstream []string `toml:"upstream"` Upstream []string `toml:"upstream"`
Timeout uint `toml:"timeout"` Timeout uint `toml:"timeout"`
Tries uint `toml:"tries"` Tries uint `toml:"tries"`
Verbose bool `toml:"verbose"` Verbose bool `toml:"verbose"`
DebugHTTPHeaders []string `toml:"debug_http_headers"`
LogGuessedIP bool `toml:"log_guessed_client_ip"` LogGuessedIP bool `toml:"log_guessed_client_ip"`
ECSAllowNonGlobalIP bool `toml:"ecs_allow_non_global_ip"` ECSAllowNonGlobalIP bool `toml:"ecs_allow_non_global_ip"`
ECSUsePreciseIP bool `toml:"ecs_use_precise_ip"` ECSUsePreciseIP bool `toml:"ecs_use_precise_ip"`
TLSClientAuth bool `toml:"tls_client_auth"` TLSClientAuth bool `toml:"tls_client_auth"`
TLSClientAuthCA string `toml:"tls_client_auth_ca"`
} }
func loadConfig(path string) (*config, error) { func loadConfig(path string) (*config, error) {

View File

@@ -34,10 +34,9 @@ import (
"strings" "strings"
"time" "time"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/idna" "golang.org/x/net/idna"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
) )
func (s *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest { func (s *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest {
@@ -91,14 +90,45 @@ func (s *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter,
if ednsClientSubnet == "0/0" { if ednsClientSubnet == "0/0" {
ednsClientSubnet = "0.0.0.0/0" ednsClientSubnet = "0.0.0.0/0"
} }
slash := strings.IndexByte(ednsClientSubnet, '/')
var err error if slash < 0 {
ednsClientFamily, ednsClientAddress, ednsClientNetmask, err = parseSubnet(ednsClientSubnet) ednsClientAddress = net.ParseIP(ednsClientSubnet)
if err != nil { if ednsClientAddress == nil {
return &DNSRequest{ return &DNSRequest{
errcode: 400, errcode: 400,
errtext: err.Error(), errtext: fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet),
}
} }
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 { } else {
ednsClientAddress = s.findClientIP(r) ednsClientAddress = s.findClientIP(r)
@@ -139,45 +169,6 @@ 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) { 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)

View File

@@ -28,7 +28,7 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -36,9 +36,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/miekg/dns"
jsondns "github.com/m13253/dns-over-https/v2/json-dns" jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns"
) )
func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest { func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest {
@@ -51,7 +50,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") { if len(requestBinary) == 0 && (r.Header.Get("Content-Type") == "application/dns-message" || r.Header.Get("Content-Type") == "application/dns-udpwireformat") {
requestBinary, err = io.ReadAll(r.Body) requestBinary, err = ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
return &DNSRequest{ return &DNSRequest{
errcode: 400, errcode: 400,
@@ -62,7 +61,7 @@ func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r
if len(requestBinary) == 0 { if len(requestBinary) == 0 {
return &DNSRequest{ return &DNSRequest{
errcode: 400, errcode: 400,
errtext: "Invalid argument value: \"dns\"", errtext: fmt.Sprintf("Invalid argument value: \"dns\""),
} }
} }
@@ -183,6 +182,8 @@ func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter
w.Header().Set("Last-Modified", now) w.Header().Set("Last-Modified", now)
w.Header().Set("Vary", "Accept") 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.FormatUint(uint64(respJSON.LeastTTL), 10)) w.Header().Set("Cache-Control", "private, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10))
@@ -202,7 +203,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 { 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 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 { if s.conf.Verbose {
@@ -217,3 +218,17 @@ func (s *Server) patchDNSCryptProxyReqID(w http.ResponseWriter, r *http.Request,
} }
return false 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,6 +26,8 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"io"
"io/ioutil"
"log" "log"
"os" "os"
"runtime" "runtime"
@@ -34,9 +36,9 @@ import (
func checkPIDFile(pidFile string) (bool, error) { func checkPIDFile(pidFile string) (bool, error) {
retry: retry:
f, err := os.OpenFile(pidFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666) f, err := os.OpenFile(pidFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if os.IsExist(err) { if os.IsExist(err) {
pidStr, err := os.ReadFile(pidFile) pidStr, err := ioutil.ReadFile(pidFile)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -60,7 +62,7 @@ retry:
return false, err return false, err
} }
defer f.Close() defer f.Close()
_, err = f.WriteString(strconv.FormatInt(int64(os.Getpid()), 10)) _, err = io.WriteString(f, strconv.FormatInt(int64(os.Getpid()), 10))
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@@ -1,119 +0,0 @@
/*
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,6 +28,7 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"math/rand" "math/rand"
"net" "net"
@@ -37,9 +38,8 @@ import (
"time" "time"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/miekg/dns"
jsondns "github.com/m13253/dns-over-https/v2/json-dns" jsondns "github.com/m13253/dns-over-https/v2/json-dns"
"github.com/miekg/dns"
) )
type Server struct { type Server struct {
@@ -53,11 +53,11 @@ type Server struct {
type DNSRequest struct { type DNSRequest struct {
request *dns.Msg request *dns.Msg
response *dns.Msg response *dns.Msg
currentUpstream string
errtext string
errcode int
transactionID uint16 transactionID uint16
currentUpstream string
isTailored bool isTailored bool
errcode int
errtext string
} }
func NewServer(conf *config) (*Server, error) { func NewServer(conf *config) (*Server, error) {
@@ -114,7 +114,7 @@ func (s *Server) Start() error {
var clientCAPool *x509.CertPool var clientCAPool *x509.CertPool
if s.conf.TLSClientAuth { if s.conf.TLSClientAuth {
if s.conf.TLSClientAuthCA != "" { if s.conf.TLSClientAuthCA != "" {
clientCA, err := os.ReadFile(s.conf.TLSClientAuthCA) clientCA, err := ioutil.ReadFile(s.conf.TLSClientAuthCA)
if err != nil { if err != nil {
log.Fatalf("Reading certificate for client authentication has failed: %v", err) 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 { func (s *Server) findClientIP(r *http.Request) net.IP {
noEcs := r.URL.Query().Get("no_ecs") noEcs := r.URL.Query().Get("no_ecs")
if strings.EqualFold(noEcs, "true") { if strings.ToLower(noEcs) == "true" {
return nil return nil
} }
@@ -319,7 +319,7 @@ func (s *Server) findClientIP(r *http.Request) net.IP {
return nil 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 { func (s *Server) patchRootRD(req *DNSRequest) *DNSRequest {
for _, question := range req.request.Question { for _, question := range req.request.Question {
if question.Name == "." { if question.Name == "." {
@@ -329,7 +329,7 @@ func (s *Server) patchRootRD(req *DNSRequest) *DNSRequest {
return req return req
} }
// Return the position index for the question of qtype from a DNS msg, otherwise return -1. // 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 { func (s *Server) indexQuestionType(msg *dns.Msg, qtype uint16) int {
for i, question := range msg.Question { for i, question := range msg.Question {
if question.Qtype == qtype { 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) 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) && if (s.indexQuestionType(req.request, dns.TypeIXFR) > -1) &&
(len(req.response.Answer) == 1) && (len(req.response.Answer) == 1) &&
(req.response.Answer[0].Header().Rrtype == dns.TypeSOA) { (req.response.Answer[0].Header().Rrtype == dns.TypeSOA) {

View File

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

24
go.mod
View File

@@ -1,20 +1,20 @@
module github.com/m13253/dns-over-https/v2 module github.com/m13253/dns-over-https/v2
go 1.24.0 go 1.19
require ( require (
github.com/BurntSushi/toml v1.6.0 github.com/BurntSushi/toml v1.2.0
github.com/gorilla/handlers v1.5.2 github.com/gorilla/handlers v1.5.1
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9
github.com/miekg/dns v1.1.70 github.com/miekg/dns v1.1.50
golang.org/x/net v0.49.0 golang.org/x/net v0.0.0-20220907135653-1e95f45603a7
) )
require ( require (
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect
golang.org/x/mod v0.32.0 // indirect golang.org/x/mod v0.4.2 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
) )

73
go.sum
View File

@@ -1,30 +1,51 @@
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 h1:w66aaP3c6SIQ0pi3QH1Tb4AMO3aWoEPxd1CNvLphbkA=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9/go.mod h1:BaIJzjD2ZnHmx2acPF6XfGLPzNCMiBbMRqJr+8/8uRI=
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc h1:RhT2pjLo3EVRmldbEcBdeRA7CGPWsNEJC+Y/N1aXQbg= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc/go.mod h1:BaIJzjD2ZnHmx2acPF6XfGLPzNCMiBbMRqJr+8/8uRI= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 h1:1WGATo9HAhkWMbfyuVU0tEFP88OIkUvwaHFveQPvzCQ=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/net v0.0.0-20220907135653-1e95f45603a7/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/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= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -119,6 +119,7 @@ func init() {
IP: net.IP{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, IP: net.IP{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
Mask: net.IPMask{0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, Mask: net.IPMask{0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}, struct{}{}) }, struct{}{})
} }
func IsGlobalIP(ip net.IP) bool { func IsGlobalIP(ip net.IP) bool {

View File

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

View File

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