mirror of
https://github.com/SamTherapy/dnscrypt.git
synced 2024-12-22 00:50:42 +00:00
DNSCrypt server implementation, major refactoring
This is a major refactoring of this library. Here's what changed: 1. Added DNSCrypt server implementation. 2. Added a command-line tool that can be used for everything related to DNSCrypt: generating resolver config, making lookups, running a DNSCrypt resolver The programming interface was also updated, this library can be used by other software to incorporate DNSCrypt for both server-side or client-side.
This commit is contained in:
commit
1b4a041840
37 changed files with 3244 additions and 766 deletions
|
@ -2,7 +2,7 @@ coverage:
|
|||
status:
|
||||
project:
|
||||
default:
|
||||
target: 40%
|
||||
target: 60%
|
||||
threshold: null
|
||||
patch: false
|
||||
changes: false
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
.idea
|
||||
.vscode
|
||||
coverage.txt
|
88
.travis.yml
88
.travis.yml
|
@ -1,14 +1,94 @@
|
|||
language: go
|
||||
sudo: false
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
- GO111MODULE=on
|
||||
|
||||
go:
|
||||
- 1.x
|
||||
- 1.x
|
||||
|
||||
before_install:
|
||||
- |-
|
||||
case $TRAVIS_OS_NAME in
|
||||
linux | osx)
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.31.0
|
||||
;;
|
||||
esac
|
||||
|
||||
script:
|
||||
- go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
- |-
|
||||
# Fail if any command fails
|
||||
set -e
|
||||
|
||||
case $TRAVIS_OS_NAME in
|
||||
linux | osx)
|
||||
# Run linter
|
||||
golangci-lint run
|
||||
|
||||
# Run tests
|
||||
go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
# Windows-386 build
|
||||
GOOS=windows GOARCH=386 VERSION=${TRAVIS_TAG:-dev} make release
|
||||
|
||||
# Windows-amd64 build
|
||||
GOOS=windows GOARCH=amd64 VERSION=${TRAVIS_TAG:-dev} make release
|
||||
|
||||
# Linux-386 build
|
||||
GOOS=linux GOARCH=386 VERSION=${TRAVIS_TAG:-dev} make release
|
||||
|
||||
# Linux-amd64 build
|
||||
GOOS=linux GOARCH=amd64 VERSION=${TRAVIS_TAG:-dev} make release
|
||||
|
||||
# Linux-arm64 build
|
||||
GOOS=linux GOARCH=arm64 VERSION=${TRAVIS_TAG:-dev} make release
|
||||
|
||||
# Linux-armv6 build
|
||||
GOOS=linux GOARCH=arm GOARM=6 VERSION=${TRAVIS_TAG:-dev} make release
|
||||
|
||||
# Linux-mips build
|
||||
GOOS=linux GOARCH=mips GOMIPS=softfloat VERSION=${TRAVIS_TAG:-dev} make release
|
||||
|
||||
# Linux-mipsle build
|
||||
GOOS=linux GOARCH=mipsle GOMIPS=softfloat VERSION=${TRAVIS_TAG:-dev} make release
|
||||
|
||||
# freebsd-armv6 build
|
||||
GOOS=freebsd GOARCH=arm GOARM=6 VERSION=${TRAVIS_TAG:-dev} make release
|
||||
|
||||
# Darwin-amd64 build
|
||||
GOOS=darwin GOARCH=amd64 VERSION=${TRAVIS_TAG:-dev} make release
|
||||
|
||||
# List build output
|
||||
ls -l build/dnscrypt-*
|
||||
;;
|
||||
windows)
|
||||
# Run tests
|
||||
go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
;;
|
||||
esac
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
- |-
|
||||
case $TRAVIS_OS_NAME in
|
||||
linux)
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
;;
|
||||
esac
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: $GITHUB_TOKEN
|
||||
file:
|
||||
- build/dnscrypt-*.zip
|
||||
- build/dnscrypt-*.tar.gz
|
||||
on:
|
||||
repo: ameshkov/dnscrypt
|
||||
tags: true
|
||||
condition: $TRAVIS_OS_NAME = linux
|
||||
file_glob: true
|
||||
skip_cleanup: true
|
||||
|
|
43
Makefile
Normal file
43
Makefile
Normal file
|
@ -0,0 +1,43 @@
|
|||
NAME=dnscrypt
|
||||
BASE_BUILDDIR=build
|
||||
BUILDNAME=$(GOOS)-$(GOARCH)$(GOARM)
|
||||
BUILDDIR=$(BASE_BUILDDIR)/$(BUILDNAME)
|
||||
VERSION?=dev
|
||||
|
||||
ifeq ($(GOOS),windows)
|
||||
ext=.exe
|
||||
archiveCmd=zip -9 -r $(NAME)-$(BUILDNAME)-$(VERSION).zip $(BUILDNAME)
|
||||
else
|
||||
ext=
|
||||
archiveCmd=tar czpvf $(NAME)-$(BUILDNAME)-$(VERSION).tar.gz $(BUILDNAME)
|
||||
endif
|
||||
|
||||
.PHONY: default
|
||||
default: build
|
||||
|
||||
build: clean test
|
||||
go build -ldflags "-X main.VersionString=$(VERSION)" -o $(NAME)$(ext) ./cmd
|
||||
|
||||
release: check-env-release
|
||||
mkdir -p $(BUILDDIR)
|
||||
cp LICENSE $(BUILDDIR)/
|
||||
cp README.md $(BUILDDIR)/
|
||||
CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags "-X main.VersionString=$(VERSION)" -o $(BUILDDIR)/$(NAME)$(ext)
|
||||
cd $(BASE_BUILDDIR) ; $(archiveCmd)
|
||||
|
||||
test:
|
||||
go test -race -v -bench=. ./...
|
||||
|
||||
clean:
|
||||
go clean
|
||||
rm -rf $(BASE_BUILDDIR)
|
||||
|
||||
check-env-release:
|
||||
@ if [ "$(GOOS)" = "" ]; then \
|
||||
echo "Environment variable GOOS not set"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@ if [ "$(GOARCH)" = "" ]; then \
|
||||
echo "Environment variable GOOS not set"; \
|
||||
exit 1; \
|
||||
fi
|
172
README.md
172
README.md
|
@ -1,35 +1,155 @@
|
|||
[![Build Status](https://travis-ci.org/ameshkov/dnscrypt.svg?branch=master)](https://travis-ci.org/ameshkov/dnscrypt)
|
||||
[![Build Status](https://travis-ci.com/ameshkov/dnscrypt.svg?branch=master)](https://travis-ci.com/ameshkov/dnscrypt)
|
||||
[![Code Coverage](https://img.shields.io/codecov/c/github/ameshkov/dnscrypt/master.svg)](https://codecov.io/github/ameshkov/dnscrypt?branch=master)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/ameshkov/dnscrypt)](https://goreportcard.com/report/ameshkov/dnscrypt)
|
||||
[![Go Doc](https://godoc.org/github.com/ameshkov/dnscrypt?status.svg)](https://godoc.org/github.com/ameshkov/dnscrypt)
|
||||
|
||||
# DNSCrypt Client
|
||||
# DNSCrypt Go
|
||||
|
||||
This is a very simple [DNSCrypt](https://dnscrypt.info/) client library written in Go.
|
||||
Golang-implementation of the [DNSCrypt v2 protocol](https://dnscrypt.info/protocol).
|
||||
|
||||
The idea is to let others use DNSCrypt resolvers in the same manner as we can use regular and DoT resolvers with [miekg's DNS library](https://github.com/miekg/dns).
|
||||
Unfortunately, I have not found an easy way to use [dnscrypt-proxy](https://github.com/jedisct1/dnscrypt-proxy) as a dependency so here's why this library was created.
|
||||
This repo includes everything you need to work with DNSCrypt. You can run your own resolver, make DNS lookups to other DNSCrypt resolvers, and you can use it as a library in your own projects.
|
||||
|
||||
### Usage
|
||||
* [Command-line tool](#commandline)
|
||||
* [How to install](#install)
|
||||
* [Running a server](#runningserver)
|
||||
* [Making lookups](#lookup)
|
||||
* [Programming interface](#api)
|
||||
* [Client](#client)
|
||||
* [Server](#server)
|
||||
|
||||
## <a id="commandline"></a> Command-line tool
|
||||
|
||||
`dnscrypt` is a helper tool that can work as a DNSCrypt client or server.
|
||||
|
||||
Please note, that even though this tool can work as a server, it's purpose is merely testing. Use [dnsproxy](https://github.com/AdguardTeam/dnsproxy) or [AdGuard Home](https://github.com/AdguardTeam/AdGuardHome) for real-life purposes.
|
||||
|
||||
|
||||
### <a id="install"></a> How to install
|
||||
|
||||
Download and unpack an archive for your platform from the [latest release](https://github.com/ameshkov/dnscrypt/releases).
|
||||
|
||||
### <a id="runningserver"></a> Running a server
|
||||
|
||||
Generate a configuration file for running a DNSCrypt server:
|
||||
|
||||
```shell script
|
||||
./dnscrypt generate --provider-name=2.dnscrypt-cert.example.org --out=config.yaml
|
||||
```
|
||||
|
||||
It will generate a configuration file that looks like this:
|
||||
|
||||
```yaml
|
||||
provider_name: 2.dnscrypt-cert.example.org
|
||||
public_key: F11DDBCC4817E543845FDDD4CB881849B64226F3DE397625669D87B919BC4FB0
|
||||
private_key: 5752095FFA56D963569951AFE70FE1690F378D13D8AD6F8054DFAA100907F8B6F11DDBCC4817E543845FDDD4CB881849B64226F3DE397625669D87B919BC4FB0
|
||||
resolver_secret: 9E46E79FEB3AB3D45F4EB3EA957DEAF5D9639A0179F1850AFABA7E58F87C74C4
|
||||
resolver_public: 9327C5E64783E19C339BD6B680A56DB85521CC6E4E0CA5DF5274E2D3CE026C6B
|
||||
es_version: 1
|
||||
certificate_ttl: 0s
|
||||
```
|
||||
|
||||
* `provider_name` - DNSCrypt resolver name.
|
||||
* `public_key`, `private_key` - keypair that is used by the DNSCrypt resolver to sign the certificate.
|
||||
* `resolver_secret`, `resolver_public` - keypair that is used by the DNSCrypt resolver to encrypt and decrypt messages.
|
||||
* `es_version` - crypto to use. Can be `1` (XSalsa20Poly1305) or `2` (XChacha20Poly1305).
|
||||
* `certificate_ttl` - certificate time-to-live. By default it's set to `0` and in this case 1-year cert is generated. The certificate is generated on `dnscrypt` start-up and it will only be valid for the specified amount of time. You should periodically restart `dnscrypt` to rotate the cert.
|
||||
|
||||
This configuration file can be used to run a DNSCrypt forwarding server:
|
||||
|
||||
```shell script
|
||||
./dnscrypt server --config=config.yaml --forward=94.140.14.140:53
|
||||
```
|
||||
|
||||
Now you can go to https://dnscrypt.info/stamps and use `provider_name` and `public_key` from this configuration to generate a DNS stamp. Here's how it looks like for a server running on `127.0.0.1:443`:
|
||||
|
||||
```
|
||||
// AdGuard DNS stamp
|
||||
stampStr := "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
|
||||
|
||||
// Initializing the DNSCrypt client
|
||||
c := dnscrypt.Client{Proto: "udp", Timeout: 10 * time.Second}
|
||||
|
||||
// Fetching and validating the server certificate
|
||||
serverInfo, rtt, err := client.Dial(stampStr)
|
||||
|
||||
// Create a DNS request
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
}
|
||||
|
||||
// Get the DNS response
|
||||
reply, rtt, err := c.Exchange(&req, serverInfo)
|
||||
```
|
||||
sdns://AQcAAAAAAAAADTEyNy4wLjAuMTo0NDMg8R3bzEgX5UOEX93Uy4gYSbZCJvPeOXYlZp2HuRm8T7AbMi5kbnNjcnlwdC1jZXJ0LmV4YW1wbGUub3Jn
|
||||
```
|
||||
|
||||
### <a id="lookup"></a> Making lookups
|
||||
|
||||
You can use that stamp to send a DNSCrypt request to your server:
|
||||
|
||||
```
|
||||
./dnscrypt lookup-stamp \
|
||||
--stamp=sdns://AQcAAAAAAAAADTEyNy4wLjAuMTo0NDMg8R3bzEgX5UOEX93Uy4gYSbZCJvPeOXYlZp2HuRm8T7AbMi5kbnNjcnlwdC1jZXJ0LmV4YW1wbGUub3Jn \
|
||||
--domain=example.org \
|
||||
--type=a
|
||||
```
|
||||
|
||||
You can also send a DNSCrypt request using a command that does not require stamps:
|
||||
|
||||
```
|
||||
./dnscrypt lookup \
|
||||
--provider-name=2.dnscrypt-cert.opendns.com \
|
||||
--public-key=b7351140206f225d3e2bd822d7fd691ea1c33cc8d6668d0cbe04bfabca43fb79 \
|
||||
--addr=208.67.220.220 \
|
||||
--domain=example.org \
|
||||
--type=a
|
||||
```
|
||||
|
||||
## <a id="api"></a> Programming interface
|
||||
|
||||
### <a id="client"></a> Client
|
||||
|
||||
```go
|
||||
// AdGuard DNS stamp
|
||||
stampStr := "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
|
||||
|
||||
// Initializing the DNSCrypt client
|
||||
c := dnscrypt.Client{Net: "udp", Timeout: 10 * time.Second}
|
||||
|
||||
// Fetching and validating the server certificate
|
||||
resolverInfo, err := client.Dial(stampStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a DNS request
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
}
|
||||
|
||||
// Get the DNS response
|
||||
reply, err := c.Exchange(&req, resolverInfo)
|
||||
```
|
||||
|
||||
## <a id="server"></a> Server
|
||||
|
||||
```go
|
||||
// Prepare the test DNSCrypt server config
|
||||
rc, err := dnscrypt.GenerateResolverConfig("example.org", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert, err := rc.CreateCert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s := &dnscrypt.Server{
|
||||
ProviderName: rc.ProviderName,
|
||||
ResolverCert: cert,
|
||||
Handler: dnscrypt.DefaultHandler,
|
||||
}
|
||||
|
||||
// Prepare TCP listener
|
||||
tcpConn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4zero, Port: 443})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare UDP listener
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 443})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start the server
|
||||
go s.ServeUDP(udpConn)
|
||||
go s.ServeTCP(tcpConn)
|
||||
```
|
||||
|
|
BIN
build/dnscrypt-windows-386-dev.zip
Normal file
BIN
build/dnscrypt-windows-386-dev.zip
Normal file
Binary file not shown.
25
build/windows-386/LICENSE
Normal file
25
build/windows-386/LICENSE
Normal file
|
@ -0,0 +1,25 @@
|
|||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
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 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.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
|
144
build/windows-386/README.md
Normal file
144
build/windows-386/README.md
Normal file
|
@ -0,0 +1,144 @@
|
|||
# DNSCrypt Go
|
||||
|
||||
Golang-implementation of the [DNSCrypt v2 protocol](https://dnscrypt.info/protocol).
|
||||
|
||||
This repo includes everything you need to work with DNSCrypt. You can run your own resolver, make DNS lookups to other DNSCrypt resolvers, and you can use it as a library in your own projects.
|
||||
|
||||
* [Command-line tool](#commandline)
|
||||
* [Running a server](#runningserver)
|
||||
* [Making lookups](#lookup)
|
||||
* [Programming interface](#api)
|
||||
* [Client](#client)
|
||||
* [Server](#server)
|
||||
|
||||
## <a id="commandline"></a> Command-line tool
|
||||
|
||||
`dnscrypt` is a helper tool that can work as a DNSCrypt client or server.
|
||||
|
||||
Please note, that even though this tool can work as a server, it's purpose is merely testing. Use [dnsproxy](https://github.com/AdguardTeam/dnsproxy) or [AdGuard Home](https://github.com/AdguardTeam/AdGuardHome) for real-life purposes.
|
||||
|
||||
### <a id="runningserver"></a> Running a server
|
||||
|
||||
Generate a configuration file for running a DNSCrypt server:
|
||||
|
||||
```shell script
|
||||
./dnscrypt generate --provider-name=2.dnscrypt-cert.example.org --out=config.yaml
|
||||
```
|
||||
|
||||
It will generate a configuration file that looks like this:
|
||||
|
||||
```yaml
|
||||
provider_name: 2.dnscrypt-cert.example.org
|
||||
public_key: F11DDBCC4817E543845FDDD4CB881849B64226F3DE397625669D87B919BC4FB0
|
||||
private_key: 5752095FFA56D963569951AFE70FE1690F378D13D8AD6F8054DFAA100907F8B6F11DDBCC4817E543845FDDD4CB881849B64226F3DE397625669D87B919BC4FB0
|
||||
resolver_secret: 9E46E79FEB3AB3D45F4EB3EA957DEAF5D9639A0179F1850AFABA7E58F87C74C4
|
||||
resolver_public: 9327C5E64783E19C339BD6B680A56DB85521CC6E4E0CA5DF5274E2D3CE026C6B
|
||||
es_version: 1
|
||||
certificate_ttl: 0s
|
||||
```
|
||||
|
||||
* `provider_name` - DNSCrypt resolver name.
|
||||
* `public_key`, `private_key` - keypair that is used by the DNSCrypt resolver to sign the certificate.
|
||||
* `resolver_secret`, `resolver_public` - keypair that is used by the DNSCrypt resolver to encrypt and decrypt messages.
|
||||
* `es_version` - crypto to use. Can be `1` (XSalsa20Poly1305) or `2` (XChacha20Poly1305).
|
||||
* `certificate_ttl` - certificate time-to-live. By default it's set to `0` and in this case 1-year cert is generated. The certificate is generated on `dnscrypt` start-up and it will only be valid for the specified amount of time. You should periodically restart `dnscrypt` to rotate the cert.
|
||||
|
||||
This configuration file can be used to run a DNSCrypt forwarding server:
|
||||
|
||||
```shell script
|
||||
./dnscrypt server --config=config.yaml --forward=94.140.14.140:53
|
||||
```
|
||||
|
||||
Now you can go to https://dnscrypt.info/stamps and use `provider_name` and `public_key` from this configuration to generate a DNS stamp. Here's how it looks like for a server running on `127.0.0.1:443`:
|
||||
|
||||
```
|
||||
sdns://AQcAAAAAAAAADTEyNy4wLjAuMTo0NDMg8R3bzEgX5UOEX93Uy4gYSbZCJvPeOXYlZp2HuRm8T7AbMi5kbnNjcnlwdC1jZXJ0LmV4YW1wbGUub3Jn
|
||||
```
|
||||
|
||||
### <a id="lookup"></a> Making lookups
|
||||
|
||||
You can use that stamp to send a DNSCrypt request to your server:
|
||||
|
||||
```
|
||||
./dnscrypt lookup-stamp \
|
||||
--stamp=sdns://AQcAAAAAAAAADTEyNy4wLjAuMTo0NDMg8R3bzEgX5UOEX93Uy4gYSbZCJvPeOXYlZp2HuRm8T7AbMi5kbnNjcnlwdC1jZXJ0LmV4YW1wbGUub3Jn \
|
||||
--domain=example.org \
|
||||
--type=a
|
||||
```
|
||||
|
||||
You can also send a DNSCrypt request using a command that does not require stamps:
|
||||
|
||||
```
|
||||
./dnscrypt lookup \
|
||||
--provider-name=2.dnscrypt-cert.opendns.com \
|
||||
--public-key=b7351140206f225d3e2bd822d7fd691ea1c33cc8d6668d0cbe04bfabca43fb79 \
|
||||
--addr=208.67.220.220 \
|
||||
--domain=example.org \
|
||||
--type=a
|
||||
```
|
||||
|
||||
## <a id="api"></a> Programming interface
|
||||
|
||||
### <a id="client"></a> Client
|
||||
|
||||
```go
|
||||
// AdGuard DNS stamp
|
||||
stampStr := "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
|
||||
|
||||
// Initializing the DNSCrypt client
|
||||
c := dnscrypt.Client{Net: "udp", Timeout: 10 * time.Second}
|
||||
|
||||
// Fetching and validating the server certificate
|
||||
resolverInfo, err := client.Dial(stampStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a DNS request
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
}
|
||||
|
||||
// Get the DNS response
|
||||
reply, err := c.Exchange(&req, resolverInfo)
|
||||
```
|
||||
|
||||
## <a id="server"></a> Server
|
||||
|
||||
```go
|
||||
// Prepare the test DNSCrypt server config
|
||||
rc, err := dnscrypt.GenerateResolverConfig("example.org", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert, err := rc.CreateCert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s := &dnscrypt.Server{
|
||||
ProviderName: rc.ProviderName,
|
||||
ResolverCert: cert,
|
||||
Handler: dnscrypt.DefaultHandler,
|
||||
}
|
||||
|
||||
// Prepare TCP listener
|
||||
tcpConn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4zero, Port: 443})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare UDP listener
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 443})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start the server
|
||||
go s.ServeUDP(udpConn)
|
||||
go s.ServeTCP(tcpConn)
|
||||
```
|
BIN
build/windows-386/dnscrypt.exe
Normal file
BIN
build/windows-386/dnscrypt.exe
Normal file
Binary file not shown.
176
cert.go
Normal file
176
cert.go
Normal file
|
@ -0,0 +1,176 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cert - DNSCrypt server certificate
|
||||
// See ResolverConfig for more info on how to create one
|
||||
type Cert struct {
|
||||
// Serial - a 4 byte serial number in big-endian format. If more than
|
||||
// one certificates are valid, the client must prefer the certificate
|
||||
// with a higher serial number.
|
||||
Serial uint32
|
||||
|
||||
// <es-version> ::= the cryptographic construction to use with this
|
||||
// certificate.
|
||||
// For X25519-XSalsa20Poly1305, <es-version> must be 0x00 0x01.
|
||||
// For X25519-XChacha20Poly1305, <es-version> must be 0x00 0x02.
|
||||
EsVersion CryptoConstruction
|
||||
|
||||
// Signature - a 64-byte signature of (<resolver-pk> <client-magic>
|
||||
// <serial> <ts-start> <ts-end> <extensions>) using the Ed25519 algorithm and the
|
||||
// provider secret key. Ed25519 must be used in this version of the
|
||||
// protocol.
|
||||
Signature [ed25519.SignatureSize]byte
|
||||
|
||||
// ResolverPk - the resolver's short-term public key, which is 32 bytes when using X25519.
|
||||
// This key is used to encrypt/decrypt DNS queries
|
||||
ResolverPk [keySize]byte
|
||||
|
||||
// ResolverSk - the resolver's short-term private key, which is 32 bytes when using X25519.
|
||||
// Note that it's only used in the server implementation and never serialized/deserialized.
|
||||
// This key is used to encrypt/decrypt DNS queries
|
||||
ResolverSk [keySize]byte
|
||||
|
||||
// ClientMagic - the first 8 bytes of a client query that is to be built
|
||||
// using the information from this certificate. It may be a truncated
|
||||
// public key. Two valid certificates cannot share the same <client-magic>.
|
||||
ClientMagic [clientMagicSize]byte
|
||||
|
||||
// NotAfter - the date the certificate is valid from, as a big-endian
|
||||
// 4-byte unsigned Unix timestamp.
|
||||
NotBefore uint32
|
||||
|
||||
// NotAfter - the date the certificate is valid until (inclusive), as a
|
||||
// big-endian 4-byte unsigned Unix timestamp.
|
||||
NotAfter uint32
|
||||
}
|
||||
|
||||
// Serialize - serializes the cert to bytes
|
||||
// <cert> ::= <cert-magic> <es-version> <protocol-minor-version> <signature>
|
||||
// <resolver-pk> <client-magic> <serial> <ts-start> <ts-end>
|
||||
// <extensions>
|
||||
// Certificates made of these information, without extensions, are 116 bytes
|
||||
// long. With the addition of the cert-magic, es-version and
|
||||
// protocol-minor-version, the record is 124 bytes long.
|
||||
func (c *Cert) Serialize() ([]byte, error) {
|
||||
// validate
|
||||
if c.EsVersion == UndefinedConstruction {
|
||||
return nil, ErrEsVersion
|
||||
}
|
||||
|
||||
if !c.VerifyDate() {
|
||||
return nil, ErrInvalidDate
|
||||
}
|
||||
|
||||
// start serializing
|
||||
b := make([]byte, 124)
|
||||
|
||||
// <cert-magic>
|
||||
copy(b[:4], certMagic[:])
|
||||
// <es-version>
|
||||
binary.BigEndian.PutUint16(b[4:6], uint16(c.EsVersion))
|
||||
// <protocol-minor-version> - always 0x00 0x00
|
||||
copy(b[6:8], []byte{0, 0})
|
||||
// <signature>
|
||||
copy(b[8:72], c.Signature[:ed25519.SignatureSize])
|
||||
// signed: (<resolver-pk> <client-magic> <serial> <ts-start> <ts-end> <extensions>)
|
||||
c.writeSigned(b[72:])
|
||||
|
||||
// done
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Deserialize - deserializes certificate from a byte array
|
||||
// <cert> ::= <cert-magic> <es-version> <protocol-minor-version> <signature>
|
||||
// <resolver-pk> <client-magic> <serial> <ts-start> <ts-end>
|
||||
// <extensions>
|
||||
func (c *Cert) Deserialize(b []byte) error {
|
||||
if len(b) < 124 {
|
||||
return ErrCertTooShort
|
||||
}
|
||||
|
||||
// <cert-magic>
|
||||
if !bytes.Equal(b[:4], certMagic[:4]) {
|
||||
return ErrCertMagic
|
||||
}
|
||||
|
||||
// <es-version>
|
||||
switch esVersion := binary.BigEndian.Uint16(b[4:6]); esVersion {
|
||||
case uint16(XSalsa20Poly1305):
|
||||
c.EsVersion = XSalsa20Poly1305
|
||||
case uint16(XChacha20Poly1305):
|
||||
c.EsVersion = XChacha20Poly1305
|
||||
default:
|
||||
return ErrEsVersion
|
||||
}
|
||||
|
||||
// Ignore 6:8, <protocol-minor-version>
|
||||
// <signature>
|
||||
copy(c.Signature[:], b[8:72])
|
||||
// <resolver-pk>
|
||||
copy(c.ResolverPk[:], b[72:104])
|
||||
// <client-magic>
|
||||
copy(c.ClientMagic[:], b[104:112])
|
||||
// <serial>
|
||||
c.Serial = binary.BigEndian.Uint32(b[112:116])
|
||||
// <ts-start> <ts-end>
|
||||
c.NotBefore = binary.BigEndian.Uint32(b[116:120])
|
||||
c.NotAfter = binary.BigEndian.Uint32(b[120:124])
|
||||
|
||||
// Deserialized with no issues
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyDate - checks that cert is valid at this moment
|
||||
func (c *Cert) VerifyDate() bool {
|
||||
if c.NotBefore >= c.NotAfter {
|
||||
return false
|
||||
}
|
||||
now := uint32(time.Now().Unix())
|
||||
if now > c.NotAfter || now < c.NotBefore {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// VerifySignature - checks if the cert is properly signed with the specified signature
|
||||
func (c *Cert) VerifySignature(publicKey ed25519.PublicKey) bool {
|
||||
b := make([]byte, 52)
|
||||
c.writeSigned(b)
|
||||
return ed25519.Verify(publicKey, b, c.Signature[:])
|
||||
}
|
||||
|
||||
// Sign - creates cert.Signature
|
||||
func (c *Cert) Sign(privateKey ed25519.PrivateKey) {
|
||||
b := make([]byte, 52)
|
||||
c.writeSigned(b)
|
||||
signature := ed25519.Sign(privateKey, b)
|
||||
copy(c.Signature[:64], signature[:64])
|
||||
}
|
||||
|
||||
// String - Cert's string representation
|
||||
func (c *Cert) String() string {
|
||||
return fmt.Sprintf("Certificate Serial=%d NotBefore=%s NotAfter=%s EsVersion=%s",
|
||||
c.Serial, time.Unix(int64(c.NotBefore), 0).String(),
|
||||
time.Unix(int64(c.NotAfter), 0).String(), c.EsVersion.String())
|
||||
}
|
||||
|
||||
// writeSigned - writes (<resolver-pk> <client-magic> <serial> <ts-start> <ts-end> <extensions>)
|
||||
func (c *Cert) writeSigned(dst []byte) {
|
||||
// <resolver-pk>
|
||||
copy(dst[:32], c.ResolverPk[:keySize])
|
||||
// <client-magic>
|
||||
copy(dst[32:40], c.ClientMagic[:clientMagicSize])
|
||||
// <serial>
|
||||
binary.BigEndian.PutUint32(dst[40:44], c.Serial)
|
||||
// <ts-start>
|
||||
binary.BigEndian.PutUint32(dst[44:48], c.NotBefore)
|
||||
// <ts-end>
|
||||
binary.BigEndian.PutUint32(dst[48:52], c.NotAfter)
|
||||
}
|
78
cert_test.go
Normal file
78
cert_test.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCertSerialize(t *testing.T) {
|
||||
cert, publicKey, _ := generateValidCert(t)
|
||||
|
||||
// not empty anymore
|
||||
assert.False(t, bytes.Equal(cert.Signature[:], make([]byte, 64)))
|
||||
|
||||
// verify the signature
|
||||
assert.True(t, cert.VerifySignature(publicKey))
|
||||
|
||||
// serialize
|
||||
b, err := cert.Serialize()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 124, len(b))
|
||||
|
||||
// check that we can deserialize it
|
||||
cert2 := Cert{}
|
||||
err = cert2.Deserialize(b)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, cert.Serial, cert2.Serial)
|
||||
assert.Equal(t, cert.NotBefore, cert2.NotBefore)
|
||||
assert.Equal(t, cert.NotAfter, cert2.NotAfter)
|
||||
assert.Equal(t, cert.EsVersion, cert2.EsVersion)
|
||||
assert.True(t, bytes.Equal(cert.ClientMagic[:], cert2.ClientMagic[:]))
|
||||
assert.True(t, bytes.Equal(cert.ResolverPk[:], cert2.ResolverPk[:]))
|
||||
assert.True(t, bytes.Equal(cert.Signature[:], cert2.Signature[:]))
|
||||
}
|
||||
|
||||
func TestCertDeserialize(t *testing.T) {
|
||||
// dig -t txt 2.dnscrypt-cert.opendns.com. -p 443 @208.67.220.220
|
||||
b, err := unpackTxtString("DNSC\\000\\001\\000\\000\\200\\226E:H\\156\\203%\\134\\218\\127]\\168\\239\\027u\\011$\\191\\008\\239\\176F\\133\\017\\171\\161\\219\\154\\142i\\164\\010\\239\\017f\\168dS\\210f\\197\\194\\169\\171w\\2499\\1891\\155<\\130\\218@/\\155\\023v\\153#d\\024\\004\\136\\180\\228K5\\233d\\180\\144\\189\\218\\186\\232%\\162K\\004\\021\\160\\139\\225\\157}\\219\\135\\163<\\215~\\223\\142/qc78aWoo]\\221\\184`]\\221\\184`_\\190\\235\\224")
|
||||
assert.Nil(t, err)
|
||||
|
||||
cert := &Cert{}
|
||||
err = cert.Deserialize(b)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, uint32(1574811744), cert.Serial)
|
||||
assert.Equal(t, XSalsa20Poly1305, cert.EsVersion)
|
||||
assert.Equal(t, uint32(1574811744), cert.NotBefore)
|
||||
assert.Equal(t, uint32(1606347744), cert.NotAfter)
|
||||
}
|
||||
|
||||
func generateValidCert(t *testing.T) (*Cert, ed25519.PublicKey, ed25519.PrivateKey) {
|
||||
cert := &Cert{
|
||||
Serial: 1,
|
||||
NotAfter: uint32(time.Now().Add(1 * time.Hour).Unix()),
|
||||
NotBefore: uint32(time.Now().Add(-1 * time.Hour).Unix()),
|
||||
EsVersion: XChacha20Poly1305,
|
||||
}
|
||||
|
||||
// generate short-term resolver private key
|
||||
resolverSk, resolverPk := generateRandomKeyPair()
|
||||
copy(cert.ResolverPk[:], resolverPk[:])
|
||||
copy(cert.ResolverSk[:], resolverSk[:])
|
||||
|
||||
// empty at first
|
||||
assert.True(t, bytes.Equal(cert.Signature[:], make([]byte, 64)))
|
||||
|
||||
// generate private key
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// sign the data
|
||||
cert.Sign(privateKey)
|
||||
|
||||
return cert, publicKey, privateKey
|
||||
}
|
286
client.go
Normal file
286
client.go
Normal file
|
@ -0,0 +1,286 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/ameshkov/dnsstamps"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Client - DNSCrypt resolver client
|
||||
type Client struct {
|
||||
Net string // protocol (can be "udp" or "tcp", by default - "udp")
|
||||
Timeout time.Duration // read/write timeout
|
||||
}
|
||||
|
||||
// ResolverInfo contains DNSCrypt resolver information necessary for decryption/encryption
|
||||
type ResolverInfo struct {
|
||||
SecretKey [keySize]byte // Client short-term secret key
|
||||
PublicKey [keySize]byte // Client short-term public key
|
||||
|
||||
ServerPublicKey ed25519.PublicKey // Resolver public key (this key is used to validate cert signature)
|
||||
ServerAddress string // Server IP address
|
||||
ProviderName string // Provider name
|
||||
|
||||
ResolverCert *Cert // Certificate info (obtained with the first unencrypted DNS request)
|
||||
SharedKey [keySize]byte // Shared key that is to be used to encrypt/decrypt messages
|
||||
}
|
||||
|
||||
// Dial fetches and validates DNSCrypt certificate from the given server
|
||||
// Data received during this call is then used for DNS requests encryption/decryption
|
||||
// stampStr is an sdns:// address which is parsed using go-dnsstamps package
|
||||
func (c *Client) Dial(stampStr string) (*ResolverInfo, error) {
|
||||
stamp, err := dnsstamps.NewServerStampFromString(stampStr)
|
||||
if err != nil {
|
||||
// Invalid SDNS stamp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stamp.Proto != dnsstamps.StampProtoTypeDNSCrypt {
|
||||
return nil, ErrInvalidDNSStamp
|
||||
}
|
||||
|
||||
return c.DialStamp(stamp)
|
||||
}
|
||||
|
||||
// DialStamp fetches and validates DNSCrypt certificate from the given server
|
||||
// Data received during this call is then used for DNS requests encryption/decryption
|
||||
func (c *Client) DialStamp(stamp dnsstamps.ServerStamp) (*ResolverInfo, error) {
|
||||
resolverInfo := &ResolverInfo{}
|
||||
|
||||
// Generate the secret/public pair
|
||||
resolverInfo.SecretKey, resolverInfo.PublicKey = generateRandomKeyPair()
|
||||
|
||||
// Set the provider properties
|
||||
resolverInfo.ServerPublicKey = stamp.ServerPk
|
||||
resolverInfo.ServerAddress = stamp.ServerAddrStr
|
||||
resolverInfo.ProviderName = stamp.ProviderName
|
||||
|
||||
cert, err := c.fetchCert(stamp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolverInfo.ResolverCert = cert
|
||||
|
||||
// Compute shared key that we'll use to encrypt/decrypt messages
|
||||
sharedKey, err := computeSharedKey(cert.EsVersion, &resolverInfo.SecretKey, &cert.ResolverPk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolverInfo.SharedKey = sharedKey
|
||||
return resolverInfo, nil
|
||||
}
|
||||
|
||||
// Exchange performs a synchronous DNS query to the specified DNSCrypt server and returns a DNS response.
|
||||
// This method creates a new network connection for every call so avoid using it for TCP.
|
||||
// DNSCrypt cert needs to be fetched and validated prior to this call using the c.DialStamp method.
|
||||
func (c *Client) Exchange(m *dns.Msg, resolverInfo *ResolverInfo) (*dns.Msg, error) {
|
||||
network := "udp"
|
||||
if c.Net == "tcp" {
|
||||
network = "tcp"
|
||||
}
|
||||
|
||||
conn, err := net.Dial(network, resolverInfo.ServerAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
r, err := c.ExchangeConn(conn, m, resolverInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ExchangeConn performs a synchronous DNS query to the specified DNSCrypt server and returns a DNS response.
|
||||
// DNSCrypt server information needs to be fetched and validated prior to this call using the c.DialStamp method
|
||||
func (c *Client) ExchangeConn(conn net.Conn, m *dns.Msg, resolverInfo *ResolverInfo) (*dns.Msg, error) {
|
||||
query, err := c.encrypt(m, resolverInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.writeQuery(conn, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := c.readResponse(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := c.decrypt(b, resolverInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// writeQuery - writes query to the network connection
|
||||
// depending on the protocol we may write a 2-byte prefix or not
|
||||
func (c *Client) writeQuery(conn net.Conn, query []byte) error {
|
||||
var err error
|
||||
|
||||
if c.Timeout > 0 {
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(c.Timeout))
|
||||
}
|
||||
|
||||
// Write to the connection
|
||||
if _, ok := conn.(*net.TCPConn); ok {
|
||||
l := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(l, uint16(len(query)))
|
||||
_, err = (&net.Buffers{l, query}).WriteTo(conn)
|
||||
} else {
|
||||
_, err = conn.Write(query)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// readResponse - reads response from the network connection
|
||||
// depending on the protocol, we may read a 2-byte prefix or not
|
||||
func (c *Client) readResponse(conn net.Conn) ([]byte, error) {
|
||||
if c.Timeout > 0 {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(c.Timeout))
|
||||
}
|
||||
|
||||
proto := "udp"
|
||||
if _, ok := conn.(*net.TCPConn); ok {
|
||||
proto = "tcp"
|
||||
}
|
||||
|
||||
if proto == "udp" {
|
||||
response := make([]byte, maxQueryLen)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response[:n], nil
|
||||
}
|
||||
|
||||
// If we got here, this is a TCP connection
|
||||
// so we should read a 2-byte prefix first
|
||||
return readPrefixed(conn)
|
||||
}
|
||||
|
||||
// encrypt - encrypts a DNS message using shared key from the resolver info
|
||||
func (c *Client) encrypt(m *dns.Msg, resolverInfo *ResolverInfo) ([]byte, error) {
|
||||
q := EncryptedQuery{
|
||||
EsVersion: resolverInfo.ResolverCert.EsVersion,
|
||||
ClientMagic: resolverInfo.ResolverCert.ClientMagic,
|
||||
ClientPk: resolverInfo.PublicKey,
|
||||
}
|
||||
query, err := m.Pack()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.Encrypt(query, resolverInfo.SharedKey)
|
||||
}
|
||||
|
||||
// decrypts - decrypts a DNS message using shared key from the resolver info
|
||||
func (c *Client) decrypt(b []byte, resolverInfo *ResolverInfo) (*dns.Msg, error) {
|
||||
dr := EncryptedResponse{
|
||||
EsVersion: resolverInfo.ResolverCert.EsVersion,
|
||||
}
|
||||
msg, err := dr.Decrypt(b, resolverInfo.SharedKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := new(dns.Msg)
|
||||
err = res.Unpack(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// fetchCert - loads DNSCrypt cert from the specified server
|
||||
func (c *Client) fetchCert(stamp dnsstamps.ServerStamp) (*Cert, error) {
|
||||
providerName := stamp.ProviderName
|
||||
if !strings.HasSuffix(providerName, ".") {
|
||||
providerName = providerName + "."
|
||||
}
|
||||
|
||||
query := new(dns.Msg)
|
||||
query.SetQuestion(providerName, dns.TypeTXT)
|
||||
client := dns.Client{Net: c.Net, UDPSize: uint16(maxQueryLen), Timeout: c.Timeout}
|
||||
r, _, err := client.Exchange(query, stamp.ServerAddrStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
return nil, ErrFailedToFetchCert
|
||||
}
|
||||
|
||||
var certErr error
|
||||
currentCert := &Cert{}
|
||||
foundValid := false
|
||||
|
||||
for _, rr := range r.Answer {
|
||||
txt, ok := rr.(*dns.TXT)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var b []byte
|
||||
b, certErr = unpackTxtString(strings.Join(txt.Txt, ""))
|
||||
if certErr != nil {
|
||||
log.Debug("[%s] failed to pack TXT record: %v", providerName, certErr)
|
||||
continue
|
||||
}
|
||||
|
||||
cert := &Cert{}
|
||||
certErr = cert.Deserialize(b)
|
||||
if certErr != nil {
|
||||
log.Debug("[%s] failed to deserialize cert: %v", providerName, certErr)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debug("[%s] fetched certificate %d", providerName, cert.Serial)
|
||||
|
||||
if !cert.VerifyDate() {
|
||||
certErr = ErrInvalidDate
|
||||
log.Debug("[%s] cert %d date is not valid", providerName, cert.Serial)
|
||||
continue
|
||||
}
|
||||
|
||||
if !cert.VerifySignature(stamp.ServerPk) {
|
||||
certErr = ErrInvalidCertSignature
|
||||
log.Debug("[%s] cert %d signature is not valid", providerName, cert.Serial)
|
||||
continue
|
||||
}
|
||||
|
||||
if cert.Serial < currentCert.Serial {
|
||||
log.Debug("[%v] cert %d superseded by a previous certificate", providerName, cert.Serial)
|
||||
continue
|
||||
}
|
||||
|
||||
if cert.Serial == currentCert.Serial {
|
||||
if cert.EsVersion > currentCert.EsVersion {
|
||||
log.Debug("[%v] Upgrading the construction from %v to %v", providerName, currentCert.EsVersion, cert.EsVersion)
|
||||
} else {
|
||||
log.Debug("[%v] Keeping the previous, preferred crypto construction", providerName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Setting the cert
|
||||
currentCert = cert
|
||||
foundValid = true
|
||||
}
|
||||
|
||||
if foundValid {
|
||||
return currentCert, nil
|
||||
}
|
||||
|
||||
return nil, certErr
|
||||
}
|
186
client_test.go
Normal file
186
client_test.go
Normal file
|
@ -0,0 +1,186 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ameshkov/dnsstamps"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseStamp(t *testing.T) {
|
||||
// Google DoH
|
||||
stampStr := "sdns://AgUAAAAAAAAAAAAOZG5zLmdvb2dsZS5jb20NL2V4cGVyaW1lbnRhbA"
|
||||
stamp, err := dnsstamps.NewServerStampFromString(stampStr)
|
||||
|
||||
if err != nil || stamp.ProviderName == "" {
|
||||
t.Fatalf("Could not parse stamp %s: %s", stampStr, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, stampStr, stamp.String())
|
||||
assert.Equal(t, dnsstamps.StampProtoTypeDoH, stamp.Proto)
|
||||
assert.Equal(t, "dns.google.com", stamp.ProviderName)
|
||||
assert.Equal(t, "/experimental", stamp.Path)
|
||||
|
||||
// AdGuard DNSCrypt
|
||||
stampStr = "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
|
||||
stamp, err = dnsstamps.NewServerStampFromString(stampStr)
|
||||
|
||||
if err != nil || stamp.ProviderName == "" {
|
||||
t.Fatalf("Could not parse stamp %s: %s", stampStr, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, stampStr, stamp.String())
|
||||
assert.Equal(t, dnsstamps.StampProtoTypeDNSCrypt, stamp.Proto)
|
||||
assert.Equal(t, "2.dnscrypt.default.ns1.adguard.com", stamp.ProviderName)
|
||||
assert.Equal(t, "", stamp.Path)
|
||||
assert.Equal(t, "176.103.130.130:5443", stamp.ServerAddrStr)
|
||||
assert.Equal(t, keySize, len(stamp.ServerPk))
|
||||
}
|
||||
|
||||
func TestInvalidStamp(t *testing.T) {
|
||||
client := Client{}
|
||||
_, err := client.Dial("sdns://AQIAAAAAAAAAFDE")
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestTimeoutOnDialError(t *testing.T) {
|
||||
// AdGuard DNS pointing to a wrong IP
|
||||
stampStr := "sdns://AQIAAAAAAAAADDguOC44Ljg6NTQ0MyDRK0fyUtzywrv4mRCG6vec5EldixbIoMQyLlLKPzkIcyIyLmRuc2NyeXB0LmRlZmF1bHQubnMxLmFkZ3VhcmQuY29t"
|
||||
client := Client{Timeout: 300 * time.Millisecond}
|
||||
|
||||
_, err := client.Dial(stampStr)
|
||||
assert.NotNil(t, err)
|
||||
assert.True(t, os.IsTimeout(err))
|
||||
}
|
||||
|
||||
func TestTimeoutOnDialExchange(t *testing.T) {
|
||||
// AdGuard DNS
|
||||
stampStr := "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
|
||||
client := Client{Timeout: 300 * time.Millisecond}
|
||||
|
||||
serverInfo, err := client.Dial(stampStr)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Point it to an IP where there's no DNSCrypt server
|
||||
serverInfo.ServerAddress = "8.8.8.8:5443"
|
||||
req := createTestMessage()
|
||||
|
||||
// Do exchange
|
||||
_, err = client.Exchange(req, serverInfo)
|
||||
|
||||
// Check error
|
||||
assert.NotNil(t, err)
|
||||
assert.True(t, os.IsTimeout(err))
|
||||
}
|
||||
|
||||
func TestFetchCertPublicResolvers(t *testing.T) {
|
||||
stamps := []struct {
|
||||
stampStr string
|
||||
}{
|
||||
{
|
||||
// AdGuard DNS
|
||||
stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
|
||||
},
|
||||
{
|
||||
// AdGuard DNS Family
|
||||
stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMjo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
|
||||
},
|
||||
{
|
||||
// AdGuard DNS Unfiltered
|
||||
stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzNjo1NDQzILXoRNa4Oj4-EmjraB--pw3jxfpo29aIFB2_LsBmstr6JTIuZG5zY3J5cHQudW5maWx0ZXJlZC5uczEuYWRndWFyZC5jb20",
|
||||
},
|
||||
{
|
||||
// Cisco OpenDNS
|
||||
stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
|
||||
},
|
||||
{
|
||||
// Cisco OpenDNS Family Shield
|
||||
stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMTIzILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range stamps {
|
||||
stamp, err := dnsstamps.NewServerStampFromString(test.stampStr)
|
||||
assert.Nil(t, err)
|
||||
|
||||
t.Run(stamp.ProviderName, func(t *testing.T) {
|
||||
c := &Client{Net: "udp"}
|
||||
resolverInfo, err := c.DialStamp(stamp)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resolverInfo)
|
||||
assert.True(t, resolverInfo.ResolverCert.VerifyDate())
|
||||
assert.True(t, resolverInfo.ResolverCert.VerifySignature(stamp.ServerPk))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangePublicResolvers(t *testing.T) {
|
||||
stamps := []struct {
|
||||
stampStr string
|
||||
}{
|
||||
{
|
||||
// AdGuard DNS
|
||||
stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
|
||||
},
|
||||
{
|
||||
// AdGuard DNS Family
|
||||
stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMjo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
|
||||
},
|
||||
{
|
||||
// AdGuard DNS Unfiltered
|
||||
stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzNjo1NDQzILXoRNa4Oj4-EmjraB--pw3jxfpo29aIFB2_LsBmstr6JTIuZG5zY3J5cHQudW5maWx0ZXJlZC5uczEuYWRndWFyZC5jb20",
|
||||
},
|
||||
{
|
||||
// Cisco OpenDNS
|
||||
stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
|
||||
},
|
||||
{
|
||||
// Cisco OpenDNS Family Shield
|
||||
stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMTIzILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range stamps {
|
||||
stamp, err := dnsstamps.NewServerStampFromString(test.stampStr)
|
||||
assert.Nil(t, err)
|
||||
|
||||
t.Run(stamp.ProviderName, func(t *testing.T) {
|
||||
checkDNSCryptServer(t, test.stampStr, "udp")
|
||||
checkDNSCryptServer(t, test.stampStr, "tcp")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkDNSCryptServer(t *testing.T, stampStr string, network string) {
|
||||
client := Client{Net: network, Timeout: 10 * time.Second}
|
||||
resolverInfo, err := client.Dial(stampStr)
|
||||
assert.Nil(t, err)
|
||||
|
||||
req := createTestMessage()
|
||||
|
||||
reply, err := client.Exchange(req, resolverInfo)
|
||||
assert.Nil(t, err)
|
||||
assertTestMessageResponse(t, reply)
|
||||
}
|
||||
|
||||
func createTestMessage() *dns.Msg {
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
}
|
||||
return &req
|
||||
}
|
||||
|
||||
func assertTestMessageResponse(t *testing.T, reply *dns.Msg) {
|
||||
assert.NotNil(t, reply)
|
||||
assert.Equal(t, 1, len(reply.Answer))
|
||||
a, ok := reply.Answer[0].(*dns.A)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, net.IPv4(8, 8, 8, 8).To4(), a.A.To4())
|
||||
}
|
50
cmd/generate.go
Normal file
50
cmd/generate.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/ameshkov/dnscrypt"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// GenerateArgs - "generate" command arguments
|
||||
type GenerateArgs struct {
|
||||
ProviderName string `short:"p" long:"provider-name" description:"DNSCrypt provider name" required:"true"`
|
||||
PrivateKey string `short:"k" long:"private-key" description:"Private key (hex-encoded)"`
|
||||
CertificateTTL int `short:"t" long:"ttl" description:"Certificate time-to-live (seconds)"`
|
||||
Out string `short:"o" long:"out" description:"Path to the resulting config file" required:"true"`
|
||||
}
|
||||
|
||||
// generate - generates DNSCrypt server configuration
|
||||
func generate(args GenerateArgs) {
|
||||
log.Info("Generating configuration for %s", args.ProviderName)
|
||||
|
||||
var privateKey []byte
|
||||
var err error
|
||||
if args.PrivateKey != "" {
|
||||
privateKey, err = dnscrypt.HexDecodeKey(args.PrivateKey)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to generate private key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
rc, err := dnscrypt.GenerateResolverConfig(args.ProviderName, privateKey)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to generate resolver config: %v", err)
|
||||
}
|
||||
|
||||
b, err := yaml.Marshal(rc)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to serialize to yaml: %v", err)
|
||||
}
|
||||
|
||||
// nolint
|
||||
err = ioutil.WriteFile(args.Out, b, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to save %s: %v", args.Out, err)
|
||||
}
|
||||
|
||||
log.Info("Configuration has been written to %s", args.Out)
|
||||
log.Info("Go to https://dnscrypt.info/stamps to generate an SDNS stamp")
|
||||
}
|
110
cmd/lookup.go
Normal file
110
cmd/lookup.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/ameshkov/dnscrypt"
|
||||
"github.com/ameshkov/dnsstamps"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// LookupStampArgs - "lookup-stamp" command arguments
|
||||
type LookupStampArgs struct {
|
||||
Stamp string `short:"s" long:"stamp" description:"DNSCrypt resolver stamp" required:"true"`
|
||||
Domain string `short:"d" long:"domain" description:"Domain to resolve" required:"true"`
|
||||
Type string `short:"t" long:"type" description:"DNS query type" default:"A"`
|
||||
}
|
||||
|
||||
// LookupArgs - "lookup" command arguments
|
||||
type LookupArgs struct {
|
||||
ProviderName string `short:"p" long:"provider-name" description:"DNSCrypt resolver provider name" required:"true"`
|
||||
PublicKey string `short:"k" long:"public-key" description:"DNSCrypt resolver public key" required:"true"`
|
||||
ServerAddr string `short:"a" long:"addr" description:"Resolver address (IP[:port]). By default, the port is 443" required:"true"`
|
||||
Domain string `short:"d" long:"domain" description:"Domain to resolve" required:"true"`
|
||||
Type string `short:"t" long:"type" description:"DNS query type" default:"A"`
|
||||
}
|
||||
|
||||
// LookupResult - lookup result that contains the cert info and the query response
|
||||
type LookupResult struct {
|
||||
Certificate struct {
|
||||
Serial uint32 `json:"serial"`
|
||||
EsVersion string `json:"encryption"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
NotBefore time.Time `json:"not_before"`
|
||||
} `json:"certificate"`
|
||||
|
||||
Reply *dns.Msg `json:"reply"`
|
||||
}
|
||||
|
||||
// lookup - performs a DNS lookup, prints DNSCrypt info and lookup results
|
||||
func lookup(args LookupArgs) {
|
||||
serverPk, err := dnscrypt.HexDecodeKey(args.PublicKey)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid resolver public key: %v", err)
|
||||
}
|
||||
|
||||
stamp := dnsstamps.ServerStamp{
|
||||
ProviderName: args.ProviderName,
|
||||
ServerPk: serverPk,
|
||||
ServerAddrStr: args.ServerAddr,
|
||||
Proto: dnsstamps.StampProtoTypeDNSCrypt,
|
||||
}
|
||||
|
||||
lookupStamp(LookupStampArgs{
|
||||
Stamp: stamp.String(),
|
||||
Domain: args.Domain,
|
||||
Type: args.Type,
|
||||
})
|
||||
}
|
||||
|
||||
// lookupStamp - performs a DNS lookup, prints DNSCrypt cert info and lookup results
|
||||
func lookupStamp(args LookupStampArgs) {
|
||||
c := &dnscrypt.Client{
|
||||
Net: "udp",
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
ri, err := c.Dial(args.Stamp)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("failed to establish connection with the server: %v", err)
|
||||
}
|
||||
|
||||
res := LookupResult{}
|
||||
res.Certificate.Serial = ri.ResolverCert.Serial
|
||||
res.Certificate.NotAfter = time.Unix(int64(ri.ResolverCert.NotAfter), 0)
|
||||
res.Certificate.NotBefore = time.Unix(int64(ri.ResolverCert.NotBefore), 0)
|
||||
res.Certificate.EsVersion = ri.ResolverCert.EsVersion.String()
|
||||
|
||||
dnsType, ok := dns.StringToType[strings.ToUpper(args.Type)]
|
||||
if !ok {
|
||||
log.Fatalf("invalid type %s", args.Type)
|
||||
}
|
||||
|
||||
req := &dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{
|
||||
Name: dns.Fqdn(args.Domain),
|
||||
Qtype: dnsType,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
}
|
||||
|
||||
reply, err := c.Exchange(req, ri)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to resolve %s %s", args.Type, args.Domain)
|
||||
}
|
||||
|
||||
res.Reply = reply
|
||||
b, err := json.MarshalIndent(res, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to marshal result to json: %v", err)
|
||||
}
|
||||
|
||||
_, _ = os.Stdout.WriteString(string(b) + "\n")
|
||||
}
|
51
cmd/main.go
Normal file
51
cmd/main.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
|
||||
goFlags "github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
// Options - command-line options
|
||||
type Options struct {
|
||||
Generate GenerateArgs `command:"generate" description:"Generates DNSCrypt server configuration"`
|
||||
LookupStamp LookupStampArgs `command:"lookup-stamp" description:"Performs a DNSCrypt lookup for the specified domain using an sdns:// stamp"`
|
||||
Lookup LookupArgs `command:"lookup" description:"Performs a DNSCrypt lookup for the specified domain"`
|
||||
Server ServerArgs `command:"server" description:"Runs a DNSCrypt resolver"`
|
||||
Version struct {
|
||||
} `command:"version" description:"Prints version"`
|
||||
}
|
||||
|
||||
// VersionString will be set through ldflags, contains current version
|
||||
var VersionString = "1.0"
|
||||
|
||||
func main() {
|
||||
var opts Options
|
||||
|
||||
var parser = goFlags.NewParser(&opts, goFlags.Default)
|
||||
_, err := parser.Parse()
|
||||
if err != nil {
|
||||
if flagsErr, ok := err.(*goFlags.Error); ok && flagsErr.Type == goFlags.ErrHelp {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
switch parser.Active.Name {
|
||||
case "version":
|
||||
log.Printf("dnscrypt version %s\n", VersionString)
|
||||
case "generate":
|
||||
generate(opts.Generate)
|
||||
case "lookup-stamp":
|
||||
lookupStamp(opts.LookupStamp)
|
||||
case "lookup":
|
||||
lookup(opts.Lookup)
|
||||
case "server":
|
||||
server(opts.Server)
|
||||
default:
|
||||
log.Fatalf("unknown command %s", parser.Active.Name)
|
||||
}
|
||||
}
|
111
cmd/server.go
Normal file
111
cmd/server.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/ameshkov/dnscrypt"
|
||||
"github.com/miekg/dns"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ServerArgs - "server" command arguments
|
||||
type ServerArgs struct {
|
||||
Config string `short:"c" long:"config" description:"Path to the DNSCrypt configuration file" required:"true"`
|
||||
Forward string `short:"f" long:"forward" description:"Forwards DNS queries to the specified address" default:"94.140.14.140:53"`
|
||||
ListenAddrs []string `short:"l" long:"listen" description:"Listening addresses" default:"0.0.0.0"`
|
||||
ListenPorts []int `short:"p" long:"port" description:"Listening ports" default:"443"`
|
||||
}
|
||||
|
||||
// server - runs a DNSCrypt server
|
||||
func server(args ServerArgs) {
|
||||
log.Info("Starting DNSCrypt server")
|
||||
|
||||
b, err := ioutil.ReadFile(args.Config)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to read the configuration: %v", err)
|
||||
}
|
||||
|
||||
rc := dnscrypt.ResolverConfig{}
|
||||
err = yaml.Unmarshal(b, &rc)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to deserialize configuration: %v", err)
|
||||
}
|
||||
|
||||
cert, err := rc.CreateCert()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to generate certificate: %v", err)
|
||||
}
|
||||
|
||||
s := &dnscrypt.Server{
|
||||
ProviderName: rc.ProviderName,
|
||||
ResolverCert: cert,
|
||||
Handler: &forwardHandler{addr: args.Forward},
|
||||
}
|
||||
|
||||
tcp, udp := createListeners(args)
|
||||
for _, t := range tcp {
|
||||
log.Info("Listening to tcp://%s", t.Addr().String())
|
||||
listen := t
|
||||
go func() { _ = s.ServeTCP(listen) }()
|
||||
}
|
||||
for _, u := range udp {
|
||||
log.Info("Listening to udp://%s", u.LocalAddr().String())
|
||||
listen := u
|
||||
go func() { _ = s.ServeUDP(listen) }()
|
||||
}
|
||||
|
||||
signalChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-signalChannel
|
||||
|
||||
log.Info("Closing all listeners")
|
||||
for _, t := range tcp {
|
||||
_ = t.Close()
|
||||
}
|
||||
for _, u := range udp {
|
||||
_ = u.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// createListeners - creates listeners for our server
|
||||
func createListeners(args ServerArgs) (tcp []net.Listener, udp []*net.UDPConn) {
|
||||
for _, addr := range args.ListenAddrs {
|
||||
ip := net.ParseIP(addr)
|
||||
if ip == nil {
|
||||
log.Fatalf("invalid listen address: %s", addr)
|
||||
}
|
||||
|
||||
for _, port := range args.ListenPorts {
|
||||
tcpListen, err := net.ListenTCP("tcp", &net.TCPAddr{IP: ip, Port: port})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to start TCP listener: %v", err)
|
||||
}
|
||||
udpListen, err := net.ListenUDP("udp", &net.UDPAddr{IP: ip, Port: port})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to start UDP listener: %v", err)
|
||||
}
|
||||
tcp = append(tcp, tcpListen)
|
||||
udp = append(udp, udpListen)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type forwardHandler struct {
|
||||
addr string
|
||||
}
|
||||
|
||||
// ServeDNS - implements Handler interface
|
||||
func (f *forwardHandler) ServeDNS(rw dnscrypt.ResponseWriter, r *dns.Msg) error {
|
||||
res, err := dns.Exchange(r, f.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rw.WriteMsg(res)
|
||||
}
|
113
constants.go
Normal file
113
constants.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package dnscrypt
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrTooShort - DNS query is shorter than possible
|
||||
ErrTooShort = errors.New("DNSCrypt message is too short")
|
||||
|
||||
// ErrQueryTooLarge - DNS query is larger than max allowed size
|
||||
ErrQueryTooLarge = errors.New("DNSCrypt query is too large")
|
||||
|
||||
// ErrEsVersion - cert contains unsupported es-version
|
||||
ErrEsVersion = errors.New("unsupported es-version")
|
||||
|
||||
// ErrInvalidDate - cert is not valid for the current time
|
||||
ErrInvalidDate = errors.New("cert has invalid ts-start or ts-end")
|
||||
|
||||
// ErrInvalidCertSignature - cert has invalid signature
|
||||
ErrInvalidCertSignature = errors.New("cert has invalid signature")
|
||||
|
||||
// ErrInvalidQuery - failed to decrypt a DNSCrypt query
|
||||
ErrInvalidQuery = errors.New("DNSCrypt query is invalid and cannot be decrypted")
|
||||
|
||||
// ErrInvalidClientMagic - client-magic does not match
|
||||
ErrInvalidClientMagic = errors.New("DNSCrypt query contains invalid client magic")
|
||||
|
||||
// ErrInvalidResolverMagic - server-magic does not match
|
||||
ErrInvalidResolverMagic = errors.New("DNSCrypt response contains invalid resolver magic")
|
||||
|
||||
// ErrInvalidResponse - failed to decrypt a DNSCrypt response
|
||||
ErrInvalidResponse = errors.New("DNSCrypt response is invalid and cannot be decrypted")
|
||||
|
||||
// ErrInvalidPadding - failed to unpad a query
|
||||
ErrInvalidPadding = errors.New("invalid padding")
|
||||
|
||||
// ErrInvalidDNSStamp - invalid DNS stamp
|
||||
ErrInvalidDNSStamp = errors.New("invalid DNS stamp")
|
||||
|
||||
// ErrFailedToFetchCert - failed to fetch DNSCrypt certificate
|
||||
ErrFailedToFetchCert = errors.New("failed to fetch DNSCrypt certificate")
|
||||
|
||||
// ErrCertTooShort - failed to deserialize cert, too short
|
||||
ErrCertTooShort = errors.New("cert is too short")
|
||||
|
||||
// ErrCertMagic - invalid cert magic
|
||||
ErrCertMagic = errors.New("invalid cert magic")
|
||||
|
||||
// ErrServerConfig - failed to start the DNSCrypt server - invalid configuration
|
||||
ErrServerConfig = errors.New("invalid server configuration")
|
||||
)
|
||||
|
||||
const (
|
||||
// <min-query-len> is a variable length, initially set to 256 bytes, and
|
||||
// must be a multiple of 64 bytes. (see https://dnscrypt.info/protocol)
|
||||
// Some servers do not work if padded length is less than 256. Example: Quad9
|
||||
minUDPQuestionSize = 256
|
||||
|
||||
// <max-query-len> - maximum allowed query length
|
||||
maxQueryLen = 1252
|
||||
|
||||
// Minimum possible DNS packet size
|
||||
minDNSPacketSize = 12 + 5
|
||||
|
||||
// See 11. Authenticated encryption and key exchange algorithm
|
||||
// The public and secret keys are 32 bytes long in storage
|
||||
keySize = 32
|
||||
|
||||
// size of the shared key used to encrypt/decrypt messages
|
||||
sharedKeySize = 32
|
||||
|
||||
// ClientMagic - the first 8 bytes of a client query that is to be built
|
||||
// using the information from this certificate. It may be a truncated
|
||||
// public key. Two valid certificates cannot share the same <client-magic>.
|
||||
clientMagicSize = 8
|
||||
|
||||
// When using X25519-XSalsa20Poly1305, this construction requires a 24 bytes
|
||||
// nonce, that must not be reused for a given shared secret.
|
||||
nonceSize = 24
|
||||
|
||||
// the first 8 bytes of every dnscrypt response. must match resolverMagic.
|
||||
resolverMagicSize = 8
|
||||
)
|
||||
|
||||
var (
|
||||
// certMagic - bytes sequence that must be in the beginning of the serialized cert
|
||||
certMagic = [4]byte{0x44, 0x4e, 0x53, 0x43}
|
||||
|
||||
// resolverMagic - byte sequence that must be in the beginning of every response
|
||||
resolverMagic = []byte{0x72, 0x36, 0x66, 0x6e, 0x76, 0x57, 0x6a, 0x38}
|
||||
)
|
||||
|
||||
// CryptoConstruction represents the encryption algorithm (either XSalsa20Poly1305 or XChacha20Poly1305)
|
||||
type CryptoConstruction uint16
|
||||
|
||||
const (
|
||||
// UndefinedConstruction is the default value for empty CertInfo only
|
||||
UndefinedConstruction CryptoConstruction = iota
|
||||
// XSalsa20Poly1305 encryption
|
||||
XSalsa20Poly1305 CryptoConstruction = 0x0001
|
||||
// XChacha20Poly1305 encryption
|
||||
XChacha20Poly1305 CryptoConstruction = 0x0002
|
||||
)
|
||||
|
||||
func (c CryptoConstruction) String() string {
|
||||
switch c {
|
||||
case XChacha20Poly1305:
|
||||
return "XChacha20Poly1305"
|
||||
case XSalsa20Poly1305:
|
||||
return "XSalsa20Poly1305"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
551
dnscrypt.go
551
dnscrypt.go
|
@ -1,551 +0,0 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ameshkov/dnscrypt/xsecretbox"
|
||||
"github.com/ameshkov/dnsstamps"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
)
|
||||
|
||||
// CryptoConstruction represents the encryption algorithm (either XSalsa20Poly1305 or XChacha20Poly1305)
|
||||
type CryptoConstruction uint16
|
||||
|
||||
const (
|
||||
// UndefinedConstruction is the default value for empty CertInfo only
|
||||
UndefinedConstruction CryptoConstruction = iota
|
||||
// XSalsa20Poly1305 encryption
|
||||
XSalsa20Poly1305
|
||||
// XChacha20Poly1305 encryption
|
||||
XChacha20Poly1305
|
||||
)
|
||||
|
||||
var (
|
||||
certMagic = [4]byte{0x44, 0x4e, 0x53, 0x43}
|
||||
serverMagic = [8]byte{0x72, 0x36, 0x66, 0x6e, 0x76, 0x57, 0x6a, 0x38}
|
||||
minDNSPacketSize = 12 + 5
|
||||
maxDNSPacketSize = 4096
|
||||
maxDNSUDPPacketSize = 1252
|
||||
)
|
||||
|
||||
const (
|
||||
clientMagicLen = 8
|
||||
nonceSize = xsecretbox.NonceSize
|
||||
halfNonceSize = xsecretbox.NonceSize / 2
|
||||
tagSize = xsecretbox.TagSize
|
||||
publicKeySize = 32
|
||||
queryOverhead = clientMagicLen + publicKeySize + halfNonceSize + tagSize
|
||||
|
||||
// <min-query-len> is a variable length, initially set to 256 bytes, and
|
||||
// must be a multiple of 64 bytes. (see https://dnscrypt.info/protocol)
|
||||
// Some servers do not work if padded length is less than 256. Example: Quad9
|
||||
minUDPQuestionSize = 256
|
||||
)
|
||||
|
||||
// Client contains parameters for a DNSCrypt client
|
||||
type Client struct {
|
||||
Proto string // Protocol ("udp" or "tcp"). Empty means "udp".
|
||||
Timeout time.Duration // Timeout for read/write operations (0 means infinite timeout)
|
||||
AdjustPayloadSize bool // If true, the client will automatically add a EDNS0 RR that will advertise a larger buffer
|
||||
}
|
||||
|
||||
// CertInfo contains DnsCrypt server certificate data retrieved from the server
|
||||
type CertInfo struct {
|
||||
Serial uint32 // Cert serial number (the cert can be superseded by another one with a higher serial number)
|
||||
ServerPk [32]byte // Server public key
|
||||
SharedKey [32]byte // Shared key
|
||||
MagicQuery [clientMagicLen]byte
|
||||
CryptoConstruction CryptoConstruction // Encryption algorithm
|
||||
NotBefore uint32 // Cert is valid starting from this date (epoch time)
|
||||
NotAfter uint32 // Cert is valid until this date (epoch time)
|
||||
}
|
||||
|
||||
// ServerInfo contains DNSCrypt server information necessary for decryption/encryption
|
||||
type ServerInfo struct {
|
||||
SecretKey [32]byte // Client secret key
|
||||
PublicKey [32]byte // Client public key
|
||||
ServerPublicKey ed25519.PublicKey // Server public key
|
||||
ServerAddress string // Server IP address
|
||||
ProviderName string // Provider name
|
||||
|
||||
ServerCert *CertInfo // Certificate info (obtained with the first unencrypted DNS request)
|
||||
}
|
||||
|
||||
// Dial fetches and validates DNSCrypt certificate from the given server
|
||||
// Data received during this call is then used for DNS requests encryption/decryption
|
||||
// stampStr is an sdns:// address which is parsed using go-dnsstamps package
|
||||
func (c *Client) Dial(stampStr string) (*ServerInfo, time.Duration, error) {
|
||||
|
||||
stamp, err := dnsstamps.NewServerStampFromString(stampStr)
|
||||
if err != nil {
|
||||
// Invalid SDNS stamp
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if stamp.Proto != dnsstamps.StampProtoTypeDNSCrypt {
|
||||
return nil, 0, errors.New("stamp is not for a DNSCrypt server")
|
||||
}
|
||||
|
||||
return c.DialStamp(stamp)
|
||||
}
|
||||
|
||||
// DialStamp fetches and validates DNSCrypt certificate from the given server
|
||||
// Data received during this call is then used for DNS requests encryption/decryption
|
||||
func (c *Client) DialStamp(stamp dnsstamps.ServerStamp) (*ServerInfo, time.Duration, error) {
|
||||
|
||||
serverInfo := ServerInfo{}
|
||||
|
||||
// Generate the secret/public pair
|
||||
if _, err := rand.Read(serverInfo.SecretKey[:]); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
curve25519.ScalarBaseMult(&serverInfo.PublicKey, &serverInfo.SecretKey)
|
||||
|
||||
// Set the provider properties
|
||||
serverInfo.ServerPublicKey = stamp.ServerPk
|
||||
serverInfo.ServerAddress = stamp.ServerAddrStr
|
||||
serverInfo.ProviderName = stamp.ProviderName
|
||||
if !strings.HasSuffix(serverInfo.ProviderName, ".") {
|
||||
serverInfo.ProviderName = serverInfo.ProviderName + "."
|
||||
}
|
||||
|
||||
// Fetch the certificate and validate it
|
||||
certInfo, rtt, err := serverInfo.fetchCurrentDNSCryptCert(c.Proto, c.Timeout)
|
||||
|
||||
if err != nil {
|
||||
return nil, rtt, err
|
||||
}
|
||||
|
||||
serverInfo.ServerCert = &certInfo
|
||||
return &serverInfo, rtt, nil
|
||||
}
|
||||
|
||||
// Exchange performs a synchronous DNS query to the specified DNSCrypt server and returns a DNS response.
|
||||
// This method creates a new network connection for every call so avoid using it for TCP.
|
||||
// DNSCrypt server information needs to be fetched and validated prior to this call using the c.DialStamp method.
|
||||
func (c *Client) Exchange(m *dns.Msg, s *ServerInfo) (*dns.Msg, time.Duration, error) {
|
||||
|
||||
now := time.Now()
|
||||
network := c.Proto
|
||||
if network == "" {
|
||||
network = "udp"
|
||||
}
|
||||
conn, err := net.Dial(network, s.ServerAddress)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
r, _, err := c.ExchangeConn(m, s, conn)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
rtt := time.Since(now)
|
||||
return r, rtt, nil
|
||||
}
|
||||
|
||||
// ExchangeConn performs a synchronous DNS query to the specified DNSCrypt server and returns a DNS response.
|
||||
// DNSCrypt server information needs to be fetched and validated prior to this call using the c.DialStamp method
|
||||
func (c *Client) ExchangeConn(m *dns.Msg, s *ServerInfo, conn net.Conn) (*dns.Msg, time.Duration, error) {
|
||||
now := time.Now()
|
||||
|
||||
if c.AdjustPayloadSize {
|
||||
c.adjustPayloadSize(m)
|
||||
}
|
||||
query, err := m.Pack()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
encryptedQuery, clientNonce, err := s.encrypt(c.Proto, query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if c.Proto == "tcp" {
|
||||
encryptedQuery, err = prefixWithSize(encryptedQuery)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Timeout > 0 {
|
||||
_ = conn.SetDeadline(time.Now().Add(c.Timeout))
|
||||
}
|
||||
_, _ = conn.Write(encryptedQuery)
|
||||
encryptedResponse := make([]byte, maxDNSPacketSize)
|
||||
|
||||
// Reading the response
|
||||
// In case if the server ServerInfo is not valid anymore (for instance, certificate was rotated) the read operation will most likely time out.
|
||||
// This might be a signal to re-dial for the server certificate.
|
||||
if c.Proto == "tcp" {
|
||||
encryptedResponse, err = readPrefixed(conn)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
} else {
|
||||
length, readErr := conn.Read(encryptedResponse)
|
||||
if readErr != nil {
|
||||
return nil, 0, readErr
|
||||
}
|
||||
encryptedResponse = encryptedResponse[:length]
|
||||
}
|
||||
|
||||
decrypted, err := s.decrypt(encryptedResponse, clientNonce)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
r := dns.Msg{}
|
||||
err = r.Unpack(decrypted)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
rtt := time.Since(now)
|
||||
return &r, rtt, nil
|
||||
}
|
||||
|
||||
// Adjusts the maximum payload size advertised in queries sent to upstream servers
|
||||
// See https://github.com/jedisct1/dnscrypt-proxy/blob/master/dnscrypt-proxy/plugin_get_set_payload_size.go
|
||||
// See here also: https://github.com/jedisct1/dnscrypt-proxy/issues/667
|
||||
func (c *Client) adjustPayloadSize(msg *dns.Msg) {
|
||||
originalMaxPayloadSize := dns.MinMsgSize
|
||||
edns0 := msg.IsEdns0()
|
||||
dnssec := false
|
||||
if edns0 != nil {
|
||||
originalMaxPayloadSize = int(edns0.UDPSize())
|
||||
dnssec = edns0.Do()
|
||||
}
|
||||
var options *[]dns.EDNS0
|
||||
|
||||
maxPayloadSize := min(maxDNSUDPPacketSize, max(originalMaxPayloadSize, maxDNSUDPPacketSize))
|
||||
|
||||
if maxPayloadSize > dns.MinMsgSize {
|
||||
var extra2 []dns.RR
|
||||
for _, extra := range msg.Extra {
|
||||
if extra.Header().Rrtype != dns.TypeOPT {
|
||||
extra2 = append(extra2, extra)
|
||||
} else if xoptions := &extra.(*dns.OPT).Option; len(*xoptions) > 0 && options == nil {
|
||||
options = xoptions
|
||||
}
|
||||
}
|
||||
msg.Extra = extra2
|
||||
msg.SetEdns0(uint16(maxPayloadSize), dnssec)
|
||||
if options != nil {
|
||||
for _, extra := range msg.Extra {
|
||||
if extra.Header().Rrtype == dns.TypeOPT {
|
||||
extra.(*dns.OPT).Option = *options
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerInfo) fetchCurrentDNSCryptCert(proto string, timeout time.Duration) (CertInfo, time.Duration, error) {
|
||||
if len(s.ServerPublicKey) != ed25519.PublicKeySize {
|
||||
return CertInfo{}, 0, errors.New("invalid public key length")
|
||||
}
|
||||
|
||||
query := new(dns.Msg)
|
||||
query.SetQuestion(s.ProviderName, dns.TypeTXT)
|
||||
client := dns.Client{Net: proto, UDPSize: uint16(maxDNSUDPPacketSize), Timeout: timeout}
|
||||
in, rtt, err := client.Exchange(query, s.ServerAddress)
|
||||
if err != nil {
|
||||
return CertInfo{}, 0, err
|
||||
}
|
||||
|
||||
certInfo := CertInfo{CryptoConstruction: UndefinedConstruction}
|
||||
for _, answerRr := range in.Answer {
|
||||
recCertInfo, err := txtToCertInfo(answerRr, s)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[%v] %s", s.ProviderName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if recCertInfo.Serial < certInfo.Serial {
|
||||
log.Printf("[%v] Superseded by a previous certificate", s.ProviderName)
|
||||
continue
|
||||
}
|
||||
|
||||
if recCertInfo.Serial == certInfo.Serial {
|
||||
if recCertInfo.CryptoConstruction > certInfo.CryptoConstruction {
|
||||
log.Printf("[%v] Upgrading the construction from %v to %v", s.ProviderName, certInfo.CryptoConstruction, recCertInfo.CryptoConstruction)
|
||||
} else {
|
||||
log.Printf("[%v] Keeping the previous, preferred crypto construction", s.ProviderName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Set the cert info
|
||||
certInfo = recCertInfo
|
||||
}
|
||||
|
||||
if certInfo.CryptoConstruction == UndefinedConstruction {
|
||||
return certInfo, 0, errors.New("no useable certificate found")
|
||||
}
|
||||
|
||||
return certInfo, rtt, nil
|
||||
}
|
||||
|
||||
func (s *ServerInfo) encrypt(proto string, packet []byte) (encrypted []byte, clientNonce []byte, err error) {
|
||||
nonce, clientNonce := make([]byte, nonceSize), make([]byte, halfNonceSize)
|
||||
rand.Read(clientNonce)
|
||||
copy(nonce, clientNonce)
|
||||
var publicKey *[publicKeySize]byte
|
||||
|
||||
sharedKey := &s.ServerCert.SharedKey
|
||||
publicKey = &s.PublicKey
|
||||
|
||||
minQuestionSize := queryOverhead + len(packet)
|
||||
if proto == "tcp" {
|
||||
var xpad [1]byte
|
||||
rand.Read(xpad[:])
|
||||
minQuestionSize += int(xpad[0])
|
||||
} else {
|
||||
minQuestionSize = max(minUDPQuestionSize, minQuestionSize)
|
||||
}
|
||||
paddedLength := min(maxDNSUDPPacketSize, (max(minQuestionSize, queryOverhead)+63) & ^63)
|
||||
|
||||
if queryOverhead+len(packet)+1 > paddedLength {
|
||||
err = errors.New("question too large; cannot be padded")
|
||||
return
|
||||
}
|
||||
encrypted = append(s.ServerCert.MagicQuery[:], publicKey[:]...)
|
||||
encrypted = append(encrypted, nonce[:halfNonceSize]...)
|
||||
padded := pad(packet, paddedLength-queryOverhead)
|
||||
if s.ServerCert.CryptoConstruction == XChacha20Poly1305 {
|
||||
encrypted = xsecretbox.Seal(encrypted, nonce, padded, sharedKey[:])
|
||||
} else {
|
||||
var xsalsaNonce [24]byte
|
||||
copy(xsalsaNonce[:], nonce)
|
||||
encrypted = secretbox.Seal(encrypted, padded, &xsalsaNonce, sharedKey)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *ServerInfo) decrypt(encrypted []byte, nonce []byte) ([]byte, error) {
|
||||
|
||||
sharedKey := &s.ServerCert.SharedKey
|
||||
serverMagicLen := len(serverMagic)
|
||||
responseHeaderLen := serverMagicLen + nonceSize
|
||||
if len(encrypted) < responseHeaderLen+tagSize+minDNSPacketSize ||
|
||||
len(encrypted) > responseHeaderLen+tagSize+maxDNSPacketSize ||
|
||||
!bytes.Equal(encrypted[:serverMagicLen], serverMagic[:]) {
|
||||
return encrypted, errors.New("invalid message size or prefix")
|
||||
}
|
||||
serverNonce := encrypted[serverMagicLen:responseHeaderLen]
|
||||
if !bytes.Equal(nonce[:halfNonceSize], serverNonce[:halfNonceSize]) {
|
||||
return encrypted, errors.New("unexpected nonce")
|
||||
}
|
||||
var packet []byte
|
||||
var err error
|
||||
if s.ServerCert.CryptoConstruction == XChacha20Poly1305 {
|
||||
packet, err = xsecretbox.Open(nil, serverNonce, encrypted[responseHeaderLen:], sharedKey[:])
|
||||
} else {
|
||||
var xsalsaServerNonce [24]byte
|
||||
copy(xsalsaServerNonce[:], serverNonce)
|
||||
var ok bool
|
||||
packet, ok = secretbox.Open(nil, encrypted[responseHeaderLen:], &xsalsaServerNonce, sharedKey)
|
||||
if !ok {
|
||||
err = errors.New("incorrect tag")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return encrypted, err
|
||||
}
|
||||
packet, err = unpad(packet)
|
||||
if err != nil || len(packet) < minDNSPacketSize {
|
||||
return encrypted, errors.New("incorrect padding")
|
||||
}
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
func txtToCertInfo(answerRr dns.RR, serverInfo *ServerInfo) (CertInfo, error) {
|
||||
now := uint32(time.Now().Unix())
|
||||
certInfo := CertInfo{CryptoConstruction: UndefinedConstruction}
|
||||
|
||||
binCert, err := packTxtString(strings.Join(answerRr.(*dns.TXT).Txt, ""))
|
||||
|
||||
// Validate the cert basic params
|
||||
if err != nil {
|
||||
return certInfo, errors.New("unable to unpack the certificate")
|
||||
}
|
||||
if len(binCert) < 124 {
|
||||
return certInfo, errors.New("certificate is too short")
|
||||
}
|
||||
if !bytes.Equal(binCert[:4], certMagic[:4]) {
|
||||
return certInfo, errors.New("invalid cert magic")
|
||||
}
|
||||
|
||||
switch esVersion := binary.BigEndian.Uint16(binCert[4:6]); esVersion {
|
||||
case 0x0001:
|
||||
certInfo.CryptoConstruction = XSalsa20Poly1305
|
||||
case 0x0002:
|
||||
certInfo.CryptoConstruction = XChacha20Poly1305
|
||||
default:
|
||||
return certInfo, fmt.Errorf("unsupported crypto construction: %v", esVersion)
|
||||
}
|
||||
|
||||
// Verify the server public key
|
||||
signature := binCert[8:72]
|
||||
signed := binCert[72:]
|
||||
if !ed25519.Verify(serverInfo.ServerPublicKey, signed, signature) {
|
||||
return certInfo, errors.New("incorrect signature")
|
||||
}
|
||||
|
||||
certInfo.Serial = binary.BigEndian.Uint32(binCert[112:116])
|
||||
|
||||
// Validate the certificate date
|
||||
certInfo.NotBefore = binary.BigEndian.Uint32(binCert[116:120])
|
||||
certInfo.NotAfter = binary.BigEndian.Uint32(binCert[120:124])
|
||||
if certInfo.NotBefore >= certInfo.NotAfter {
|
||||
return certInfo, fmt.Errorf("certificate ends before it starts (%v >= %v)", certInfo.NotBefore, certInfo.NotAfter)
|
||||
}
|
||||
if now > certInfo.NotAfter || now < certInfo.NotBefore {
|
||||
return certInfo, errors.New("certificate not valid at the current date")
|
||||
}
|
||||
|
||||
var serverPk [32]byte
|
||||
copy(serverPk[:], binCert[72:104])
|
||||
certInfo.SharedKey = computeSharedKey(certInfo.CryptoConstruction, &serverInfo.SecretKey, &serverPk, &serverInfo.ProviderName)
|
||||
|
||||
copy(certInfo.ServerPk[:], serverPk[:])
|
||||
copy(certInfo.MagicQuery[:], binCert[104:112])
|
||||
|
||||
return certInfo, nil
|
||||
}
|
||||
|
||||
func computeSharedKey(cryptoConstruction CryptoConstruction, secretKey *[32]byte, serverPk *[32]byte, providerName *string) (sharedKey [32]byte) {
|
||||
if cryptoConstruction == XChacha20Poly1305 {
|
||||
var err error
|
||||
sharedKey, err = xsecretbox.SharedKey(*secretKey, *serverPk)
|
||||
if err != nil {
|
||||
log.Printf("[%v] Weak public key", providerName)
|
||||
}
|
||||
} else {
|
||||
box.Precompute(&sharedKey, serverPk, secretKey)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isDigit(b byte) bool { return b >= '0' && b <= '9' }
|
||||
|
||||
func dddToByte(s []byte) byte {
|
||||
return (s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0')
|
||||
}
|
||||
|
||||
func packTxtString(s string) ([]byte, error) {
|
||||
bs := make([]byte, len(s))
|
||||
msg := make([]byte, 0)
|
||||
copy(bs, s)
|
||||
for i := 0; i < len(bs); i++ {
|
||||
if bs[i] == '\\' {
|
||||
i++
|
||||
if i == len(bs) {
|
||||
break
|
||||
}
|
||||
if i+2 < len(bs) && isDigit(bs[i]) && isDigit(bs[i+1]) && isDigit(bs[i+2]) {
|
||||
msg = append(msg, dddToByte(bs[i:]))
|
||||
i += 2
|
||||
} else if bs[i] == 't' {
|
||||
msg = append(msg, '\t')
|
||||
} else if bs[i] == 'r' {
|
||||
msg = append(msg, '\r')
|
||||
} else if bs[i] == 'n' {
|
||||
msg = append(msg, '\n')
|
||||
} else {
|
||||
msg = append(msg, bs[i])
|
||||
}
|
||||
} else {
|
||||
msg = append(msg, bs[i])
|
||||
}
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func pad(packet []byte, minSize int) []byte {
|
||||
packet = append(packet, 0x80)
|
||||
for len(packet) < minSize {
|
||||
packet = append(packet, 0)
|
||||
}
|
||||
return packet
|
||||
}
|
||||
|
||||
func unpad(packet []byte) ([]byte, error) {
|
||||
for i := len(packet); ; {
|
||||
if i == 0 {
|
||||
return nil, errors.New("invalid padding (short packet)")
|
||||
}
|
||||
i--
|
||||
if packet[i] == 0x80 {
|
||||
return packet[:i], nil
|
||||
} else if packet[i] != 0x00 {
|
||||
return nil, errors.New("invalid padding (delimiter not found)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prefixWithSize(packet []byte) ([]byte, error) {
|
||||
packetLen := len(packet)
|
||||
if packetLen > 0xffff {
|
||||
return packet, errors.New("packet too large")
|
||||
}
|
||||
packet = append(append(packet, 0), 0)
|
||||
copy(packet[2:], packet[:len(packet)-2])
|
||||
binary.BigEndian.PutUint16(packet[0:2], uint16(len(packet)-2))
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
func readPrefixed(conn net.Conn) ([]byte, error) {
|
||||
buf := make([]byte, 2+maxDNSPacketSize)
|
||||
packetLength, pos := -1, 0
|
||||
for {
|
||||
readnb, err := conn.Read(buf[pos:])
|
||||
if err != nil {
|
||||
return buf, err
|
||||
}
|
||||
pos += readnb
|
||||
if pos >= 2 && packetLength < 0 {
|
||||
packetLength = int(binary.BigEndian.Uint16(buf[0:2]))
|
||||
if packetLength > maxDNSPacketSize-1 {
|
||||
return buf, errors.New("packet too large")
|
||||
}
|
||||
if packetLength < minDNSPacketSize {
|
||||
return buf, errors.New("packet too short")
|
||||
}
|
||||
}
|
||||
if packetLength >= 0 && pos >= 2+packetLength {
|
||||
return buf[2 : 2+packetLength], nil
|
||||
}
|
||||
}
|
||||
}
|
164
dnscrypt_test.go
164
dnscrypt_test.go
|
@ -1,164 +0,0 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ameshkov/dnsstamps"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestParseStamp(t *testing.T) {
|
||||
// Google DoH
|
||||
stampStr := "sdns://AgUAAAAAAAAAAAAOZG5zLmdvb2dsZS5jb20NL2V4cGVyaW1lbnRhbA"
|
||||
stamp, err := dnsstamps.NewServerStampFromString(stampStr)
|
||||
|
||||
if err != nil || stamp.ProviderName == "" {
|
||||
t.Fatalf("Could not parse stamp %s: %s", stampStr, err)
|
||||
}
|
||||
|
||||
log.Println(stampStr)
|
||||
log.Printf("Proto=%s\n", stamp.Proto.String())
|
||||
log.Printf("ProviderName=%s\n", stamp.ProviderName)
|
||||
log.Printf("Path=%s\n", stamp.Path)
|
||||
log.Println("")
|
||||
|
||||
// AdGuard DNSCrypt
|
||||
stampStr = "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
|
||||
stamp, err = dnsstamps.NewServerStampFromString(stampStr)
|
||||
|
||||
if err != nil || stamp.ProviderName == "" {
|
||||
t.Fatalf("Could not parse stamp %s: %s", stampStr, err)
|
||||
}
|
||||
|
||||
log.Println(stampStr)
|
||||
log.Printf("Proto=%s\n", stamp.Proto.String())
|
||||
log.Printf("ProviderName=%s\n", stamp.ProviderName)
|
||||
log.Printf("Path=%s\n", stamp.Path)
|
||||
log.Printf("ServerAddrStr=%s\n", stamp.ServerAddrStr)
|
||||
log.Printf("ServerPk len=%d\n", len(stamp.ServerPk))
|
||||
log.Println("")
|
||||
}
|
||||
|
||||
func TestInvalidStamp(t *testing.T) {
|
||||
client := Client{}
|
||||
_, _, err := client.Dial("sdns://AQIAAAAAAAAAFDE")
|
||||
if err == nil {
|
||||
t.Fatalf("Dial must not have been possible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutOnDialError(t *testing.T) {
|
||||
// AdGuard DNS pointing to a wrong IP
|
||||
stampStr := "sdns://AQIAAAAAAAAADDguOC44Ljg6NTQ0MyDRK0fyUtzywrv4mRCG6vec5EldixbIoMQyLlLKPzkIcyIyLmRuc2NyeXB0LmRlZmF1bHQubnMxLmFkZ3VhcmQuY29t"
|
||||
client := Client{Timeout: 300 * time.Millisecond}
|
||||
|
||||
_, _, err := client.Dial(stampStr)
|
||||
if err == nil {
|
||||
t.Fatalf("Dial must not have been possible")
|
||||
}
|
||||
|
||||
if !os.IsTimeout(err) {
|
||||
t.Fatalf("Not the timeout error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutOnDialExchange(t *testing.T) {
|
||||
// AdGuard DNS
|
||||
stampStr := "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
|
||||
client := Client{Timeout: 300 * time.Millisecond}
|
||||
|
||||
serverInfo, _, err := client.Dial(stampStr)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not establish connection with %s", stampStr)
|
||||
}
|
||||
|
||||
// Point it to an IP where there's no DNSCrypt server
|
||||
serverInfo.ServerAddress = "8.8.8.8:5443"
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
}
|
||||
|
||||
//
|
||||
_, _, err = client.Exchange(&req, serverInfo)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Exchange must not have been possible")
|
||||
}
|
||||
|
||||
if !os.IsTimeout(err) {
|
||||
t.Fatalf("Not the timeout error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDnsCryptResolver(t *testing.T) {
|
||||
stamps := []struct {
|
||||
stampStr string
|
||||
}{
|
||||
{
|
||||
// AdGuard DNS
|
||||
stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
|
||||
},
|
||||
{
|
||||
// AdGuard DNS Family
|
||||
stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMjo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
|
||||
},
|
||||
{
|
||||
// AdGuard DNS Unfiltered
|
||||
stampStr: "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzNjo1NDQzILXoRNa4Oj4-EmjraB--pw3jxfpo29aIFB2_LsBmstr6JTIuZG5zY3J5cHQudW5maWx0ZXJlZC5uczEuYWRndWFyZC5jb20",
|
||||
},
|
||||
{
|
||||
// Cisco OpenDNS
|
||||
stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
|
||||
},
|
||||
{
|
||||
// Cisco OpenDNS Family Shield
|
||||
stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMTIzILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range stamps {
|
||||
t.Run(test.stampStr, func(t *testing.T) {
|
||||
checkDNSCryptServer(t, test.stampStr, "")
|
||||
checkDNSCryptServer(t, test.stampStr, "tcp")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkDNSCryptServer(t *testing.T, stampStr string, proto string) {
|
||||
client := Client{Proto: proto, Timeout: 10 * time.Second, AdjustPayloadSize: true}
|
||||
serverInfo, rtt, err := client.Dial(stampStr)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not establish connection with %s", stampStr)
|
||||
}
|
||||
|
||||
log.Printf("Established a connection with %s, ttl=%v, rtt=%v, proto=%s", serverInfo.ProviderName, time.Unix(int64(serverInfo.ServerCert.NotAfter), 0), rtt, proto)
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
}
|
||||
|
||||
reply, rtt, err := client.Exchange(&req, serverInfo)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't talk to upstream %s: %s", serverInfo.ProviderName, err)
|
||||
}
|
||||
if len(reply.Answer) != 1 {
|
||||
t.Fatalf("DNS upstream %s returned reply with wrong number of answers - %d", serverInfo.ProviderName, len(reply.Answer))
|
||||
}
|
||||
if a, ok := reply.Answer[0].(*dns.A); ok {
|
||||
if !net.IPv4(8, 8, 8, 8).Equal(a.A) {
|
||||
t.Fatalf("DNS upstream %s returned wrong answer instead of 8.8.8.8: %v", serverInfo.ProviderName, a.A)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("DNS upstream %s returned wrong answer type instead of A: %v", serverInfo.ProviderName, reply.Answer[0])
|
||||
}
|
||||
log.Printf("Got proper response from %s, rtt=%v, proto=%s", serverInfo.ProviderName, rtt, proto)
|
||||
}
|
72
doc.go
72
doc.go
|
@ -1,30 +1,64 @@
|
|||
/*
|
||||
Package dnscrypt implements a very simple DNSCrypt client library.
|
||||
Package dnscrypt includes everything you need to work with DNSCrypt. You can run your own resolver, make DNS lookups to other DNSCrypt resolvers, and you can use it as a library in your own projects.
|
||||
|
||||
The idea is to let others use DNSCrypt resolvers in the same manner as we can use regular and DoT resolvers with miekg's DNS library.
|
||||
Here's how to create a simple DNSCrypt client:
|
||||
|
||||
Here is a simple usage example:
|
||||
// AdGuard DNS stamp
|
||||
stampStr := "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
|
||||
|
||||
// AdGuard DNS stamp
|
||||
stampStr := "sdns://AQIAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
|
||||
// Initializing the DNSCrypt client
|
||||
c := dnscrypt.Client{Net: "udp", Timeout: 10 * time.Second}
|
||||
|
||||
// Initializing the DNSCrypt client
|
||||
c := dnscrypt.Client{Proto: "udp", Timeout: 10 * time.Second}
|
||||
// Fetching and validating the server certificate
|
||||
resolverInfo, err := client.Dial(stampStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetching and validating the server certificate
|
||||
serverInfo, rtt, err := client.Dial(stampStr)
|
||||
// Create a DNS request
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
}
|
||||
|
||||
// Create a DNS request
|
||||
req := dns.Msg{}
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = []dns.Question{
|
||||
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
}
|
||||
// Get the DNS response
|
||||
reply, err := c.Exchange(&req, resolverInfo)
|
||||
|
||||
// Get the DNS response
|
||||
reply, rtt, err := c.Exchange(&req, serverInfo)
|
||||
Here's how to run a DNSCrypt resolver:
|
||||
|
||||
Unfortunately, I have not found an easy way to use dnscrypt-proxy as a dependency so here's why this library was created.
|
||||
// Prepare the test DNSCrypt server config
|
||||
rc, err := dnscrypt.GenerateResolverConfig("example.org", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert, err := rc.CreateCert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s := &dnscrypt.Server{
|
||||
ProviderName: rc.ProviderName,
|
||||
ResolverCert: cert,
|
||||
Handler: dnscrypt.DefaultHandler,
|
||||
}
|
||||
|
||||
// Prepare TCP listener
|
||||
tcpConn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4zero, Port: 443})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare UDP listener
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 443})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start the server
|
||||
go s.ServeUDP(udpConn)
|
||||
go s.ServeTCP(tcpConn)
|
||||
*/
|
||||
package dnscrypt
|
||||
|
|
141
encrypted_query.go
Normal file
141
encrypted_query.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/ameshkov/dnscrypt/xsecretbox"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
)
|
||||
|
||||
// EncryptedQuery - a structure for encrypting and decrypting client queries
|
||||
//
|
||||
// <dnscrypt-query> ::= <client-magic> <client-pk> <client-nonce> <encrypted-query>
|
||||
// <encrypted-query> ::= AE(<shared-key> <client-nonce> <client-nonce-pad>, <client-query> <client-query-pad>)
|
||||
type EncryptedQuery struct {
|
||||
// EsVersion - encryption to use
|
||||
EsVersion CryptoConstruction
|
||||
|
||||
// ClientMagic - a 8 byte identifier for the resolver certificate
|
||||
// chosen by the client.
|
||||
ClientMagic [clientMagicSize]byte
|
||||
|
||||
// ClientPk - the client's public key
|
||||
ClientPk [keySize]byte
|
||||
|
||||
// With a 24 bytes nonce, a question sent by a DNSCrypt client must be
|
||||
// encrypted using the shared secret, and a nonce constructed as follows:
|
||||
// 12 bytes chosen by the client followed by 12 NUL (0) bytes.
|
||||
//
|
||||
// The client's half of the nonce can include a timestamp in addition to a
|
||||
// counter or to random bytes, so that when a response is received, the
|
||||
// client can use this timestamp to immediately discard responses to
|
||||
// queries that have been sent too long ago, or dated in the future.
|
||||
Nonce [nonceSize]byte
|
||||
}
|
||||
|
||||
// Encrypt - encrypts the specified DNS query, returns encrypted data ready to be sent.
|
||||
//
|
||||
// Note that this method will generate a random nonce automatically.
|
||||
//
|
||||
// The following fields must be set before calling this method:
|
||||
// * EsVersion -- to encrypt the query
|
||||
// * ClientMagic -- to send it with the query
|
||||
// * ClientPk -- to send it with the query
|
||||
func (q *EncryptedQuery) Encrypt(packet []byte, sharedKey [sharedKeySize]byte) ([]byte, error) {
|
||||
var query []byte
|
||||
|
||||
// Step 1: generate nonce
|
||||
binary.BigEndian.PutUint64(q.Nonce[:8], uint64(time.Now().UnixNano()))
|
||||
rand.Read(q.Nonce[8:12])
|
||||
|
||||
// Unencrypted part of the query:
|
||||
// <client-magic> <client-pk> <client-nonce>
|
||||
query = append(query, q.ClientMagic[:]...)
|
||||
query = append(query, q.ClientPk[:]...)
|
||||
query = append(query, q.Nonce[:nonceSize/2]...)
|
||||
|
||||
// <client-query> <client-query-pad>
|
||||
padded := pad(packet)
|
||||
|
||||
// <encrypted-query>
|
||||
nonce := q.Nonce
|
||||
if q.EsVersion == XChacha20Poly1305 {
|
||||
query = xsecretbox.Seal(query, nonce[:], padded, sharedKey[:])
|
||||
} else if q.EsVersion == XSalsa20Poly1305 {
|
||||
var xsalsaNonce [nonceSize]byte
|
||||
copy(xsalsaNonce[:], nonce[:])
|
||||
query = secretbox.Seal(query, padded, &xsalsaNonce, &sharedKey)
|
||||
} else {
|
||||
return nil, ErrEsVersion
|
||||
}
|
||||
|
||||
if len(query) > maxQueryLen {
|
||||
return nil, ErrQueryTooLarge
|
||||
}
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
// Decrypt - decrypts the client query, returns decrypted DNS packet.
|
||||
//
|
||||
// Please note, that before calling this method the following fields must be set:
|
||||
// * ClientMagic -- to verify the query
|
||||
// * EsVersion -- to decrypt
|
||||
func (q *EncryptedQuery) Decrypt(query []byte, serverSecretKey [keySize]byte) ([]byte, error) {
|
||||
headerLength := clientMagicSize + keySize + nonceSize/2
|
||||
if len(query) < headerLength+xsecretbox.TagSize+minDNSPacketSize {
|
||||
return nil, ErrInvalidQuery
|
||||
}
|
||||
|
||||
// read and verify <client-magic>
|
||||
clientMagic := [clientMagicSize]byte{}
|
||||
copy(clientMagic[:], query[:clientMagicSize])
|
||||
if !bytes.Equal(clientMagic[:], q.ClientMagic[:]) {
|
||||
return nil, ErrInvalidClientMagic
|
||||
}
|
||||
|
||||
// read <client-pk>
|
||||
idx := clientMagicSize
|
||||
copy(q.ClientPk[:keySize], query[idx:idx+keySize])
|
||||
|
||||
// generate server shared key
|
||||
sharedKey, err := computeSharedKey(q.EsVersion, &serverSecretKey, &q.ClientPk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read <client-nonce>
|
||||
idx = idx + keySize
|
||||
copy(q.Nonce[:nonceSize/2], query[idx:idx+nonceSize/2])
|
||||
|
||||
// read and decrypt <encrypted-query>
|
||||
idx = idx + nonceSize/2
|
||||
encryptedQuery := query[idx:]
|
||||
var packet []byte
|
||||
if q.EsVersion == XChacha20Poly1305 {
|
||||
packet, err = xsecretbox.Open(nil, q.Nonce[:], encryptedQuery, sharedKey[:])
|
||||
if err != nil {
|
||||
return nil, ErrInvalidQuery
|
||||
}
|
||||
} else if q.EsVersion == XSalsa20Poly1305 {
|
||||
var xsalsaServerNonce [24]byte
|
||||
copy(xsalsaServerNonce[:], q.Nonce[:])
|
||||
var ok bool
|
||||
packet, ok = secretbox.Open(nil, encryptedQuery, &xsalsaServerNonce, &sharedKey)
|
||||
if !ok {
|
||||
return nil, ErrInvalidQuery
|
||||
}
|
||||
} else {
|
||||
return nil, ErrEsVersion
|
||||
}
|
||||
|
||||
packet, err = unpad(packet)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidPadding
|
||||
}
|
||||
|
||||
return packet, nil
|
||||
}
|
57
encrypted_query_test.go
Normal file
57
encrypted_query_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDNSCryptQueryEncryptDecryptXSalsa20Poly1305(t *testing.T) {
|
||||
testDNSCryptQueryEncryptDecrypt(t, XSalsa20Poly1305)
|
||||
}
|
||||
|
||||
func TestDNSCryptQueryEncryptDecryptXChacha20Poly1305(t *testing.T) {
|
||||
testDNSCryptQueryEncryptDecrypt(t, XChacha20Poly1305)
|
||||
}
|
||||
|
||||
func testDNSCryptQueryEncryptDecrypt(t *testing.T, esVersion CryptoConstruction) {
|
||||
// Generate the secret/public pairs
|
||||
clientSecretKey, clientPublicKey := generateRandomKeyPair()
|
||||
serverSecretKey, serverPublicKey := generateRandomKeyPair()
|
||||
|
||||
// Generate client shared key
|
||||
clientSharedKey, err := computeSharedKey(esVersion, &clientSecretKey, &serverPublicKey)
|
||||
assert.Nil(t, err)
|
||||
|
||||
clientMagic := [clientMagicSize]byte{}
|
||||
_, _ = rand.Read(clientMagic[:])
|
||||
|
||||
q1 := EncryptedQuery{
|
||||
EsVersion: esVersion,
|
||||
ClientPk: clientPublicKey,
|
||||
ClientMagic: clientMagic,
|
||||
}
|
||||
|
||||
// Generate random packet
|
||||
packet := make([]byte, 100)
|
||||
_, _ = rand.Read(packet[:])
|
||||
|
||||
// Encrypt it
|
||||
encrypted, err := q1.Encrypt(packet, clientSharedKey)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Now let's try decrypting it
|
||||
q2 := EncryptedQuery{
|
||||
EsVersion: esVersion,
|
||||
ClientMagic: clientMagic,
|
||||
}
|
||||
|
||||
// Decrypt it
|
||||
decrypted, err := q2.Decrypt(encrypted, serverSecretKey)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Check that packet is the same
|
||||
assert.True(t, bytes.Equal(packet, decrypted))
|
||||
}
|
106
encrypted_response.go
Normal file
106
encrypted_response.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/ameshkov/dnscrypt/xsecretbox"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
)
|
||||
|
||||
// EncryptedResponse - structure for encrypting/decrypting server responses
|
||||
//
|
||||
// <dnscrypt-response> ::= <resolver-magic> <nonce> <encrypted-response>
|
||||
// <encrypted-response> ::= AE(<shared-key>, <nonce>, <resolver-response> <resolver-response-pad>)
|
||||
type EncryptedResponse struct {
|
||||
// EsVersion - encryption to use
|
||||
EsVersion CryptoConstruction
|
||||
|
||||
// Nonce - <nonce> ::= <client-nonce> <resolver-nonce>
|
||||
// <client-nonce> ::= the nonce sent by the client in the related query.
|
||||
Nonce [nonceSize]byte
|
||||
}
|
||||
|
||||
// Encrypt - encrypts the server response
|
||||
//
|
||||
// EsVersion must be set.
|
||||
// Nonce needs to be set to "client-nonce".
|
||||
// This method will generate "resolver-nonce" and set it automatically.
|
||||
func (r *EncryptedResponse) Encrypt(packet []byte, sharedKey [sharedKeySize]byte) ([]byte, error) {
|
||||
var response []byte
|
||||
|
||||
// Step 1: generate nonce
|
||||
rand.Read(r.Nonce[12:16])
|
||||
binary.BigEndian.PutUint64(r.Nonce[16:nonceSize], uint64(time.Now().UnixNano()))
|
||||
|
||||
// Unencrypted part of the query:
|
||||
response = append(response, resolverMagic[:]...)
|
||||
response = append(response, r.Nonce[:]...)
|
||||
|
||||
// <resolver-response> <resolver-response-pad>
|
||||
padded := pad(packet)
|
||||
|
||||
// <encrypted-response>
|
||||
nonce := r.Nonce
|
||||
if r.EsVersion == XChacha20Poly1305 {
|
||||
response = xsecretbox.Seal(response, nonce[:], padded, sharedKey[:])
|
||||
} else if r.EsVersion == XSalsa20Poly1305 {
|
||||
var xsalsaNonce [nonceSize]byte
|
||||
copy(xsalsaNonce[:], nonce[:])
|
||||
response = secretbox.Seal(response, padded, &xsalsaNonce, &sharedKey)
|
||||
} else {
|
||||
return nil, ErrEsVersion
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Decrypt - decrypts the server response
|
||||
//
|
||||
// EsVersion must be set.
|
||||
func (r *EncryptedResponse) Decrypt(response []byte, sharedKey [sharedKeySize]byte) ([]byte, error) {
|
||||
headerLength := len(resolverMagic) + nonceSize
|
||||
if len(response) < headerLength+xsecretbox.TagSize+minDNSPacketSize {
|
||||
return nil, ErrInvalidResponse
|
||||
}
|
||||
|
||||
// read and verify <resolver-magic>
|
||||
magic := [resolverMagicSize]byte{}
|
||||
copy(magic[:], response[:resolverMagicSize])
|
||||
if !bytes.Equal(magic[:], resolverMagic[:]) {
|
||||
return nil, ErrInvalidResolverMagic
|
||||
}
|
||||
|
||||
// read nonce
|
||||
copy(r.Nonce[:], response[resolverMagicSize:nonceSize+resolverMagicSize])
|
||||
|
||||
// read and decrypt <encrypted-response>
|
||||
encryptedResponse := response[nonceSize+resolverMagicSize:]
|
||||
var packet []byte
|
||||
var err error
|
||||
if r.EsVersion == XChacha20Poly1305 {
|
||||
packet, err = xsecretbox.Open(nil, r.Nonce[:], encryptedResponse, sharedKey[:])
|
||||
if err != nil {
|
||||
return nil, ErrInvalidResponse
|
||||
}
|
||||
} else if r.EsVersion == XSalsa20Poly1305 {
|
||||
var xsalsaServerNonce [24]byte
|
||||
copy(xsalsaServerNonce[:], r.Nonce[:])
|
||||
var ok bool
|
||||
packet, ok = secretbox.Open(nil, encryptedResponse, &xsalsaServerNonce, &sharedKey)
|
||||
if !ok {
|
||||
return nil, ErrInvalidResponse
|
||||
}
|
||||
} else {
|
||||
return nil, ErrEsVersion
|
||||
}
|
||||
|
||||
packet, err = unpad(packet)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidPadding
|
||||
}
|
||||
|
||||
return packet, nil
|
||||
}
|
72
encrypted_response_test.go
Normal file
72
encrypted_response_test.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/ameshkov/dnscrypt/xsecretbox"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDNSCryptResponseEncryptDecryptXSalsa20Poly1305(t *testing.T) {
|
||||
testDNSCryptResponseEncryptDecrypt(t, XSalsa20Poly1305)
|
||||
}
|
||||
|
||||
func TestDNSCryptResponseEncryptDecryptXChacha20Poly1305(t *testing.T) {
|
||||
testDNSCryptResponseEncryptDecrypt(t, XChacha20Poly1305)
|
||||
}
|
||||
|
||||
func testDNSCryptResponseEncryptDecrypt(t *testing.T, esVersion CryptoConstruction) {
|
||||
// Generate the secret/public pairs
|
||||
clientSecretKey, clientPublicKey := generateRandomKeyPair()
|
||||
serverSecretKey, serverPublicKey := generateRandomKeyPair()
|
||||
|
||||
// Generate client shared key
|
||||
clientSharedKey, err := computeSharedKey(esVersion, &clientSecretKey, &serverPublicKey)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Generate server shared key
|
||||
serverSharedKey, err := computeSharedKey(esVersion, &serverSecretKey, &clientPublicKey)
|
||||
assert.Nil(t, err)
|
||||
|
||||
r1 := &EncryptedResponse{
|
||||
EsVersion: esVersion,
|
||||
}
|
||||
// Fill client-nonce
|
||||
_, _ = rand.Read(r1.Nonce[:nonceSize/12])
|
||||
|
||||
// Generate random packet
|
||||
packet := make([]byte, 100)
|
||||
_, _ = rand.Read(packet[:])
|
||||
|
||||
// Encrypt it
|
||||
encrypted, err := r1.Encrypt(packet, serverSharedKey)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Now let's try decrypting it
|
||||
r2 := &EncryptedResponse{
|
||||
EsVersion: esVersion,
|
||||
}
|
||||
|
||||
// Decrypt it
|
||||
decrypted, err := r2.Decrypt(encrypted, clientSharedKey)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Check that packet is the same
|
||||
assert.True(t, bytes.Equal(packet, decrypted))
|
||||
|
||||
// Now check invalid data (some random stuff)
|
||||
_, err = r2.Decrypt(packet, clientSharedKey)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// Empty array
|
||||
_, err = r2.Decrypt([]byte{}, clientSharedKey)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
// Minimum valid size
|
||||
b := make([]byte, len(resolverMagic)+nonceSize+xsecretbox.TagSize+minDNSPacketSize)
|
||||
_, _ = rand.Read(b)
|
||||
_, err = r2.Decrypt(b, clientSharedKey)
|
||||
assert.NotNil(t, err)
|
||||
}
|
167
generate.go
Normal file
167
generate.go
Normal file
|
@ -0,0 +1,167 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/ameshkov/dnsstamps"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
const dnsCryptV2Prefix = "2.dnscrypt-cert."
|
||||
|
||||
// ResolverConfig - DNSCrypt resolver configuration
|
||||
type ResolverConfig struct {
|
||||
// DNSCrypt provider name
|
||||
ProviderName string `yaml:"provider_name"`
|
||||
|
||||
// PublicKey - DNSCrypt resolver public key
|
||||
PublicKey string `yaml:"public_key"`
|
||||
|
||||
// PrivateKey - DNSCrypt resolver private key
|
||||
// The main and only purpose of this key is to sign the certificate
|
||||
PrivateKey string `yaml:"private_key"`
|
||||
|
||||
// ResolverSk - hex-encoded short-term private key.
|
||||
// This key is used to encrypt/decrypt DNS queries.
|
||||
// If not set, we'll generate a new random ResolverSk and ResolverPk.
|
||||
ResolverSk string `yaml:"resolver_secret"`
|
||||
|
||||
// ResolverSk - hex-encoded short-term public key corresponding to ResolverSk.
|
||||
// This key is used to encrypt/decrypt DNS queries.
|
||||
ResolverPk string `yaml:"resolver_public"`
|
||||
|
||||
// EsVersion - crypto to use in this resolver
|
||||
EsVersion CryptoConstruction `yaml:"es_version"`
|
||||
|
||||
// CertificateTTL - time-to-live for the certificate that is generated using this ResolverConfig.
|
||||
// If not set, we'll use 1 year by default.
|
||||
CertificateTTL time.Duration `yaml:"certificate_ttl"`
|
||||
}
|
||||
|
||||
// CreateCert - generates a signed Cert to be used by Server
|
||||
func (rc *ResolverConfig) CreateCert() (*Cert, error) {
|
||||
log.Printf("Creating signed DNSCrypt certificate")
|
||||
|
||||
notAfter := time.Now()
|
||||
if rc.CertificateTTL > 0 {
|
||||
notAfter = notAfter.Add(rc.CertificateTTL)
|
||||
} else {
|
||||
// Default cert validity is 1 year
|
||||
notAfter = notAfter.Add(time.Hour * 24 * 365)
|
||||
}
|
||||
|
||||
cert := &Cert{
|
||||
Serial: uint32(time.Now().Unix()),
|
||||
NotAfter: uint32(notAfter.Unix()),
|
||||
NotBefore: uint32(time.Now().Unix()),
|
||||
EsVersion: rc.EsVersion,
|
||||
}
|
||||
|
||||
// short-term public key
|
||||
resolverPk, err := HexDecodeKey(rc.ResolverPk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// short-term private key
|
||||
resolverSk, err := HexDecodeKey(rc.ResolverSk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resolverPk) != keySize || len(resolverSk) != keySize {
|
||||
log.Printf("Short-term keys are not set, generating random ones")
|
||||
sk, pk := generateRandomKeyPair()
|
||||
resolverSk = sk[:]
|
||||
resolverPk = pk[:]
|
||||
}
|
||||
|
||||
copy(cert.ResolverPk[:], resolverPk[:])
|
||||
copy(cert.ResolverSk[:], resolverSk)
|
||||
|
||||
// private key
|
||||
privateKey, err := HexDecodeKey(rc.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// sign the data
|
||||
cert.Sign(privateKey)
|
||||
|
||||
log.Info("Signed cert: %s", cert.String())
|
||||
|
||||
// done
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// CreateStamp - generates a DNS stamp for this resolver
|
||||
func (rc *ResolverConfig) CreateStamp(addr string) (dnsstamps.ServerStamp, error) {
|
||||
stamp := dnsstamps.ServerStamp{
|
||||
ProviderName: rc.ProviderName,
|
||||
Proto: dnsstamps.StampProtoTypeDNSCrypt,
|
||||
}
|
||||
|
||||
serverPk, err := HexDecodeKey(rc.PublicKey)
|
||||
if err != nil {
|
||||
return stamp, err
|
||||
}
|
||||
|
||||
stamp.ServerPk = serverPk
|
||||
stamp.ServerAddrStr = addr
|
||||
return stamp, nil
|
||||
}
|
||||
|
||||
// GenerateResolverConfig - generates resolver configuration for a given provider name.
|
||||
// providerName is mandatory. If needed, "2.dnscrypt-cert." prefix is added to it.
|
||||
// privateKey is optional. If not set, it will be generated automatically.
|
||||
func GenerateResolverConfig(providerName string, privateKey ed25519.PrivateKey) (ResolverConfig, error) {
|
||||
rc := ResolverConfig{
|
||||
// Use XSalsa20Poly1305 by default
|
||||
EsVersion: XSalsa20Poly1305,
|
||||
}
|
||||
if !strings.HasPrefix(providerName, dnsCryptV2Prefix) {
|
||||
providerName = dnsCryptV2Prefix + providerName
|
||||
}
|
||||
rc.ProviderName = providerName
|
||||
|
||||
var err error
|
||||
if privateKey == nil {
|
||||
// privateKey = gene
|
||||
_, privateKey, err = ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return rc, err
|
||||
}
|
||||
}
|
||||
rc.PrivateKey = HexEncodeKey(privateKey)
|
||||
rc.PublicKey = HexEncodeKey(privateKey.Public().(ed25519.PublicKey))
|
||||
|
||||
resolverSk, resolverPk := generateRandomKeyPair()
|
||||
rc.ResolverSk = HexEncodeKey(resolverSk[:])
|
||||
rc.ResolverPk = HexEncodeKey(resolverPk[:])
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// HexEncodeKey - encodes a byte slice to a hex-encoded string.
|
||||
func HexEncodeKey(b []byte) string {
|
||||
return strings.ToUpper(hex.EncodeToString(b))
|
||||
}
|
||||
|
||||
// HexDecodeKey - decodes a hex-encoded string with (optional) colons
|
||||
// to a byte array.
|
||||
func HexDecodeKey(str string) ([]byte, error) {
|
||||
return hex.DecodeString(strings.ReplaceAll(str, ":", ""))
|
||||
}
|
||||
|
||||
// generateRandomKeyPair - generates a random key-pair
|
||||
func generateRandomKeyPair() (privateKey [keySize]byte, publicKey [keySize]byte) {
|
||||
privateKey = [keySize]byte{}
|
||||
publicKey = [keySize]byte{}
|
||||
|
||||
_, _ = rand.Read(privateKey[:])
|
||||
curve25519.ScalarBaseMult(&publicKey, &privateKey)
|
||||
return
|
||||
}
|
38
generate_test.go
Normal file
38
generate_test.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHexEncodeKey(t *testing.T) {
|
||||
str := HexEncodeKey([]byte{1, 2, 3, 4})
|
||||
assert.Equal(t, "01020304", str)
|
||||
}
|
||||
|
||||
func TestHexDecodeKey(t *testing.T) {
|
||||
b, err := HexDecodeKey("01:02:03:04")
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, bytes.Equal(b, []byte{1, 2, 3, 4}))
|
||||
}
|
||||
|
||||
func TestGenerateResolverConfig(t *testing.T) {
|
||||
rc, err := GenerateResolverConfig("example.org", nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "2.dnscrypt-cert.example.org", rc.ProviderName)
|
||||
assert.Equal(t, ed25519.PrivateKeySize*2, len(rc.PrivateKey))
|
||||
assert.Equal(t, keySize*2, len(rc.ResolverSk))
|
||||
assert.Equal(t, keySize*2, len(rc.ResolverPk))
|
||||
|
||||
cert, err := rc.CreateCert()
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.True(t, cert.VerifyDate())
|
||||
|
||||
publicKey, err := HexDecodeKey(rc.PublicKey)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, cert.VerifySignature(publicKey))
|
||||
}
|
6
go.mod
6
go.mod
|
@ -1,13 +1,17 @@
|
|||
module github.com/ameshkov/dnscrypt
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/golibs v0.4.2
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635
|
||||
github.com/ameshkov/dnsstamps v1.0.1
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/miekg/dns v1.1.29
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
|
||||
)
|
||||
|
||||
go 1.14
|
||||
|
|
20
go.sum
20
go.sum
|
@ -1,11 +1,25 @@
|
|||
github.com/AdguardTeam/golibs v0.4.2 h1:7M28oTZFoFwNmp8eGPb3ImmYbxGaJLyQXeIFVHjME0o=
|
||||
github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
|
||||
github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4JhnPug=
|
||||
github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
|
||||
|
@ -27,3 +41,9 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
60
handler.go
Normal file
60
handler.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const defaultTimeout = 10 * time.Second
|
||||
|
||||
// Handler is implemented by any value that implements ServeDNS.
|
||||
type Handler interface {
|
||||
ServeDNS(rw ResponseWriter, r *dns.Msg) error
|
||||
}
|
||||
|
||||
// ResponseWriter - interface that needs to be implemented for different protocols
|
||||
type ResponseWriter interface {
|
||||
LocalAddr() net.Addr // LocalAddr - local socket address
|
||||
RemoteAddr() net.Addr // RemoteAddr - remote client socket address
|
||||
WriteMsg(m *dns.Msg) error // WriteMsg - writes response message to the client
|
||||
}
|
||||
|
||||
// DefaultHandler - default Handler implementation
|
||||
// that is used by Server if custom handler is not configured
|
||||
var DefaultHandler Handler = &defaultHandler{
|
||||
udpClient: &dns.Client{
|
||||
Net: "udp",
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
tcpClient: &dns.Client{
|
||||
Net: "tcp",
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
addr: "94.140.14.140:53",
|
||||
}
|
||||
|
||||
type defaultHandler struct {
|
||||
udpClient *dns.Client
|
||||
tcpClient *dns.Client
|
||||
addr string
|
||||
}
|
||||
|
||||
// ServeDNS - implements Handler interface
|
||||
func (h *defaultHandler) ServeDNS(rw ResponseWriter, r *dns.Msg) error {
|
||||
// Google DNS
|
||||
res, _, err := h.udpClient.Exchange(r, h.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Truncated {
|
||||
res, _, err = h.tcpClient.Exchange(r, h.addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rw.WriteMsg(res)
|
||||
}
|
141
server.go
Normal file
141
server.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Server - a simple DNSCrypt server implementation
|
||||
type Server struct {
|
||||
// ProviderName - DNSCrypt provider name
|
||||
ProviderName string
|
||||
|
||||
// ResolverCert - contains resolver certificate.
|
||||
ResolverCert *Cert
|
||||
|
||||
// Handler to invoke. If nil, uses DefaultHandler.
|
||||
Handler Handler
|
||||
}
|
||||
|
||||
// serveDNS - serves DNS response
|
||||
func (s *Server) serveDNS(rw ResponseWriter, r *dns.Msg) {
|
||||
if r == nil || len(r.Question) != 1 || r.Response {
|
||||
log.Tracef("Invalid query: %v", r)
|
||||
return
|
||||
}
|
||||
|
||||
log.Tracef("Handling a DNS query: %s", r.Question[0].Name)
|
||||
|
||||
handler := s.Handler
|
||||
if handler == nil {
|
||||
handler = DefaultHandler
|
||||
}
|
||||
|
||||
err := handler.ServeDNS(rw, r)
|
||||
if err != nil {
|
||||
log.Tracef("Error while handing a DNS query: %v", err)
|
||||
|
||||
reply := &dns.Msg{}
|
||||
reply.SetRcode(r, dns.RcodeServerFailure)
|
||||
_ = rw.WriteMsg(reply)
|
||||
}
|
||||
}
|
||||
|
||||
// encrypt - encrypts DNSCrypt response
|
||||
func (s *Server) encrypt(m *dns.Msg, q EncryptedQuery) ([]byte, error) {
|
||||
r := EncryptedResponse{
|
||||
EsVersion: q.EsVersion,
|
||||
Nonce: q.Nonce,
|
||||
}
|
||||
packet, err := m.Pack()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sharedKey, err := computeSharedKey(q.EsVersion, &s.ResolverCert.ResolverSk, &q.ClientPk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.Encrypt(packet, sharedKey)
|
||||
}
|
||||
|
||||
// decrypt - decrypts the incoming message and returns a DNS message to process
|
||||
func (s *Server) decrypt(b []byte) (*dns.Msg, EncryptedQuery, error) {
|
||||
q := EncryptedQuery{
|
||||
EsVersion: s.ResolverCert.EsVersion,
|
||||
ClientMagic: s.ResolverCert.ClientMagic,
|
||||
}
|
||||
msg, err := q.Decrypt(b, s.ResolverCert.ResolverSk)
|
||||
if err != nil {
|
||||
// Failed to decrypt, dropping it
|
||||
return nil, q, err
|
||||
}
|
||||
|
||||
r := new(dns.Msg)
|
||||
err = r.Unpack(msg)
|
||||
if err != nil {
|
||||
// Invalid DNS message, ignore
|
||||
return nil, q, err
|
||||
}
|
||||
|
||||
return r, q, nil
|
||||
}
|
||||
|
||||
// handleHandshake - handles a TXT request that requests certificate data
|
||||
func (s *Server) handleHandshake(b []byte, certTxt string) ([]byte, error) {
|
||||
m := new(dns.Msg)
|
||||
err := m.Unpack(b)
|
||||
if err != nil {
|
||||
// Not a handshake, just ignore it
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(m.Question) != 1 || m.Response {
|
||||
// Invalid query
|
||||
return nil, ErrInvalidQuery
|
||||
}
|
||||
|
||||
q := m.Question[0]
|
||||
providerName := dns.Fqdn(s.ProviderName)
|
||||
if q.Qtype != dns.TypeTXT || q.Name != providerName {
|
||||
// Invalid provider name or type, doing nothing
|
||||
return nil, ErrInvalidQuery
|
||||
}
|
||||
|
||||
reply := new(dns.Msg)
|
||||
reply.SetReply(m)
|
||||
txt := &dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Ttl: 60, // use 60 seconds by default, but it shouldn't matter
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Txt: []string{
|
||||
certTxt,
|
||||
},
|
||||
}
|
||||
reply.Answer = append(reply.Answer, txt)
|
||||
return reply.Pack()
|
||||
}
|
||||
|
||||
// validate - checks if the Server config is properly set
|
||||
func (s *Server) validate() bool {
|
||||
if s.ResolverCert == nil {
|
||||
log.Error("ResolverCert must be set")
|
||||
return false
|
||||
}
|
||||
|
||||
if !s.ResolverCert.VerifyDate() {
|
||||
log.Error("ResolverCert date is not valid")
|
||||
return false
|
||||
}
|
||||
|
||||
if s.ProviderName == "" {
|
||||
log.Error("ProviderName must be set")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
122
server_tcp.go
Normal file
122
server_tcp.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// TCPResponseWriter - ResponseWriter implementation for TCP
|
||||
type TCPResponseWriter struct {
|
||||
tcpConn net.Conn
|
||||
encrypt encryptionFunc
|
||||
req *dns.Msg
|
||||
query EncryptedQuery
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ ResponseWriter = &TCPResponseWriter{}
|
||||
|
||||
// LocalAddr - server socket local address
|
||||
func (w *TCPResponseWriter) LocalAddr() net.Addr {
|
||||
return w.tcpConn.LocalAddr()
|
||||
}
|
||||
|
||||
// RemoteAddr - client's address
|
||||
func (w *TCPResponseWriter) RemoteAddr() net.Addr {
|
||||
return w.tcpConn.RemoteAddr()
|
||||
}
|
||||
|
||||
// WriteMsg - writes DNS message to the client
|
||||
func (w *TCPResponseWriter) WriteMsg(m *dns.Msg) error {
|
||||
m.Truncate(dnsSize("tcp", w.req))
|
||||
|
||||
res, err := w.encrypt(m, w.query)
|
||||
if err != nil {
|
||||
log.Tracef("Failed to encrypt the DNS query: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return writePrefixed(res, w.tcpConn)
|
||||
}
|
||||
|
||||
// ServeTCP - listens to TCP connections, queries are then processed by Server.Handler.
|
||||
// It blocks the calling goroutine and to stop it you need to close the listener.
|
||||
func (s *Server) ServeTCP(l net.Listener) error {
|
||||
// Check that server is properly configured
|
||||
if !s.validate() {
|
||||
return ErrServerConfig
|
||||
}
|
||||
|
||||
// Serialize the cert right away and prepare it to be sent to the client
|
||||
certBuf, err := s.ResolverCert.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certTxt := packTxtString(certBuf)
|
||||
|
||||
log.Info("Entering DNSCrypt TCP listening loop tcp://%s", l.Addr().String())
|
||||
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err == nil {
|
||||
go func() {
|
||||
_ = s.handleTCPConnection(conn, certTxt)
|
||||
_ = conn.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if isConnClosed(err) {
|
||||
log.Info("udpListen.ReadFrom() returned because we're reading from a closed connection, exiting loop")
|
||||
} else {
|
||||
log.Info("got error when reading from UDP listen: %s", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleTCPConnection(conn net.Conn, certTxt string) error {
|
||||
for {
|
||||
b, err := readPrefixed(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(b) < minDNSPacketSize {
|
||||
// Ignore the packets that are too short
|
||||
return ErrTooShort
|
||||
}
|
||||
|
||||
if bytes.Equal(b[:clientMagicSize], s.ResolverCert.ClientMagic[:]) {
|
||||
// This is an encrypted message, we should decrypt it
|
||||
m, q, err := s.decrypt(b)
|
||||
if err != nil {
|
||||
log.Tracef("failed to decrypt incoming message: %v", err)
|
||||
return err
|
||||
}
|
||||
rw := &TCPResponseWriter{
|
||||
tcpConn: conn,
|
||||
encrypt: s.encrypt,
|
||||
req: m,
|
||||
query: q,
|
||||
}
|
||||
s.serveDNS(rw, m)
|
||||
} else {
|
||||
// Most likely this a DNS message requesting the certificate
|
||||
reply, err := s.handleHandshake(b, certTxt)
|
||||
if err != nil {
|
||||
log.Tracef("Failed to process a plain DNS query: %v", err)
|
||||
return err
|
||||
}
|
||||
err = writePrefixed(reply, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
169
server_test.go
Normal file
169
server_test.go
Normal file
|
@ -0,0 +1,169 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ameshkov/dnsstamps"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestServerUDPServeCert(t *testing.T) {
|
||||
testServerServeCert(t, "udp")
|
||||
}
|
||||
|
||||
func TestServerTCPServeCert(t *testing.T) {
|
||||
testServerServeCert(t, "tcp")
|
||||
}
|
||||
|
||||
func TestServerUDPRespondMessages(t *testing.T) {
|
||||
testServerRespondMessages(t, "udp")
|
||||
}
|
||||
|
||||
func TestServerTCPRespondMessages(t *testing.T) {
|
||||
testServerRespondMessages(t, "tcp")
|
||||
}
|
||||
|
||||
func testServerServeCert(t *testing.T, network string) {
|
||||
srv := newTestServer(t, &testHandler{})
|
||||
defer srv.Close()
|
||||
|
||||
client := &Client{
|
||||
Net: network,
|
||||
Timeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
serverAddr := fmt.Sprintf("127.0.0.1:%d", srv.UDPAddr().Port)
|
||||
if network == "tcp" {
|
||||
serverAddr = fmt.Sprintf("127.0.0.1:%d", srv.TCPAddr().Port)
|
||||
}
|
||||
|
||||
stamp := dnsstamps.ServerStamp{
|
||||
ServerAddrStr: serverAddr,
|
||||
ServerPk: srv.resolverPk,
|
||||
ProviderName: srv.server.ProviderName,
|
||||
Proto: dnsstamps.StampProtoTypeDNSCrypt,
|
||||
}
|
||||
ri, err := client.DialStamp(stamp)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ri)
|
||||
|
||||
assert.Equal(t, ri.ProviderName, srv.server.ProviderName)
|
||||
assert.True(t, bytes.Equal(srv.server.ResolverCert.ClientMagic[:], ri.ResolverCert.ClientMagic[:]))
|
||||
assert.Equal(t, srv.server.ResolverCert.EsVersion, ri.ResolverCert.EsVersion)
|
||||
assert.Equal(t, srv.server.ResolverCert.Signature, ri.ResolverCert.Signature)
|
||||
assert.Equal(t, srv.server.ResolverCert.NotBefore, ri.ResolverCert.NotBefore)
|
||||
assert.Equal(t, srv.server.ResolverCert.NotAfter, ri.ResolverCert.NotAfter)
|
||||
assert.True(t, bytes.Equal(srv.server.ResolverCert.ResolverPk[:], ri.ResolverCert.ResolverPk[:]))
|
||||
assert.True(t, bytes.Equal(srv.server.ResolverCert.ResolverPk[:], ri.ResolverCert.ResolverPk[:]))
|
||||
}
|
||||
|
||||
func testServerRespondMessages(t *testing.T, network string) {
|
||||
srv := newTestServer(t, &testHandler{})
|
||||
defer srv.Close()
|
||||
|
||||
client := &Client{
|
||||
Timeout: 1 * time.Second,
|
||||
Net: network,
|
||||
}
|
||||
|
||||
serverAddr := fmt.Sprintf("127.0.0.1:%d", srv.UDPAddr().Port)
|
||||
if network == "tcp" {
|
||||
serverAddr = fmt.Sprintf("127.0.0.1:%d", srv.TCPAddr().Port)
|
||||
}
|
||||
|
||||
stamp := dnsstamps.ServerStamp{
|
||||
ServerAddrStr: serverAddr,
|
||||
ServerPk: srv.resolverPk,
|
||||
ProviderName: srv.server.ProviderName,
|
||||
Proto: dnsstamps.StampProtoTypeDNSCrypt,
|
||||
}
|
||||
ri, err := client.DialStamp(stamp)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ri)
|
||||
|
||||
conn, err := net.Dial(network, stamp.ServerAddrStr)
|
||||
assert.Nil(t, err)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
m := createTestMessage()
|
||||
res, err := client.ExchangeConn(conn, m, ri)
|
||||
assert.Nil(t, err)
|
||||
assertTestMessageResponse(t, res)
|
||||
}
|
||||
}
|
||||
|
||||
type testServer struct {
|
||||
server *Server
|
||||
resolverPk ed25519.PublicKey
|
||||
udpConn *net.UDPConn
|
||||
tcpListen net.Listener
|
||||
handler Handler
|
||||
}
|
||||
|
||||
func (s *testServer) TCPAddr() *net.TCPAddr {
|
||||
return s.tcpListen.Addr().(*net.TCPAddr)
|
||||
}
|
||||
|
||||
func (s *testServer) UDPAddr() *net.UDPAddr {
|
||||
return s.udpConn.LocalAddr().(*net.UDPAddr)
|
||||
}
|
||||
|
||||
func (s *testServer) Close() {
|
||||
_ = s.udpConn.Close()
|
||||
_ = s.tcpListen.Close()
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T, handler Handler) *testServer {
|
||||
rc, err := GenerateResolverConfig("example.org", nil)
|
||||
assert.Nil(t, err)
|
||||
cert, err := rc.CreateCert()
|
||||
assert.Nil(t, err)
|
||||
|
||||
s := &Server{
|
||||
ProviderName: rc.ProviderName,
|
||||
ResolverCert: cert,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
privateKey, err := HexDecodeKey(rc.PrivateKey)
|
||||
assert.Nil(t, err)
|
||||
publicKey := ed25519.PrivateKey(privateKey).Public().(ed25519.PublicKey)
|
||||
srv := &testServer{
|
||||
server: s,
|
||||
resolverPk: publicKey,
|
||||
}
|
||||
|
||||
srv.tcpListen, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4zero, Port: 0})
|
||||
assert.Nil(t, err)
|
||||
srv.udpConn, err = net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
assert.Nil(t, err)
|
||||
|
||||
go s.ServeUDP(srv.udpConn)
|
||||
go s.ServeTCP(srv.tcpListen)
|
||||
return srv
|
||||
}
|
||||
|
||||
type testHandler struct{}
|
||||
|
||||
// ServeDNS - implements Handler interface
|
||||
func (h *testHandler) ServeDNS(rw ResponseWriter, r *dns.Msg) error {
|
||||
// Google DNS
|
||||
res := new(dns.Msg)
|
||||
res.SetReply(r)
|
||||
answer := new(dns.A)
|
||||
answer.Hdr = dns.RR_Header{
|
||||
Name: r.Question[0].Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Ttl: 300,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
answer.A = net.IPv4(8, 8, 8, 8)
|
||||
res.Answer = append(res.Answer, answer)
|
||||
return rw.WriteMsg(res)
|
||||
}
|
124
server_udp.go
Normal file
124
server_udp.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type encryptionFunc func(m *dns.Msg, q EncryptedQuery) ([]byte, error)
|
||||
|
||||
// UDPResponseWriter - ResponseWriter implementation for UDP
|
||||
type UDPResponseWriter struct {
|
||||
udpConn *net.UDPConn // UDP connection
|
||||
remoteAddr *net.UDPAddr // Remote peer address
|
||||
localIP net.IP // Local IP (that was used to accept the remote connection)
|
||||
encrypt encryptionFunc // DNSCRypt encryption function
|
||||
req *dns.Msg // DNS query that was processed
|
||||
query EncryptedQuery // DNSCrypt query properties
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ ResponseWriter = &UDPResponseWriter{}
|
||||
|
||||
// LocalAddr - server socket local address
|
||||
func (w *UDPResponseWriter) LocalAddr() net.Addr {
|
||||
return w.udpConn.LocalAddr()
|
||||
}
|
||||
|
||||
// RemoteAddr - client's address
|
||||
func (w *UDPResponseWriter) RemoteAddr() net.Addr {
|
||||
return w.remoteAddr
|
||||
}
|
||||
|
||||
// WriteMsg - writes DNS message to the client
|
||||
func (w *UDPResponseWriter) WriteMsg(m *dns.Msg) error {
|
||||
m.Truncate(dnsSize("udp", w.req))
|
||||
|
||||
res, err := w.encrypt(m, w.query)
|
||||
if err != nil {
|
||||
log.Tracef("Failed to encrypt the DNS query: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = udpWrite(res, w.udpConn, w.remoteAddr, w.localIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeUDP - listens to UDP connections, queries are then processed by Server.Handler.
|
||||
// It blocks the calling goroutine and to stop it you need to close the listener.
|
||||
func (s *Server) ServeUDP(l *net.UDPConn) error {
|
||||
// Check that server is properly configured
|
||||
if !s.validate() {
|
||||
return ErrServerConfig
|
||||
}
|
||||
|
||||
// set UDP options to allow receiving OOB data
|
||||
err := udpSetOptions(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Buffer to read incoming messages
|
||||
b := make([]byte, dns.MaxMsgSize)
|
||||
|
||||
// Serialize the cert right away and prepare it to be sent to the client
|
||||
certBuf, err := s.ResolverCert.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certTxt := packTxtString(certBuf)
|
||||
|
||||
// Init oobSize - it will be used later when reading and writing UDP messages
|
||||
oobSize := udpGetOOBSize()
|
||||
|
||||
log.Info("Entering DNSCrypt UDP listening loop on udp://%s", l.LocalAddr().String())
|
||||
|
||||
for {
|
||||
n, localIP, addr, err := udpRead(l, b, oobSize)
|
||||
if n < minDNSPacketSize {
|
||||
// Ignore the packets that are too short
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.Equal(b[:clientMagicSize], s.ResolverCert.ClientMagic[:]) {
|
||||
// This is an encrypted message, we should decrypt it
|
||||
m, q, err := s.decrypt(b[:n])
|
||||
if err == nil {
|
||||
rw := &UDPResponseWriter{
|
||||
udpConn: l,
|
||||
remoteAddr: addr,
|
||||
localIP: localIP,
|
||||
encrypt: s.encrypt,
|
||||
req: m,
|
||||
query: q,
|
||||
}
|
||||
go s.serveDNS(rw, m)
|
||||
} else {
|
||||
log.Tracef("Failed to decrypt incoming message len=%d: %v", n, err)
|
||||
}
|
||||
} else {
|
||||
// Most likely this a DNS message requesting the certificate
|
||||
reply, err := s.handleHandshake(b, certTxt)
|
||||
if err != nil {
|
||||
log.Tracef("Failed to process a plain DNS query: %v", err)
|
||||
}
|
||||
if err == nil {
|
||||
_, _ = l.WriteTo(reply, addr)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if isConnClosed(err) {
|
||||
log.Info("udpListen.ReadFrom() returned because we're reading from a closed connection, exiting loop")
|
||||
} else {
|
||||
log.Info("got error when reading from UDP listen: %s", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
83
udp_unix.go
Normal file
83
udp_unix.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
// +build aix darwin dragonfly linux netbsd openbsd solaris freebsd
|
||||
|
||||
package dnscrypt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"golang.org/x/net/ipv4"
|
||||
"golang.org/x/net/ipv6"
|
||||
)
|
||||
|
||||
// udpGetOOBSize - get max. size of received OOB data
|
||||
// It will then be used in the ReadMsgUDP function
|
||||
func udpGetOOBSize() int {
|
||||
oob4 := ipv4.NewControlMessage(ipv4.FlagDst | ipv4.FlagInterface)
|
||||
oob6 := ipv6.NewControlMessage(ipv6.FlagDst | ipv6.FlagInterface)
|
||||
|
||||
if len(oob4) > len(oob6) {
|
||||
return len(oob4)
|
||||
}
|
||||
return len(oob6)
|
||||
}
|
||||
|
||||
// udpSetOptions - set options on a UDP socket to be able to receive the necessary OOB data
|
||||
func udpSetOptions(c *net.UDPConn) error {
|
||||
err6 := ipv6.NewPacketConn(c).SetControlMessage(ipv6.FlagDst|ipv6.FlagInterface, true)
|
||||
err4 := ipv4.NewPacketConn(c).SetControlMessage(ipv4.FlagDst|ipv4.FlagInterface, true)
|
||||
if err6 != nil && err4 != nil {
|
||||
return fmt.Errorf("failed to call SetControlMessage: ipv4: %v ipv6: %v", err4, err6)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// udpRead - receive payload and OOB data from the UDP socket
|
||||
func udpRead(c *net.UDPConn, buf []byte, udpOOBSize int) (int, net.IP, *net.UDPAddr, error) {
|
||||
var oobn int
|
||||
oob := make([]byte, udpOOBSize)
|
||||
var err error
|
||||
var n int
|
||||
var remoteAddr *net.UDPAddr
|
||||
n, oobn, _, remoteAddr, err = c.ReadMsgUDP(buf, oob)
|
||||
if err != nil {
|
||||
return -1, nil, nil, err
|
||||
}
|
||||
|
||||
localIP := udpGetDstFromOOB(oob[:oobn])
|
||||
return n, localIP, remoteAddr, nil
|
||||
}
|
||||
|
||||
// udpWrite - writes to the UDP socket and sets local IP to OOB data
|
||||
func udpWrite(bytes []byte, conn *net.UDPConn, remoteAddr *net.UDPAddr, localIP net.IP) (int, error) {
|
||||
n, _, err := conn.WriteMsgUDP(bytes, udpMakeOOBWithSrc(localIP), remoteAddr)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// udpGetDstFromOOB - get destination IP from OOB data
|
||||
func udpGetDstFromOOB(oob []byte) net.IP {
|
||||
cm6 := &ipv6.ControlMessage{}
|
||||
if cm6.Parse(oob) == nil && cm6.Dst != nil {
|
||||
return cm6.Dst
|
||||
}
|
||||
|
||||
cm4 := &ipv4.ControlMessage{}
|
||||
if cm4.Parse(oob) == nil && cm4.Dst != nil {
|
||||
return cm4.Dst
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// udpMakeOOBWithSrc - make OOB data with a specified source IP
|
||||
func udpMakeOOBWithSrc(ip net.IP) []byte {
|
||||
if ip.To4() == nil {
|
||||
cm := &ipv6.ControlMessage{}
|
||||
cm.Src = ip
|
||||
return cm.Marshal()
|
||||
}
|
||||
|
||||
cm := &ipv4.ControlMessage{}
|
||||
cm.Src = ip
|
||||
return cm.Marshal()
|
||||
}
|
30
udp_windows.go
Normal file
30
udp_windows.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package dnscrypt
|
||||
|
||||
import "net"
|
||||
|
||||
// udpGetOOBSize - get max. size of received OOB data
|
||||
// Does nothing on Windows
|
||||
func udpGetOOBSize() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// udpSetOptions - set options on a UDP socket to be able to receive the necessary OOB data
|
||||
// Does nothing on Windows
|
||||
func udpSetOptions(c *net.UDPConn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// udpRead - receive payload from the UDP socket
|
||||
func udpRead(c *net.UDPConn, buf []byte, _ int) (int, net.IP, *net.UDPAddr, error) {
|
||||
n, addr, err := c.ReadFrom(buf)
|
||||
var udpAddr *net.UDPAddr
|
||||
if addr != nil {
|
||||
udpAddr = addr.(*net.UDPAddr)
|
||||
}
|
||||
return n, nil, udpAddr, err
|
||||
}
|
||||
|
||||
// udpWrite - writes to the UDP socket
|
||||
func udpWrite(bytes []byte, conn *net.UDPConn, remoteAddr *net.UDPAddr, _ net.IP) (int, error) {
|
||||
return conn.WriteTo(bytes, remoteAddr)
|
||||
}
|
251
util.go
Normal file
251
util.go
Normal file
|
@ -0,0 +1,251 @@
|
|||
package dnscrypt
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/ameshkov/dnscrypt/xsecretbox"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
// Prior to encryption, queries are padded using the ISO/IEC 7816-4
|
||||
// format. The padding starts with a byte valued 0x80 followed by a
|
||||
// variable number of NUL bytes.
|
||||
//
|
||||
// ## Padding for client queries over UDP
|
||||
//
|
||||
// <client-query> <client-query-pad> must be at least <min-query-len>
|
||||
// bytes. If the length of the client query is less than <min-query-len>,
|
||||
// the padding length must be adjusted in order to satisfy this
|
||||
// requirement.
|
||||
//
|
||||
// <min-query-len> is a variable length, initially set to 256 bytes, and
|
||||
// must be a multiple of 64 bytes.
|
||||
//
|
||||
// ## Padding for client queries over TCP
|
||||
//
|
||||
// The length of <client-query-pad> is randomly chosen between 1 and 256
|
||||
// bytes (including the leading 0x80), but the total length of <client-query>
|
||||
// <client-query-pad> must be a multiple of 64 bytes.
|
||||
//
|
||||
// For example, an originally unpadded 56-bytes DNS query can be padded as:
|
||||
//
|
||||
// <56-bytes-query> 0x80 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
// or
|
||||
// <56-bytes-query> 0x80 (0x00 * 71)
|
||||
// or
|
||||
// <56-bytes-query> 0x80 (0x00 * 135)
|
||||
// or
|
||||
// <56-bytes-query> 0x80 (0x00 * 199)
|
||||
func pad(packet []byte) []byte {
|
||||
// get closest divisible by 64 to <packet-len> + 1 byte for 0x80
|
||||
minQuestionSize := (len(packet)+1+63)/64 + 64
|
||||
|
||||
// padded size can't be less than minUDPQuestionSize
|
||||
minQuestionSize = max(minUDPQuestionSize, minQuestionSize)
|
||||
|
||||
packet = append(packet, 0x80)
|
||||
for len(packet) < minQuestionSize {
|
||||
packet = append(packet, 0)
|
||||
}
|
||||
|
||||
return packet
|
||||
}
|
||||
|
||||
// unpad - removes padding bytes
|
||||
func unpad(packet []byte) ([]byte, error) {
|
||||
for i := len(packet); ; {
|
||||
if i == 0 {
|
||||
return nil, ErrInvalidPadding
|
||||
}
|
||||
i--
|
||||
if packet[i] == 0x80 {
|
||||
if i < minDNSPacketSize {
|
||||
return nil, ErrInvalidPadding
|
||||
}
|
||||
|
||||
return packet[:i], nil
|
||||
} else if packet[i] != 0x00 {
|
||||
return nil, ErrInvalidPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// computeSharedKey - computes a shared key
|
||||
func computeSharedKey(cryptoConstruction CryptoConstruction, secretKey *[keySize]byte, publicKey *[keySize]byte) ([keySize]byte, error) {
|
||||
if cryptoConstruction == XChacha20Poly1305 {
|
||||
sharedKey, err := xsecretbox.SharedKey(*secretKey, *publicKey)
|
||||
if err != nil {
|
||||
return sharedKey, err
|
||||
}
|
||||
return sharedKey, nil
|
||||
} else if cryptoConstruction == XSalsa20Poly1305 {
|
||||
sharedKey := [sharedKeySize]byte{}
|
||||
box.Precompute(&sharedKey, publicKey, secretKey)
|
||||
return sharedKey, nil
|
||||
}
|
||||
return [keySize]byte{}, ErrEsVersion
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func isDigit(b byte) bool { return b >= '0' && b <= '9' }
|
||||
|
||||
func dddToByte(s []byte) byte {
|
||||
return (s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0')
|
||||
}
|
||||
|
||||
const (
|
||||
escapedByteSmall = "" +
|
||||
`\000\001\002\003\004\005\006\007\008\009` +
|
||||
`\010\011\012\013\014\015\016\017\018\019` +
|
||||
`\020\021\022\023\024\025\026\027\028\029` +
|
||||
`\030\031`
|
||||
escapedByteLarge = `\127\128\129` +
|
||||
`\130\131\132\133\134\135\136\137\138\139` +
|
||||
`\140\141\142\143\144\145\146\147\148\149` +
|
||||
`\150\151\152\153\154\155\156\157\158\159` +
|
||||
`\160\161\162\163\164\165\166\167\168\169` +
|
||||
`\170\171\172\173\174\175\176\177\178\179` +
|
||||
`\180\181\182\183\184\185\186\187\188\189` +
|
||||
`\190\191\192\193\194\195\196\197\198\199` +
|
||||
`\200\201\202\203\204\205\206\207\208\209` +
|
||||
`\210\211\212\213\214\215\216\217\218\219` +
|
||||
`\220\221\222\223\224\225\226\227\228\229` +
|
||||
`\230\231\232\233\234\235\236\237\238\239` +
|
||||
`\240\241\242\243\244\245\246\247\248\249` +
|
||||
`\250\251\252\253\254\255`
|
||||
)
|
||||
|
||||
// escapeByte returns the \DDD escaping of b which must
|
||||
// satisfy b < ' ' || b > '~'.
|
||||
func escapeByte(b byte) string {
|
||||
if b < ' ' {
|
||||
return escapedByteSmall[b*4 : b*4+4]
|
||||
}
|
||||
|
||||
b -= '~' + 1
|
||||
// The cast here is needed as b*4 may overflow byte.
|
||||
return escapedByteLarge[int(b)*4 : int(b)*4+4]
|
||||
}
|
||||
|
||||
func packTxtString(buf []byte) string {
|
||||
var out strings.Builder
|
||||
out.Grow(3 + len(buf))
|
||||
for i := 0; i < len(buf); i++ {
|
||||
b := buf[i]
|
||||
switch {
|
||||
case b == '"' || b == '\\':
|
||||
out.WriteByte('\\')
|
||||
out.WriteByte(b)
|
||||
case b < ' ' || b > '~':
|
||||
out.WriteString(escapeByte(b))
|
||||
default:
|
||||
out.WriteByte(b)
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func unpackTxtString(s string) ([]byte, error) {
|
||||
bs := make([]byte, len(s))
|
||||
msg := make([]byte, 0)
|
||||
copy(bs, s)
|
||||
for i := 0; i < len(bs); i++ {
|
||||
if bs[i] == '\\' {
|
||||
i++
|
||||
if i == len(bs) {
|
||||
break
|
||||
}
|
||||
if i+2 < len(bs) && isDigit(bs[i]) && isDigit(bs[i+1]) && isDigit(bs[i+2]) {
|
||||
msg = append(msg, dddToByte(bs[i:]))
|
||||
i += 2
|
||||
} else if bs[i] == 't' {
|
||||
msg = append(msg, '\t')
|
||||
} else if bs[i] == 'r' {
|
||||
msg = append(msg, '\r')
|
||||
} else if bs[i] == 'n' {
|
||||
msg = append(msg, '\n')
|
||||
} else {
|
||||
msg = append(msg, bs[i])
|
||||
}
|
||||
} else {
|
||||
msg = append(msg, bs[i])
|
||||
}
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// dnsSize returns if buffer size *advertised* in the requests OPT record.
|
||||
// Or when the request was over TCP, we return the maximum allowed size of 64K.
|
||||
func dnsSize(proto string, r *dns.Msg) int {
|
||||
size := uint16(0)
|
||||
if o := r.IsEdns0(); o != nil {
|
||||
size = o.UDPSize()
|
||||
}
|
||||
|
||||
if proto != "udp" {
|
||||
return dns.MaxMsgSize
|
||||
}
|
||||
|
||||
if size < dns.MinMsgSize {
|
||||
return dns.MinMsgSize
|
||||
}
|
||||
|
||||
// normalize size
|
||||
return int(size)
|
||||
}
|
||||
|
||||
// readPrefixed -- reads a DNS message with a 2-byte prefix containing message length
|
||||
func readPrefixed(conn net.Conn) ([]byte, error) {
|
||||
l := make([]byte, 2)
|
||||
_, err := conn.Read(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packetLen := binary.BigEndian.Uint16(l)
|
||||
if packetLen > dns.MaxMsgSize {
|
||||
return nil, ErrQueryTooLarge
|
||||
}
|
||||
|
||||
buf := make([]byte, packetLen)
|
||||
_, err = io.ReadFull(conn, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// writePrefixed -- write a DNS message to a TCP connection
|
||||
// it first writes a 2-byte prefix followed by the message itself
|
||||
func writePrefixed(b []byte, conn net.Conn) error {
|
||||
l := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(l, uint16(len(b)))
|
||||
_, err := (&net.Buffers{l, b}).WriteTo(conn)
|
||||
return err
|
||||
}
|
||||
|
||||
// isConnClosed - checks if the error signals of a closed server connecting
|
||||
func isConnClosed(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
nerr, ok := err.(*net.OpError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.Contains(nerr.Err.Error(), "use of closed network connection") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
Loading…
Reference in a new issue