1
0
Fork 0
mirror of https://github.com/SamTherapy/dnscrypt.git synced 2024-07-02 21:56:06 +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:
Andrey Meshkov 2020-10-19 17:35:08 +03:00 committed by GitHub
commit 1b4a041840
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 3244 additions and 766 deletions

View file

@ -2,7 +2,7 @@ coverage:
status:
project:
default:
target: 40%
target: 60%
threshold: null
patch: false
changes: false

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
.idea
.vscode
coverage.txt

View file

@ -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
View 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
View file

@ -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)
```

Binary file not shown.

25
build/windows-386/LICENSE Normal file
View 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
View 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)
```

Binary file not shown.

176
cert.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"
}
}

View file

@ -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
}
}
}

View file

@ -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
View file

@ -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
View 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
View 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
View 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
}

View 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
View 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
View 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
View file

@ -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
View file

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}