Compare commits

..

5 Commits

Author SHA1 Message Date
James Swineson
9985c4b279 fix relative path 2019-03-26 15:57:01 +08:00
James Swineson
97a27e60e1 update comments 2019-03-26 15:46:57 +08:00
James Swineson
ca14db8929 move package.sh to contrib 2019-03-26 12:30:57 +08:00
James Swineson
a95e3e5f39 add networkmanager dispatcher script 2019-03-26 12:28:14 +08:00
James Swineson
5010a16458 initial dirty auto packaging 2019-03-26 12:16:07 +08:00
48 changed files with 587 additions and 1857 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

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

5
.gitignore vendored
View File

@@ -3,9 +3,7 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
darwin-wrapper/doh-logger build/
doh-client/doh-client
doh-server/doh-server
# Test binary, build with `go test -c` # Test binary, build with `go test -c`
*.test *.test
@@ -17,4 +15,3 @@ doh-server/doh-server
.glide/ .glide/
.idea/ .idea/
vendor/

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,86 +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
- No new features in this release
- Bumped versions of Go toolchain and third-party dependencies, requested by #128
## Version 2.3.0
- The repository now conforms to the Go semvar standard (Fixed #115, thanks to @leiless)
## Version 2.2.5
- Add client certificate authentication
- Fixing documentation related to Docker
## Version 2.2.4
- Add options to configure ECS netmask length
- Add an option to disable TLS verification (Note: dangerous)
## Version 2.2.3
- Use the library ipTree to determine whether an IP is global routable, improving the performance
- Google's 8.8.8.8 resolver is now marked as "Good ECS" in the example configuration file
## Version 2.2.2
- Allow client to opt-out EDNS0 Client Support
- [JSON-DoH] Honor DNSSEC OK flag for incoming DNS requests
- [JSON-DoH] Add support for non-standard response formats
- `X-Real-IP` is now used in logging if set by frontend load balancer
- Fix documentation
## Version 2.2.1
- Fix messy log
## Version 2.2.0
- Breaking change: The configuration format of doh-server is changed
- Add support for type prefix for upstream addresses of doh-server
- Add support for DNS-over-TLS upstream addresses of doh-server
- Remove `tcp_only` configuration option in doh-server
- Add `no_user_agent` configuration option in doh-server
- Add an RPM package script with SELinux policy
- Fix Opcode never assigned in `jsonDNS.PrepareReply`
- Improve error logging / checking
- Updated Readme
## Version 2.1.2
- Update address for google's resolver
- Fix a typo
## Version 2.1.1
- Add a set of Dockerfile contributed by the community
- Include DNS.SB's resolver in example configuration
## Version 2.1.0
- Add `local_addr` configuration for doh-server (#39)
- Fix a problem when compiling on macOS 10.14.4 or newer
- Add Quad9 DoH server to the example `doh-client.conf`
- Use TCP when appropriate for the given query type/response (AXFR/IXFR)
## Version 2.0.1 ## Version 2.0.1
- Fix a crash with the random load balancing algorithm. - Fix a crash with the random load balancing algorithm.

View File

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

View File

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

View File

@@ -1,11 +1,15 @@
.PHONY: all clean install uninstall .PHONY: all clean install uninstall deps
PREFIX = /usr/local PREFIX = /usr/local
ifeq ($(GOROOT),) ifeq ($(GOROOT),)
GOBUILD = go build -ldflags "-s -w" GOBUILD = go build
GOGET = go get -d -v
GOGET_UPDATE = go get -d -u -v
else else
GOBUILD = $(GOROOT)/bin/go build -ldflags "-s -w" GOBUILD = $(GOROOT)/bin/go build
GOGET = $(GOROOT)/bin/go get -d -v
GOGET_UPDATE = $(GOROOT)/bin/go get -d -u -v
endif endif
ifeq ($(shell uname),Darwin) ifeq ($(shell uname),Darwin)
@@ -53,8 +57,13 @@ uninstall:
$(MAKE) -C launchd uninstall "DESTDIR=$(DESTDIR)"; \ $(MAKE) -C launchd uninstall "DESTDIR=$(DESTDIR)"; \
fi fi
doh-client/doh-client: doh-client/client.go doh-client/config/config.go doh-client/google.go doh-client/ietf.go doh-client/main.go doh-client/version.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go deps:
@# I am not sure if it is the correct way to keep the common library updated
$(GOGET_UPDATE) github.com/m13253/dns-over-https/json-dns
$(GOGET) ./doh-client ./doh-server
doh-client/doh-client: deps doh-client/client.go doh-client/config/config.go doh-client/google.go doh-client/ietf.go doh-client/main.go doh-client/version.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go
cd doh-client && $(GOBUILD) cd doh-client && $(GOBUILD)
doh-server/doh-server: doh-server/config.go doh-server/google.go doh-server/ietf.go doh-server/main.go doh-server/server.go doh-server/version.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go doh-server/doh-server: deps doh-server/config.go doh-server/google.go doh-server/ietf.go doh-server/main.go doh-server/server.go doh-server/version.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go
cd doh-server && $(GOBUILD) cd doh-server && $(GOBUILD)

304
Readme.md
View File

@@ -4,78 +4,57 @@ DNS-over-HTTPS
Client and server software to query DNS over HTTPS, using [Google DNS-over-HTTPS protocol](https://developers.google.com/speed/public-dns/docs/dns-over-https) Client and server software to query DNS over HTTPS, using [Google DNS-over-HTTPS protocol](https://developers.google.com/speed/public-dns/docs/dns-over-https)
and [IETF DNS-over-HTTPS (RFC 8484)](https://www.rfc-editor.org/rfc/rfc8484.txt). and [IETF DNS-over-HTTPS (RFC 8484)](https://www.rfc-editor.org/rfc/rfc8484.txt).
## Guides ## Guide
- [Tutorial: Setup your own DNS-over-HTTPS (DoH) server](https://www.aaflalo.me/2018/10/tutorial-setup-dns-over-https-server/). (Thanks to Antoine Aflalo) [Tutorial to setup your own DNS-over-HTTPS (DoH) server](https://www.aaflalo.me/2018/10/tutorial-setup-dns-over-https-server/). (Thanks to Antoine Aflalo)
- [Tutorial: Setup your own Docker based DNS-over-HTTPS (DoH) server](https://github.com/satishweb/docker-doh/blob/master/README.md). (Thanks to Satish Gaikwad)
## Installing ## Installing
### From Source
- 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`: Install [Go](https://golang.org), at least version 1.10.
```bash
mkdir ~/gopath (Note for Debian/Ubuntu users: You need to set `$GOROOT` if you could not get your new version of Go selected by the Makefile.)
export GOPATH=~/gopath
``` First create an empty directory, used for `$GOPATH`:
- To build the program, type:
```bash mkdir ~/gopath
make export GOPATH=~/gopath
```
- To install DNS-over-HTTPS as Systemd services, type: To build the program, type:
```bash
sudo make install make
```
- By default, [Google DNS over HTTPS](https://dns.google.com) is used. It should To install DNS-over-HTTPS as Systemd services, type:
sudo make install
By default, [Google DNS over HTTPS](https://dns.google.com) is used. It should
work for most users (except for People's Republic of China). If you need to work for most users (except for People's Republic of China). If you need to
modify the default settings, type: modify the default settings, type:
```bash
sudoedit /etc/dns-over-https/doh-client.conf
```
- To automatically start DNS-over-HTTPS client as a system service, type:
```bash
sudo systemctl start doh-client.service
sudo systemctl enable doh-client.service
```
- Then, modify your DNS settings (usually with NetworkManager) to 127.0.0.1.
- To test your configuration, type: sudoedit /etc/dns-over-https/doh-client.conf
```bash
dig www.google.com
Output:
;; SERVER: 127.0.0.1#53(127.0.0.1)
```
#### Uninstall
- To uninstall, type: To automatically start DNS-over-HTTPS client as a system service, type:
```bash
sudo make uninstall
```
> Note: The configuration files are kept at `/etc/dns-over-https`. Remove them manually if you want.
### Using docker image sudo systemctl start doh-client.service
```bash sudo systemctl enable doh-client.service
docker run -d --name doh-server \
-p 8053:8053 \
-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" \
-e DOH_SERVER_TRIES="3" \
-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: Then, modify your DNS settings (usually with NetworkManager) to 127.0.0.1.
- `m13253/dns-over-https-server:latest` To test your configuration, type:
- `m13253/dns-over-https-client:latest`
## Logging dig www.google.com
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). If it is OK, you will wee:
;; SERVER: 127.0.0.1#53(127.0.0.1)
### Uninstalling
To uninstall, type:
sudo make uninstall
The configuration files are kept at `/etc/dns-over-https`. Remove them manually if you want.
## Server Configuration ## Server Configuration
@@ -94,215 +73,18 @@ 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,
otherwise DNS recursion may happen. otherwise DNS recursion may happen.
### Configuration file
The main configuration file is `doh-client.conf`.
**Server selectors.** If several upstream servers are set, one is selected according to `upstream_selector` for each request. With `upstream_selector = "random"`, a random upstream server will be chosen for each request.
```toml
# available selector: random (default) or weighted_round_robin or lvs_weighted_round_robin
upstream_selector = "random"
```
### Example configuration: Apache
```bash
SSLProtocol TLSv1.2
SSLHonorCipherOrder On
SSLCipherSuite ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+3DES:!aNULL:!MD5:!DSS:!eNULL:!EXP:!LOW:!MD5
SSLUseStapling on
SSLStaplingCache shmcb:/var/lib/apache2/stapling_cache(512000)
<VirtualHost *:443>
ServerName MY_SERVER_NAME
Protocols h2 http/1.1
ProxyPass /dns-query http://[::1]:8053/dns-query
ProxyPassReverse /dns-query http://[::1]:8053/dns-query
</VirtualHost>
```
(Credit: [Joan Moreau](https://github.com/m13253/dns-over-https/issues/51#issuecomment-526820884))
### Example configuration: Nginx
```bash
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name MY_SERVER_NAME;
server_tokens off;
ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.3 requires nginx >= 1.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;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
ssl_early_data off; # 0-RTT, enable if desired - Requires nginx >= 1.15.4
resolver 1.1.1.1 valid=300s; # Replace with your local resolver
resolver_timeout 5s;
# HTTP Security Headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=63072000";
ssl_certificate /path/to/your/server/certificates/fullchain.pem;
ssl_certificate_key /path/to/your/server/certificates/privkey.pem;
location /dns-query {
proxy_pass http://localhost:8053/dns-query;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
(Credit: [Cipherli.st](https://cipherli.st/))
### Example configuration: Caddy (v2)
```bash
my.server.name {
reverse_proxy * localhost:8053
tls your@email.address
try_files {path} {path}/index.php /index.php?{query}
}
```
### Example configuration: Docker Compose + Traefik + Unbound (Raspberry Pi/Linux/Mac) [linux/amd64,linux/arm64,linux/arm/v7]
```yaml
version: '2.2'
networks:
default:
services:
proxy:
# The official v2 Traefik docker image
image: traefik:v2.3
hostname: proxy
networks:
- default
environment:
TRAEFIK_ACCESSLOG: "true"
TRAEFIK_API: "true"
TRAEFIK_PROVIDERS_DOCKER: "true"
TRAEFIK_API_INSECURE: "true"
TRAEFIK_PROVIDERS_DOCKER_NETWORK: "${STACK}_default"
# DNS provider specific environment variables for DNS Challenge using route53 (AWS)
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
AWS_REGION: ${AWS_REGION}
AWS_HOSTED_ZONE_ID: ${AWS_HOSTED_ZONE_ID}
ports:
# The HTTP port
- "80:80"
# The HTTPS port
- "443:443"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
command:
#- "--log.level=DEBUG"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
# Providers list:
# https://docs.traefik.io/https/acme/#providers
# https://go-acme.github.io/lego/dns/
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=route53"
# Enable below line to use staging letsencrypt server.
#- "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.letsencrypt.acme.email=${EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/certs/acme.json"
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
- ./data/proxy/certs:/certs
doh-server:
image: satishweb/doh-server:latest
hostname: doh-server
networks:
- default
environment:
# Enable below line to see more logs
# DEBUG: "1"
UPSTREAM_DNS_SERVER: "udp:unbound:53"
DOH_HTTP_PREFIX: "${DOH_HTTP_PREFIX}"
DOH_SERVER_LISTEN: ":${DOH_SERVER_LISTEN}"
DOH_SERVER_TIMEOUT: "10"
DOH_SERVER_TRIES: "3"
DOH_SERVER_VERBOSE: "false"
#volumes:
# - ./doh-server.conf:/server/doh-server.conf
# - ./app-config:/app-config
depends_on:
- unbound
labels:
- "traefik.enable=true"
- "traefik.http.routers.doh-server.rule=Host(`${SUBDOMAIN}.${DOMAIN}`) && Path(`${DOH_HTTP_PREFIX}`)"
- "traefik.http.services.doh-server.loadbalancer.server.port=${DOH_SERVER_LISTEN}"
- "traefik.http.middlewares.mw-doh-compression.compress=true"
- "traefik.http.routers.doh-server.tls=true"
- "traefik.http.middlewares.mw-doh-tls.headers.sslredirect=true"
- "traefik.http.middlewares.mw-doh-tls.headers.sslforcehost=true"
- "traefik.http.routers.doh-server.tls.certresolver=letsencrypt"
- "traefik.http.routers.doh-server.tls.domains[0].main=${DOMAIN}"
- "traefik.http.routers.doh-server.tls.domains[0].sans=${SUBDOMAIN}.${DOMAIN}"
# Protection from requests flood
- "traefik.http.middlewares.mw-doh-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.mw-doh-ratelimit.ratelimit.burst=50"
- "traefik.http.middlewares.mw-doh-ratelimit.ratelimit.period=10s"
unbound:
image: satishweb/unbound:latest
hostname: unbound
networks:
- default
ports:
# Disable these ports if DOH server is the only client
- 53:53/tcp
- 53:53/udp
volumes:
- ./unbound.sample.conf:/templates/unbound.sample.conf
- ./data/unbound/custom:/etc/unbound/custom
# Keep your custom.hosts file inside custom folder
#environment:
# DEBUG: "1"
````
> Complete Guide available at: https://github.com/satishweb/docker-doh
> IPV6 Support for Docker Compose based configuration TBA
### 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.
## EDNS0-Client-Subnet (GeoDNS) ## EDNS0-Client-Subnet (GeoDNS)
@@ -343,10 +125,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.

173
contrib/package.sh Executable file
View File

@@ -0,0 +1,173 @@
#!/bin/bash
set -euo pipefail
# This is a script used for automated packaging.
# Debian maintainers please don't use this.
#
# Environment assumption:
# * Ubuntu 16.04
# * run with normal user
# * sudo with no password
# * go and fpm is pre-installed
# * rpmbuild is required if you need rpm packages
#
# Compatible with Azure DevOps hosted Ubuntu 16.04 agent
export DEBIAN_FRONTEND="noninteractive"
export DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/..
export BUILD_BINARIESDIRECTORY="${BUILD_BINARIESDIRECTORY:-${DIR}/build/bin}"
export BUILD_ARTIFACTSTAGINGDIRECTORY="${BUILD_ARTIFACTSTAGINGDIRECTORY:-${DIR}/build/packages}"
export TMP_DIRECTORY="/tmp/dohbuild"
export GOPATH="${GOPATH:-/tmp/go}"
export GOBIN="${GOBIN:-/tmp/go/bin}"
function prepare_env() {
echo "Checking dependencies"
if ! [ -x "$(command -v go)" ]; then
echo "Please install golang"
exit 1
fi
if [ -x "$(command -v apt-get)" ]; then
sudo apt-get -y update
fi
if ! [ -x "$(command -v rpmbuild)" ]; then
# TODO: correctly install rpmbuild
! sudo apt-get -y install rpmbuild
fi
# if ! [ -x "$(command -v upx)" ]; then
# sudo apt-get -y install upx
# fi
echo "Creating directories"
mkdir -p "${BUILD_BINARIESDIRECTORY}/nm-dispatcher"
mkdir -p "${BUILD_BINARIESDIRECTORY}/launchd"
mkdir -p "${BUILD_BINARIESDIRECTORY}/systemd"
mkdir -p "${BUILD_BINARIESDIRECTORY}/config"
mkdir -p "${BUILD_ARTIFACTSTAGINGDIRECTORY}"
mkdir -p "${TMP_DIRECTORY}"
}
function build_common() {
cp NetworkManager/dispatcher.d/* "${BUILD_BINARIESDIRECTORY}"/nm-dispatcher
cp launchd/*.plist "${BUILD_BINARIESDIRECTORY}"/launchd
cp systemd/*.service "${BUILD_BINARIESDIRECTORY}"/systemd
cp doh-server/doh-server.conf "${BUILD_BINARIESDIRECTORY}"/config
cp doh-client/doh-client.conf "${BUILD_BINARIESDIRECTORY}"/config
}
# used to get version
function build_native() {
echo "Building a native binary..."
go build -ldflags="-s -w" -o ${BUILD_BINARIESDIRECTORY}/"${EXE}"-native
}
function build() {
echo "Building ${EXE} for OS=$1 ARCH=$2"
env GOOS="$1" GOARCH="$2" go build -ldflags="-s -w" -o ${BUILD_BINARIESDIRECTORY}/"${EXE}"-"$3"
# echo "Compressing executable"
# ! upx --ultra-brute ${BUILD_BINARIESDIRECTORY}/${EXE}-"$3" || true
}
function package() {
VERSION=$("${BUILD_BINARIESDIRECTORY}/${EXE}-native" --version | head -n 1 | cut -d" " -f2)
REVISION=$(git log --pretty=format:'%h' -n 1)
echo "Packaging ${EXE} ${VERSION} for OS=$1 ARCH=$2 TYPE=$3 DST=$4"
! rm -rf "${TMP_DIRECTORY}"/*
mkdir -p "${TMP_DIRECTORY}"/usr/bin
cp "${BUILD_BINARIESDIRECTORY}"/"${EXE}"-"$3" "${TMP_DIRECTORY}"/usr/bin/"${EXE}"
mkdir -p "${TMP_DIRECTORY}"/usr/lib/systemd/system
cp "${BUILD_BINARIESDIRECTORY}"/systemd/"${EXE}".service "${TMP_DIRECTORY}"/usr/lib/systemd/system
mkdir -p "${TMP_DIRECTORY}"/etc/dns-over-https
cp "${BUILD_BINARIESDIRECTORY}"/config/"${EXE}".conf "${TMP_DIRECTORY}"/etc/dns-over-https
mkdir -p "${TMP_DIRECTORY}"/etc/NetworkManager/dispatcher.d
cp "${BUILD_BINARIESDIRECTORY}"/nm-dispatcher/"${EXE}" "${TMP_DIRECTORY}"/etc/NetworkManager/dispatcher.d
# call fpm
fpm --input-type dir \
--output-type $4 \
--chdir "${TMP_DIRECTORY}" \
--package "${BUILD_ARTIFACTSTAGINGDIRECTORY}" \
--name "${EXE}" \
--description "${DESCR}" \
--version "${VERSION}" \
--iteration "${REVISION}" \
--url "https://github.com/m13253/dns-over-https" \
--vendor "Star Brilliant <coder@poorlab.com>" \
--license "MIT License" \
--category "net" \
--maintainer "James Swineson <autopkg@public.swineson.me>" \
--architecture "$2" \
--force \
.
}
cd "${DIR}"/..
prepare_env
make deps
build_common
pushd doh-server
export EXE="doh-server"
export DESCR="DNS-over-HTTPS Server"
build_native
build linux amd64 linux-amd64
package linux amd64 linux-amd64 deb
! package linux amd64 linux-amd64 rpm
package linux amd64 linux-amd64 pacman
build linux arm linux-armhf
package linux arm linux-armhf deb
! package linux arm linux-armhf rpm
package linux arm linux-armhf pacman
build linux arm64 linux-arm64
package linux arm64 linux-arm64 deb
! package linux arm64 linux-arm64 rpm
package linux arm64 linux-arm64 pacman
# build darwin amd64 darwin-amd64
# build windows 386 windows-x86.exe
# build windows amd64 windows-amd64.exe
popd
pushd doh-client
export EXE="doh-client"
export DESCR="DNS-over-HTTPS Client"
build_native
build linux amd64 linux-amd64
package linux amd64 linux-amd64 deb
! package linux amd64 linux-amd64 rpm
package linux amd64 linux-amd64 pacman
build linux arm linux-armhf
package linux arm linux-armhf deb
! package linux arm linux-armhf rpm
package linux arm linux-armhf pacman
build linux arm64 linux-arm64
package linux arm64 linux-arm64 deb
! package linux arm64 linux-arm64 rpm
package linux arm64 linux-arm64 pacman
# build darwin amd64 darwin-amd64
# build windows 386 windows-x86.exe
# build windows amd64 windows-amd64.exe
popd

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ PREFIX = /usr/local
all: doh-logger all: doh-logger
doh-logger: doh-logger.swift doh-logger: doh-logger.swift
$(SWIFTC) -o $@ -O $< $(SWIFTC) -o $@ -O -static-stdlib $<
clean: clean:
rm -f doh-logger rm -f doh-logger

View File

@@ -25,7 +25,6 @@ package main
import ( import (
"context" "context"
"crypto/tls"
"fmt" "fmt"
"log" "log"
"math/rand" "math/rand"
@@ -38,40 +37,39 @@ import (
"sync" "sync"
"time" "time"
"github.com/m13253/dns-over-https/doh-client/config"
"github.com/m13253/dns-over-https/doh-client/selector"
"github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/idna" "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 { 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) {
@@ -249,7 +247,6 @@ func (c *Client) newHTTPClient() error {
MaxIdleConnsPerHost: 10, MaxIdleConnsPerHost: 10,
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
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},
} }
if c.conf.Other.NoIPv6 { if c.conf.Other.NoIPv6 {
c.httpTransport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { c.httpTransport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
@@ -308,7 +305,7 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
if len(r.Question) != 1 { if len(r.Question) != 1 {
log.Println("Number of questions is not 1") log.Println("Number of questions is not 1")
reply := jsondns.PrepareReply(r) reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeFormatError reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply) w.WriteMsg(reply)
return return
@@ -359,7 +356,7 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
return return
} }
log.Println(err) log.Println(err)
reply = jsondns.PrepareReply(r) reply = jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
return return
@@ -428,7 +425,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)
}*/ }*/
@@ -474,7 +471,7 @@ func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddre
if err != nil { if err != nil {
return return
} }
if ip := remoteAddr.IP; jsondns.IsGlobalIP(ip) { if ip := remoteAddr.IP; jsonDNS.IsGlobalIP(ip) {
if ipv4 := ip.To4(); ipv4 != nil { if ipv4 := ip.To4(); ipv4 != nil {
ednsClientAddress = ipv4.Mask(ipv4Mask24) ednsClientAddress = ipv4.Mask(ipv4Mask24)
ednsClientNetmask = 24 ednsClientNetmask = 24

View File

@@ -47,16 +47,14 @@ type upstream struct {
} }
type others struct { type others struct {
Bootstrap []string `toml:"bootstrap"` Bootstrap []string `toml:"bootstrap"`
Passthrough []string `toml:"passthrough"` Passthrough []string `toml:"passthrough"`
Timeout uint `toml:"timeout"` Timeout uint `toml:"timeout"`
NoCookies bool `toml:"no_cookies"` NoCookies bool `toml:"no_cookies"`
NoECS bool `toml:"no_ecs"` NoECS bool `toml:"no_ecs"`
NoIPv6 bool `toml:"no_ipv6"` NoIPv6 bool `toml:"no_ipv6"`
NoUserAgent bool `toml:"no_user_agent"` Verbose bool `toml:"verbose"`
Verbose bool `toml:"verbose"` DebugHTTPHeaders []string `toml:"debug_http_headers"`
DebugHTTPHeaders []string `toml:"debug_http_headers"`
TLSInsecureSkipVerify bool `toml:"insecure_tls_skip_verify"`
} }
type Config struct { type Config struct {

View File

@@ -4,9 +4,6 @@ listen = [
"127.0.0.1:5380", "127.0.0.1:5380",
"[::1]:53", "[::1]:53",
"[::1]:5380", "[::1]:5380",
## To listen on both 0.0.0.0:53 and [::]:53, use the following line
# ":53",
] ]
# HTTP path for upstream resolver # HTTP path for upstream resolver
@@ -18,34 +15,34 @@ upstream_selector = "random"
# weight should in (0, 100], if upstream_selector is random, weight will be ignored # weight should in (0, 100], if upstream_selector is random, weight will be ignored
## Google's resolver, good ECS, good DNSSEC ## Google's productive resolver, good ECS, bad DNSSEC
#[[upstream.upstream_ietf]] #[[upstream.upstream_google]]
# url = "https://dns.google/dns-query" # url = "https://dns.google.com/resolve"
# weight = 50 # weight = 50
## CloudFlare's resolver, bad ECS, good DNSSEC ## CloudFlare's resolver, bad ECS, good DNSSEC
## ECS is disabled for privacy by design: https://developers.cloudflare.com/1.1.1.1/nitty-gritty-details/#edns-client-subnet #[[upstream.upstream_google]]
# url = "https://cloudflare-dns.com/dns-query"
# weight = 50
## CloudFlare's resolver, bad ECS, good DNSSEC
#[[upstream.upstream_google]]
# url = "https://1.1.1.1/dns-query"
# weight = 50
# CloudFlare's resolver, bad ECS, good DNSSEC
[[upstream.upstream_ietf]] [[upstream.upstream_ietf]]
url = "https://cloudflare-dns.com/dns-query" url = "https://cloudflare-dns.com/dns-query"
weight = 50 weight = 50
## CloudFlare's resolver, bad ECS, good DNSSEC ## CloudFlare's resolver, bad ECS, good DNSSEC
## ECS is disabled for privacy by design: https://developers.cloudflare.com/1.1.1.1/nitty-gritty-details/#edns-client-subnet
## Note that some ISPs have problems connecting to 1.1.1.1, try 1.0.0.1 if problems happen.
#[[upstream.upstream_ietf]] #[[upstream.upstream_ietf]]
# url = "https://1.1.1.1/dns-query" # url = "https://1.1.1.1/dns-query"
# weight = 50 # weight = 50
## DNS.SB's resolver, good ECS, good DNSSEC ## Google's experimental resolver, good ECS, good DNSSEC
## The provider claims no logging: https://dns.sb/doh/
#[[upstream.upstream_ietf]] #[[upstream.upstream_ietf]]
# url = "https://doh.dns.sb/dns-query" # url = "https://dns.google.com/experimental"
# weight = 50
## Quad9's resolver, bad ECS, good DNSSEC
## ECS is disabled for privacy by design: https://www.quad9.net/faq/#What_is_EDNS_Client-Subnet
#[[upstream.upstream_ietf]]
# url = "https://9.9.9.9/dns-query"
# weight = 50 # weight = 50
## CloudFlare's resolver for Tor, available only with Tor ## CloudFlare's resolver for Tor, available only with Tor
@@ -64,7 +61,7 @@ upstream_selector = "random"
# bootstrap server, please make this list empty. # bootstrap server, please make this list empty.
bootstrap = [ bootstrap = [
# Google's resolver, good ECS, good DNSSEC # Google's resolver, bad ECS, good DNSSEC
"8.8.8.8:53", "8.8.8.8:53",
"8.8.4.4:53", "8.8.4.4:53",
@@ -119,22 +116,5 @@ no_ecs = false
# Note that DNS listening and bootstrapping is not controlled by this option. # Note that DNS listening and bootstrapping is not controlled by this option.
no_ipv6 = false no_ipv6 = false
# Disable submitting User-Agent
#
# It is generally not recommended to disable submitting User-Agent because it
# is still possible to probe client version according to behavior differences,
# such as TLS handshaking, handling of malformed packets, and specific bugs.
# Additionally, User-Agent is an important way for the server to distinguish
# buggy, old, or insecure clients, and to workaround specific bugs.
# (e.g. doh-server can detect and workaround certain issues of DNSCrypt-Proxy
# and older Firefox.)
no_user_agent = false
# Enable logging # Enable logging
verbose = false verbose = false
# insecure_tls_skip_verification will disable necessary TLS security verification.
# This option is designed for testing or development purposes,
# turning on this option on public Internet may cause your connection
# vulnerable to MITM attack.
insecure_tls_skip_verify = false

View File

@@ -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/m13253/dns-over-https/doh-client/selector"
"github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/m13253/dns-over-https/v2/doh-client/selector"
jsondns "github.com/m13253/dns-over-https/v2/json-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 {
@@ -45,7 +44,7 @@ func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter
questionName := question.Name questionName := question.Name
questionClass := question.Qclass questionClass := question.Qclass
if questionClass != dns.ClassINET { if questionClass != dns.ClassINET {
reply := jsondns.PrepareReply(r) reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeRefused reply.Rcode = dns.RcodeRefused
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
@@ -68,9 +67,6 @@ func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter
udpSize := uint16(512) udpSize := uint16(512)
if opt := r.IsEdns0(); opt != nil { if opt := r.IsEdns0(); opt != nil {
udpSize = opt.UDPSize() udpSize = opt.UDPSize()
if opt.Do() {
requestURL += "&do=1"
}
} }
ednsClientAddress, ednsClientNetmask := c.findClientIP(w, r) ednsClientAddress, ednsClientNetmask := c.findClientIP(w, r)
@@ -78,10 +74,10 @@ 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)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
@@ -90,11 +86,7 @@ func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter
} }
req.Header.Set("Accept", "application/json, application/dns-message, application/dns-udpwireformat") req.Header.Set("Accept", "application/json, application/dns-message, application/dns-udpwireformat")
if !c.conf.Other.NoUserAgent { req.Header.Set("User-Agent", USER_AGENT)
req.Header.Set("User-Agent", USER_AGENT)
} else {
req.Header.Set("User-Agent", "")
}
req = req.WithContext(ctx) req = req.WithContext(ctx)
c.httpClientMux.RLock() c.httpClientMux.RLock()
@@ -112,7 +104,7 @@ func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsondns.PrepareReply(r) reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
@@ -122,7 +114,7 @@ func (c *Client) generateRequestGoogle(ctx context.Context, w dns.ResponseWriter
return &DNSRequest{ return &DNSRequest{
response: resp, response: resp,
reply: jsondns.PrepareReply(r), reply: jsonDNS.PrepareReply(r),
udpSize: udpSize, udpSize: udpSize,
ednsClientAddress: ednsClientAddress, ednsClientAddress: ednsClientAddress,
ednsClientNetmask: ednsClientNetmask, ednsClientNetmask: ednsClientNetmask,
@@ -141,7 +133,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
@@ -149,7 +141,7 @@ func (c *Client) parseResponseGoogle(ctx context.Context, w dns.ResponseWriter,
return return
} }
var respJSON jsondns.Response var respJSON jsonDNS.Response
err = json.Unmarshal(body, &respJSON) err = json.Unmarshal(body, &respJSON)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@@ -161,14 +153,8 @@ func (c *Client) parseResponseGoogle(ctx context.Context, w dns.ResponseWriter,
if respJSON.Status != dns.RcodeSuccess && respJSON.Comment != "" { if respJSON.Status != dns.RcodeSuccess && respJSON.Comment != "" {
log.Printf("DNS error: %s\n", respJSON.Comment) log.Printf("DNS error: %s\n", respJSON.Comment)
} }
fixEmptyNames(&respJSON)
fullReply := jsondns.Unmarshal(req.reply, &respJSON, req.udpSize, req.ednsClientNetmask) fullReply := jsonDNS.Unmarshal(req.reply, &respJSON, req.udpSize, req.ednsClientNetmask)
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,21 +162,14 @@ 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)
} }
// Fix DNS response empty []RR.Name
// Additional section won't be rectified
// see: https://stackoverflow.com/questions/52136176/what-is-additional-section-in-dns-and-how-it-works
func fixEmptyNames(respJSON *jsondns.Response) {
for i := range respJSON.Answer {
if respJSON.Answer[i].Name == "" {
respJSON.Answer[i].Name = "."
}
}
for i := range respJSON.Authority {
if respJSON.Authority[i].Name == "" {
respJSON.Authority[i].Name = "."
}
}
}

View File

@@ -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/m13253/dns-over-https/doh-client/selector"
"github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/m13253/dns-over-https/v2/doh-client/selector"
jsondns "github.com/m13253/dns-over-https/v2/json-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 {
@@ -91,7 +90,7 @@ func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter,
requestBinary, err := r.Pack() requestBinary, err := r.Pack()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsondns.PrepareReply(r) reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeFormatError reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
@@ -105,10 +104,10 @@ 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)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
@@ -119,7 +118,7 @@ func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter,
req, err = http.NewRequest(http.MethodPost, upstream.URL, bytes.NewReader(requestBinary)) req, err = http.NewRequest(http.MethodPost, upstream.URL, bytes.NewReader(requestBinary))
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsondns.PrepareReply(r) reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
@@ -129,11 +128,7 @@ func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter,
req.Header.Set("Content-Type", "application/dns-message") req.Header.Set("Content-Type", "application/dns-message")
} }
req.Header.Set("Accept", "application/dns-message, application/dns-udpwireformat, application/json") req.Header.Set("Accept", "application/dns-message, application/dns-udpwireformat, application/json")
if !c.conf.Other.NoUserAgent { req.Header.Set("User-Agent", USER_AGENT)
req.Header.Set("User-Agent", USER_AGENT)
} else {
req.Header.Set("User-Agent", "")
}
req = req.WithContext(ctx) req = req.WithContext(ctx)
c.httpClientMux.RLock() c.httpClientMux.RLock()
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
@@ -150,7 +145,7 @@ func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter,
if err != nil { if err != nil {
log.Println(err) log.Println(err)
reply := jsondns.PrepareReply(r) reply := jsonDNS.PrepareReply(r)
reply.Rcode = dns.RcodeServerFailure reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply) w.WriteMsg(reply)
return &DNSRequest{ return &DNSRequest{
@@ -160,7 +155,7 @@ func (c *Client) generateRequestIETF(ctx context.Context, w dns.ResponseWriter,
return &DNSRequest{ return &DNSRequest{
response: resp, response: resp,
reply: jsondns.PrepareReply(r), reply: jsonDNS.PrepareReply(r),
udpSize: udpSize, udpSize: udpSize,
ednsClientAddress: ednsClientAddress, ednsClientAddress: ednsClientAddress,
ednsClientNetmask: ednsClientNetmask, ednsClientNetmask: ednsClientNetmask,
@@ -179,9 +174,9 @@ 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.Println(err)
req.reply.Rcode = dns.RcodeServerFailure req.reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(req.reply) w.WriteMsg(req.reply)
return return
@@ -192,7 +187,7 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
if nowDate, err := time.Parse(http.TimeFormat, headerNow); err == nil { if nowDate, err := time.Parse(http.TimeFormat, headerNow); err == nil {
now = nowDate now = nowDate
} else { } else {
log.Printf("Date header parse error from upstream %s: %v\n", req.currentUpstream, err) log.Println(err)
} }
} }
headerLastModified := req.response.Header.Get("Last-Modified") headerLastModified := req.response.Header.Get("Last-Modified")
@@ -201,7 +196,7 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
if lastModifiedDate, err := time.Parse(http.TimeFormat, headerLastModified); err == nil { if lastModifiedDate, err := time.Parse(http.TimeFormat, headerLastModified); err == nil {
lastModified = lastModifiedDate lastModified = lastModifiedDate
} else { } else {
log.Printf("Last-Modified header parse error from upstream %s: %v\n", req.currentUpstream, err) log.Println(err)
} }
} }
timeDelta := now.Sub(lastModified) timeDelta := now.Sub(lastModified)
@@ -212,7 +207,7 @@ func (c *Client) parseResponseIETF(ctx context.Context, w dns.ResponseWriter, r
fullReply := new(dns.Msg) fullReply := new(dns.Msg)
err = fullReply.Unpack(body) err = fullReply.Unpack(body)
if err != nil { if err != nil {
log.Printf("unpacking error from upstream %s: %v\n", req.currentUpstream, err) log.Println(err)
req.reply.Rcode = dns.RcodeServerFailure req.reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(req.reply) w.WriteMsg(req.reply)
return return
@@ -232,22 +227,23 @@ 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.Println(err)
req.reply.Rcode = dns.RcodeServerFailure req.reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(req.reply) w.WriteMsg(req.reply)
return return
} }
_, err = w.Write(buf) if !isTCP && len(buf) > int(req.udpSize) {
if err != nil { fullReply.Truncated = true
log.Printf("failed to write to client: %v\n", err) buf, err = fullReply.Pack()
if err != nil {
log.Println(err)
return
}
buf = buf[:req.udpSize]
} }
w.Write(buf)
} }
func fixRecordTTL(rr dns.RR, delta time.Duration) dns.RR { func fixRecordTTL(rr dns.RR, delta time.Duration) dns.RR {

View File

@@ -26,19 +26,21 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"io"
"io/ioutil"
"log" "log"
"os" "os"
"runtime" "runtime"
"strconv" "strconv"
"github.com/m13253/dns-over-https/v2/doh-client/config" "github.com/m13253/dns-over-https/doh-client/config"
) )
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
} }
@@ -87,7 +89,7 @@ func main() {
flag.Parse() flag.Parse()
if *showVersion { if *showVersion {
fmt.Printf("doh-client %s\nHomepage: https://github.com/m13253/dns-over-https\n", VERSION) fmt.Printf("doh-server %s\nHomepage: https://github.com/m13253/dns-over-https\n", VERSION)
return return
} }

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.0.1"
USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)" USER_AGENT = "DNS-over-HTTPS/" + VERSION + " (+https://github.com/m13253/dns-over-https)"
) )

View File

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

View File

@@ -2,9 +2,6 @@
listen = [ listen = [
"127.0.0.1:8053", "127.0.0.1:8053",
"[::1]:8053", "[::1]:8053",
## To listen on both 0.0.0.0:8053 and [::]:8053, use the following line
# ":8053",
] ]
# Local address and port for upstream DNS # Local address and port for upstream DNS
@@ -27,16 +24,11 @@ path = "/dns-query"
# Upstream DNS resolver # Upstream DNS resolver
# If multiple servers are specified, a random one will be chosen each time. # If multiple servers are specified, a random one will be chosen each time.
# You can use "udp", "tcp" or "tcp-tls" for the type prefix.
# For "udp", UDP will first be used, and switch to TCP when the server asks to
# or the response is too large.
# For "tcp", only TCP will be used.
# For "tcp-tls", DNS-over-TLS (RFC 7858) will be used to secure the upstream connection.
upstream = [ upstream = [
"udp:1.1.1.1:53", "1.1.1.1:53",
"udp:1.0.0.1:53", "1.0.0.1:53",
"udp:8.8.8.8:53", "8.8.8.8:53",
"udp:8.8.4.4:53", "8.8.4.4:53",
] ]
# Upstream timeout # Upstream timeout
@@ -45,34 +37,12 @@ timeout = 10
# Number of tries if upstream DNS fails # Number of tries if upstream DNS fails
tries = 3 tries = 3
# Only use TCP for DNS query
tcp_only = false
# Enable logging # Enable logging
verbose = false verbose = false
# Enable log IP from HTTPS-reverse proxy header: X-Forwarded-For or X-Real-IP # Enable log IP from HTTPS-reverse proxy header: X-Forwarded-For or X-Real-IP
# Note: http uri/useragent log cannot be controlled by this config # Note: http uri/useragent log cannot be controlled by this config
log_guessed_client_ip = false log_guessed_client_ip = false
# By default, non global IP addresses are never forwarded to upstream servers.
# This is to prevent two things from happening:
# 1. the upstream server knowing your private LAN addresses;
# 2. the upstream server unable to provide geographically near results,
# or even fail to provide any result.
# However, if you are deploying a split tunnel corporation network
# environment, or for any other reason you want to inhibit this
# behavior and allow local (eg RFC1918) address to be forwarded,
# change the following option to "true".
ecs_allow_non_global_ip = false
# If ECS is added to the request, let the full IP address or
# cap it to 24 or 128 mask. This option is to be used only on private
# networks where knwoledge of the terminal endpoint may be required for
# security purposes (eg. DNS Firewalling). Not a good option on the
# internet where IP address may be used to identify the user and
# not only the approximate location.
ecs_use_precise_ip = false
# If DOH is used for a controlled network, it is possible to enable
# the client TLS certificate validation with a specific certificate
# authority used to sign any client one. Disabled by default.
# tls_client_auth = true
# tls_client_auth_ca = "root-ca-public.crt"

View File

@@ -34,10 +34,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/m13253/dns-over-https/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,51 +169,12 @@ 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)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
jsondns.FormatError(w, fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), 500) jsonDNS.FormatError(w, fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), 500)
return return
} }

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/m13253/dns-over-https/json-dns"
"github.com/miekg/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 { 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\""),
} }
} }
@@ -126,7 +125,6 @@ func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r
} }
} }
isTailored := edns0Subnet == nil isTailored := edns0Subnet == nil
if edns0Subnet == nil { if edns0Subnet == nil {
ednsClientFamily := uint16(0) ednsClientFamily := uint16(0)
ednsClientAddress := s.findClientIP(r) ednsClientAddress := s.findClientIP(r)
@@ -135,20 +133,10 @@ func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r
if ipv4 := ednsClientAddress.To4(); ipv4 != nil { if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1 ednsClientFamily = 1
ednsClientAddress = ipv4 ednsClientAddress = ipv4
if s.conf.ECSUsePreciseIP { ednsClientNetmask = 24
ednsClientNetmask = 32
} else {
ednsClientNetmask = 24
ednsClientAddress = ednsClientAddress.Mask(net.CIDRMask(24, 32))
}
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
if s.conf.ECSUsePreciseIP { ednsClientNetmask = 56
ednsClientNetmask = 128
} else {
ednsClientNetmask = 56
ednsClientAddress = ednsClientAddress.Mask(net.CIDRMask(56, 128))
}
} }
edns0Subnet = new(dns.EDNS0_SUBNET) edns0Subnet = new(dns.EDNS0_SUBNET)
edns0Subnet.Code = dns.EDNS0SUBNET edns0Subnet.Code = dns.EDNS0SUBNET
@@ -168,12 +156,12 @@ func (s *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r
} }
func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) { func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) {
respJSON := jsondns.Marshal(req.response) respJSON := jsonDNS.Marshal(req.response)
req.response.Id = req.transactionID req.response.Id = req.transactionID
respBytes, err := req.response.Pack() respBytes, err := req.response.Pack()
if err != nil { if err != nil {
log.Printf("DNS packet construct failure with upstream %s: %v\n", req.currentUpstream, err) log.Println(err)
jsondns.FormatError(w, fmt.Sprintf("DNS packet construct failure (%s)", err.Error()), 500) jsonDNS.FormatError(w, fmt.Sprintf("DNS packet construct failure (%s)", err.Error()), 500)
return return
} }
@@ -183,6 +171,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))
@@ -193,21 +183,15 @@ func (s *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter
} }
if respJSON.Status == dns.RcodeServerFailure { if respJSON.Status == dns.RcodeServerFailure {
log.Printf("received server failure from upstream %s: %v\n", req.currentUpstream, req.response)
w.WriteHeader(503) w.WriteHeader(503)
} }
_, err = w.Write(respBytes) w.Write(respBytes)
if err != nil {
log.Printf("failed to write to client: %v\n", err)
}
} }
// Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe. // Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe
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 { log.Println("DNSCrypt-Proxy detected. Patching response.")
log.Println("DNSCrypt-Proxy detected. Patching response.")
}
w.Header().Set("Content-Type", "application/dns-message") w.Header().Set("Content-Type", "application/dns-message")
w.Header().Set("Vary", "Accept, User-Agent") w.Header().Set("Vary", "Accept, User-Agent")
now := time.Now().UTC().Format(http.TimeFormat) now := time.Now().UTC().Format(http.TimeFormat)
@@ -217,3 +201,15 @@ 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") {
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

@@ -25,8 +25,6 @@ package main
import ( import (
"context" "context"
"crypto/tls"
"crypto/x509"
"fmt" "fmt"
"log" "log"
"math/rand" "math/rand"
@@ -37,27 +35,25 @@ import (
"time" "time"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/m13253/dns-over-https/json-dns"
"github.com/miekg/dns" "github.com/miekg/dns"
jsondns "github.com/m13253/dns-over-https/v2/json-dns"
) )
type Server struct { type Server struct {
conf *config conf *config
udpClient *dns.Client udpClient *dns.Client
tcpClient *dns.Client tcpClient *dns.Client
tcpClientTLS *dns.Client servemux *http.ServeMux
servemux *http.ServeMux
} }
type DNSRequest struct { type DNSRequest struct {
request *dns.Msg request *dns.Msg
response *dns.Msg response *dns.Msg
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) {
@@ -73,10 +69,6 @@ func NewServer(conf *config) (*Server, error) {
Net: "tcp", Net: "tcp",
Timeout: timeout, Timeout: timeout,
}, },
tcpClientTLS: &dns.Client{
Net: "tcp-tls",
Timeout: timeout,
},
servemux: http.NewServeMux(), servemux: http.NewServeMux(),
} }
if conf.LocalAddr != "" { if conf.LocalAddr != "" {
@@ -96,10 +88,6 @@ func NewServer(conf *config) (*Server, error) {
Timeout: timeout, Timeout: timeout,
LocalAddr: tcpLocalAddr, LocalAddr: tcpLocalAddr,
} }
s.tcpClientTLS.Dialer = &net.Dialer{
Timeout: timeout,
LocalAddr: tcpLocalAddr,
}
} }
s.servemux.HandleFunc(conf.Path, s.handlerFunc) s.servemux.HandleFunc(conf.Path, s.handlerFunc)
return s, nil return s, nil
@@ -110,48 +98,12 @@ func (s *Server) Start() error {
if s.conf.Verbose { if s.conf.Verbose {
servemux = handlers.CombinedLoggingHandler(os.Stdout, servemux) servemux = handlers.CombinedLoggingHandler(os.Stdout, servemux)
} }
var clientCAPool *x509.CertPool
if s.conf.TLSClientAuth {
if s.conf.TLSClientAuthCA != "" {
clientCA, err := os.ReadFile(s.conf.TLSClientAuthCA)
if err != nil {
log.Fatalf("Reading certificate for client authentication has failed: %v", err)
}
clientCAPool = x509.NewCertPool()
clientCAPool.AppendCertsFromPEM(clientCA)
log.Println("Certificate loaded for client TLS authentication")
} else {
log.Fatalln("TLS client authentication requires both tls_client_auth and tls_client_auth_ca, exiting.")
}
}
results := make(chan error, len(s.conf.Listen)) results := make(chan error, len(s.conf.Listen))
for _, addr := range s.conf.Listen { for _, addr := range s.conf.Listen {
go func(addr string) { go func(addr string) {
var err error var err error
if s.conf.Cert != "" || s.conf.Key != "" { if s.conf.Cert != "" || s.conf.Key != "" {
if clientCAPool != nil { err = http.ListenAndServeTLS(addr, s.conf.Cert, s.conf.Key, servemux)
srvtls := &http.Server{
Handler: servemux,
Addr: addr,
TLSConfig: &tls.Config{
ClientCAs: clientCAPool,
ClientAuth: tls.RequireAndVerifyClientCert,
GetCertificate: func(info *tls.ClientHelloInfo) (certificate *tls.Certificate, e error) {
c, err := tls.LoadX509KeyPair(s.conf.Cert, s.conf.Key)
if err != nil {
fmt.Printf("Error loading server certificate key pair: %v\n", err)
return nil, err
}
return &c, nil
},
},
}
err = srvtls.ListenAndServeTLS("", "")
} else {
err = http.ListenAndServeTLS(addr, s.conf.Cert, s.conf.Key, servemux)
}
} else { } else {
err = http.ListenAndServe(addr, servemux) err = http.ListenAndServe(addr, servemux)
} }
@@ -175,18 +127,6 @@ func (s *Server) Start() error {
func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) { func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
if strings.ContainsRune(realIP, ':') {
r.RemoteAddr = "[" + realIP + "]:0"
} else {
r.RemoteAddr = realIP + ":0"
}
_, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
r.RemoteAddr = realIP
}
}
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS, POST") w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS, POST")
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
@@ -255,22 +195,23 @@ func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
} else if contentType == "application/dns-udpwireformat" { } else if contentType == "application/dns-udpwireformat" {
req = s.parseRequestIETF(ctx, w, r) req = s.parseRequestIETF(ctx, w, r)
} else { } else {
jsondns.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 415) jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 415)
return return
} }
if req.errcode == 444 { if req.errcode == 444 {
return return
} }
if req.errcode != 0 { if req.errcode != 0 {
jsondns.FormatError(w, req.errtext, req.errcode) jsonDNS.FormatError(w, req.errtext, req.errcode)
return return
} }
req = s.patchRootRD(req) req = s.patchRootRD(req)
err := s.doDNSQuery(ctx, req) var err error
req, err = s.doDNSQuery(ctx, req)
if err != nil { if err != nil {
jsondns.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503) jsonDNS.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503)
return return
} }
@@ -284,17 +225,12 @@ 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")
if strings.EqualFold(noEcs, "true") {
return nil
}
XForwardedFor := r.Header.Get("X-Forwarded-For") XForwardedFor := r.Header.Get("X-Forwarded-For")
if XForwardedFor != "" { if XForwardedFor != "" {
for _, addr := range strings.Split(XForwardedFor, ",") { for _, addr := range strings.Split(XForwardedFor, ",") {
addr = strings.TrimSpace(addr) addr = strings.TrimSpace(addr)
ip := net.ParseIP(addr) ip := net.ParseIP(addr)
if jsondns.IsGlobalIP(ip) { if jsonDNS.IsGlobalIP(ip) {
return ip return ip
} }
} }
@@ -303,23 +239,21 @@ func (s *Server) findClientIP(r *http.Request) net.IP {
if XRealIP != "" { if XRealIP != "" {
addr := strings.TrimSpace(XRealIP) addr := strings.TrimSpace(XRealIP)
ip := net.ParseIP(addr) ip := net.ParseIP(addr)
if s.conf.ECSAllowNonGlobalIP || jsondns.IsGlobalIP(ip) { if jsonDNS.IsGlobalIP(ip) {
return ip return ip
} }
} }
remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr)
if err != nil { if err != nil {
return nil return nil
} }
ip := remoteAddr.IP if ip := remoteAddr.IP; jsonDNS.IsGlobalIP(ip) {
if s.conf.ECSAllowNonGlobalIP || jsondns.IsGlobalIP(ip) {
return ip return 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,54 +263,24 @@ func (s *Server) patchRootRD(req *DNSRequest) *DNSRequest {
return req return req
} }
// Return the position index for the question of qtype from a DNS msg, otherwise return -1. func (s *Server) doDNSQuery(ctx context.Context, req *DNSRequest) (resp *DNSRequest, err error) {
func (s *Server) indexQuestionType(msg *dns.Msg, qtype uint16) int { // TODO(m13253): Make ctx work. Waiting for a patch for ExchangeContext from miekg/dns.
for i, question := range msg.Question {
if question.Qtype == qtype {
return i
}
}
return -1
}
func (s *Server) doDNSQuery(ctx context.Context, req *DNSRequest) (err error) {
numServers := len(s.conf.Upstream) numServers := len(s.conf.Upstream)
for i := uint(0); i < s.conf.Tries; i++ { for i := uint(0); i < s.conf.Tries; i++ {
req.currentUpstream = s.conf.Upstream[rand.Intn(numServers)] req.currentUpstream = s.conf.Upstream[rand.Intn(numServers)]
if !s.conf.TCPOnly {
upstream, t := addressAndType(req.currentUpstream) req.response, _, err = s.udpClient.Exchange(req.request, req.currentUpstream)
if err == nil && req.response != nil && req.response.Truncated {
switch t { log.Println(err)
default: req.response, _, err = s.tcpClient.Exchange(req.request, req.currentUpstream)
log.Printf("invalid DNS type %q in upstream %q", t, upstream)
return &configError{"invalid DNS type"}
// Use DNS-over-TLS (DoT) if configured to do so
case "tcp-tls":
req.response, _, err = s.tcpClientTLS.ExchangeContext(ctx, req.request, upstream)
case "tcp", "udp":
// Use TCP if always configured to or if the Query type dictates it (AXFR)
if t == "tcp" || (s.indexQuestionType(req.request, dns.TypeAXFR) > -1) {
req.response, _, err = s.tcpClient.ExchangeContext(ctx, req.request, upstream)
} else {
req.response, _, err = s.udpClient.ExchangeContext(ctx, req.request, upstream)
if err == nil && req.response != nil && req.response.Truncated {
log.Println(err)
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
if (s.indexQuestionType(req.request, dns.TypeIXFR) > -1) &&
(len(req.response.Answer) == 1) &&
(req.response.Answer[0].Header().Rrtype == dns.TypeSOA) {
req.response, _, err = s.tcpClient.ExchangeContext(ctx, req.request, upstream)
}
} }
} else {
req.response, _, err = s.tcpClient.Exchange(req.request, req.currentUpstream)
} }
if err == nil { if err == nil {
return nil return req, nil
} }
log.Printf("DNS error from upstream %s: %s\n", req.currentUpstream, err.Error()) log.Printf("DNS error from upstream %s: %s\n", req.currentUpstream, err.Error())
} }
return err return req, err
} }

View File

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

25
go.mod
View File

@@ -1,20 +1,13 @@
module github.com/m13253/dns-over-https/v2 module github.com/m13253/dns-over-https
go 1.23.5 go 1.12
require ( require (
github.com/BurntSushi/toml v1.5.0 github.com/BurntSushi/toml v0.3.1
github.com/gorilla/handlers v1.5.2 github.com/gorilla/handlers v1.4.0
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc github.com/miekg/dns v1.1.6
github.com/miekg/dns v1.1.64 golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
golang.org/x/net v0.38.0 golang.org/x/net v0.0.0-20190311183353-d8887717615a
) golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect
golang.org/x/sys v0.0.0-20190312061237-fead79001313 // indirect
require (
github.com/felixge/httpsnoop v1.0.4 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.31.0 // indirect
) )

53
go.sum
View File

@@ -1,28 +1,25 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/miekg/dns v1.1.6 h1:jVwb4GDwD65q/gtItR/lIZHjNH93QfeGxZUkzJcW9mc=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/miekg/dns v1.1.6/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc h1:RhT2pjLo3EVRmldbEcBdeRA7CGPWsNEJC+Y/N1aXQbg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
github.com/infobloxopen/go-trees v0.0.0-20221216143356-66ceba885ebc/go.mod h1:BaIJzjD2ZnHmx2acPF6XfGLPzNCMiBbMRqJr+8/8uRI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U=
github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190308023053-584f3b12f43e h1:K7CV15oJ823+HLXQ+M7MSMrUg8LjfqY7O3naO+8Pp/I=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.0.0-20190308023053-584f3b12f43e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
*/ */
package jsondns package jsonDNS
import ( import (
"net" "net"

View File

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

View File

@@ -21,7 +21,7 @@
DEALINGS IN THE SOFTWARE. DEALINGS IN THE SOFTWARE.
*/ */
package jsondns package jsonDNS
import ( import (
"fmt" "fmt"
@@ -38,7 +38,7 @@ func PrepareReply(req *dns.Msg) *dns.Msg {
reply := new(dns.Msg) reply := new(dns.Msg)
reply.Id = req.Id reply.Id = req.Id
reply.Response = true reply.Response = true
reply.Opcode = req.Opcode reply.Opcode = reply.Opcode
reply.RecursionDesired = req.RecursionDesired reply.RecursionDesired = req.RecursionDesired
reply.RecursionAvailable = req.RecursionDesired reply.RecursionAvailable = req.RecursionDesired
reply.CheckingDisabled = req.CheckingDisabled reply.CheckingDisabled = req.CheckingDisabled

View File

@@ -12,7 +12,7 @@ LimitNOFILE=1048576
Restart=always Restart=always
RestartSec=3 RestartSec=3
Type=simple Type=simple
DynamicUser=yes User=nobody
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -10,7 +10,7 @@ LimitNOFILE=1048576
Restart=always Restart=always
RestartSec=3 RestartSec=3
Type=simple Type=simple
DynamicUser=yes User=nobody
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target