├── .codecov.yml
├── .github
└── workflows
│ ├── build.yaml
│ └── lint.yaml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── Makefile
├── README.md
├── cert.go
├── cert_test.go
├── client.go
├── client_test.go
├── cmd
├── convert_dnscrypt_wrapper.go
├── generate.go
├── lookup.go
├── main.go
└── server.go
├── constants.go
├── doc.go
├── encrypted_query.go
├── encrypted_query_test.go
├── encrypted_response.go
├── encrypted_response_test.go
├── generate.go
├── generate_test.go
├── go.mod
├── go.sum
├── handler.go
├── server.go
├── server_bench_test.go
├── server_tcp.go
├── server_test.go
├── server_udp.go
├── testdata
└── dnscrypt-cert.opendns.txt
├── util.go
├── util_test.go
└── xsecretbox
├── doc.go
├── sharedkey.go
├── xsecretbox.go
└── xsecretbox_test.go
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | target: 60%
6 | threshold: null
7 | patch: false
8 | changes: false
9 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | 'on':
4 | 'push':
5 | 'tags':
6 | - 'v*'
7 | 'branches':
8 | - '*'
9 | 'pull_request':
10 |
11 | jobs:
12 | tests:
13 | runs-on: ${{ matrix.os }}
14 | env:
15 | GO111MODULE: "on"
16 | strategy:
17 | matrix:
18 | os:
19 | - windows-latest
20 | - macos-latest
21 | - ubuntu-latest
22 |
23 | steps:
24 | - uses: actions/checkout@master
25 |
26 | - uses: actions/setup-go@v3
27 | with:
28 | go-version: 1.x
29 |
30 | - name: Run tests
31 | run: |-
32 | go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
33 |
34 | - name: Upload coverage
35 | uses: codecov/codecov-action@v3
36 | if: "success() && matrix.os == 'ubuntu-latest'"
37 | with:
38 | token: ${{ secrets.CODECOV_TOKEN }}
39 | file: ./coverage.txt
40 |
41 | build:
42 | needs:
43 | - tests
44 | runs-on: ubuntu-latest
45 | env:
46 | GO111MODULE: "on"
47 | steps:
48 | - uses: actions/checkout@master
49 |
50 | - uses: actions/setup-go@v3
51 | with:
52 | go-version: 1.x
53 |
54 | - name: Prepare environment
55 | run: |-
56 | RELEASE_VERSION="${GITHUB_REF##*/}"
57 | if [[ "${RELEASE_VERSION}" != v* ]]; then RELEASE_VERSION='dev'; fi
58 | echo "RELEASE_VERSION=\"${RELEASE_VERSION}\"" >> $GITHUB_ENV
59 |
60 | # Win
61 | - run: GOOS=windows GOARCH=386 VERSION=${RELEASE_VERSION} make release
62 | - run: GOOS=windows GOARCH=amd64 VERSION=${RELEASE_VERSION} make release
63 |
64 | # MacOS
65 | - run: GOOS=darwin GOARCH=amd64 VERSION=${RELEASE_VERSION} make release
66 |
67 | # Linux X86
68 | - run: GOOS=linux GOARCH=386 VERSION=${RELEASE_VERSION} make release
69 | - run: GOOS=linux GOARCH=amd64 VERSION=${RELEASE_VERSION} make release
70 |
71 | # Linux ARM
72 | - run: GOOS=linux GOARCH=arm GOARM=6 VERSION=${RELEASE_VERSION} make release
73 | - run: GOOS=linux GOARCH=arm64 VERSION=${RELEASE_VERSION} make release
74 |
75 | # Linux MIPS/MIPSLE
76 | - run: GOOS=linux GOARCH=mips GOMIPS=softfloat VERSION=${RELEASE_VERSION} make release
77 | - run: GOOS=linux GOARCH=mipsle GOMIPS=softfloat VERSION=${RELEASE_VERSION} make release
78 |
79 | # FreeBSD X86
80 | - run: GOOS=freebsd GOARCH=386 VERSION=${RELEASE_VERSION} make release
81 | - run: GOOS=freebsd GOARCH=amd64 VERSION=${RELEASE_VERSION} make release
82 |
83 | # FreeBSD ARM/ARM64
84 | - run: GOOS=freebsd GOARCH=arm GOARM=6 VERSION=${RELEASE_VERSION} make release
85 | - run: GOOS=freebsd GOARCH=arm64 VERSION=${RELEASE_VERSION} make release
86 |
87 | - run: ls -l build/dnscrypt-*
88 |
89 | - name: Create release
90 | if: startsWith(github.ref, 'refs/tags/v')
91 | id: create_release
92 | uses: actions/create-release@v1
93 | env:
94 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
95 | with:
96 | tag_name: ${{ github.ref }}
97 | release_name: Release ${{ github.ref }}
98 | draft: false
99 | prerelease: false
100 |
101 | - name: Upload
102 | if: startsWith(github.ref, 'refs/tags/v')
103 | uses: xresloader/upload-to-github-release@v1
104 | env:
105 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
106 | with:
107 | file: "build/dnscrypt-*.tar.gz;build/dnscrypt-*.zip"
108 | tags: true
109 | draft: false
110 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: golangci-lint
2 | 'on':
3 | 'push':
4 | 'tags':
5 | - 'v*'
6 | 'branches':
7 | - '*'
8 | 'pull_request':
9 |
10 | jobs:
11 | golangci:
12 | runs-on:
13 | ${{ matrix.os }}
14 | strategy:
15 | matrix:
16 | os:
17 | - ubuntu-latest
18 | - macos-latest
19 | steps:
20 | - uses: actions/checkout@v3
21 | - name: golangci-lint
22 | uses: golangci/golangci-lint-action@v2.3.0
23 | with:
24 | # This field is required. Dont set the patch version to always use
25 | # the latest patch version.
26 | version: v1.64.7
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | coverage.txt
4 | build
5 | dnscrypt
6 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | # options for analysis running
2 | run:
3 | # default concurrency is a available CPU number
4 | concurrency: 4
5 |
6 | # timeout for analysis, e.g. 30s, 5m, default is 1m
7 | deadline: 2m
8 |
9 | # all available settings of specific linters
10 | linters-settings:
11 | gocyclo:
12 | min-complexity: 20
13 |
14 | linters:
15 | enable:
16 | - errcheck
17 | - govet
18 | - ineffassign
19 | - staticcheck
20 | - unused
21 | - dupl
22 | - gocyclo
23 | - goimports
24 | - revive
25 | - gosec
26 | - misspell
27 | - stylecheck
28 | - unconvert
29 | disable-all: true
30 |
31 | issues:
32 | exclude-use-default: false
33 |
34 | # List of regexps of issue texts to exclude, empty list by default.
35 | # But independently of this option we use default exclude patterns,
36 | # it can be disabled by `exclude-use-default: false`. To list all
37 | # excluded by default patterns execute `golangci-lint run --help`
38 | exclude:
39 | # SA1019: rand.Read has been deprecated since Go 1.20
40 | # Exclude it for now, needs to be fixed later.
41 | - SA1019
42 | # gosec: Potential file inclusion via variable
43 | # Exclude as it is required it in the command-line tool.
44 | - G304
45 | # gosec: Use of weak random number generator
46 | # Used in tests.
47 | - G404
48 | # gosec: integer overflow conversion
49 | # Suppress it for now, needs to be fixed later.
50 | - G115
51 | # gosec: slice bounds out of range
52 | # Suppress, these are false positives
53 | - G602
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
26 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | NAME=dnscrypt
2 | BASE_BUILDDIR=build
3 | BUILDNAME=$(GOOS)-$(GOARCH)$(GOARM)
4 | BUILDDIR=$(BASE_BUILDDIR)/$(BUILDNAME)
5 | VERSION?=dev
6 |
7 | ifeq ($(GOOS),windows)
8 | ext=.exe
9 | archiveCmd=zip -9 -r $(NAME)-$(BUILDNAME)-$(VERSION).zip $(BUILDNAME)
10 | else
11 | ext=
12 | archiveCmd=tar czpvf $(NAME)-$(BUILDNAME)-$(VERSION).tar.gz $(BUILDNAME)
13 | endif
14 |
15 | .PHONY: default
16 | default: build
17 |
18 | build: clean test
19 | go build -ldflags "-X main.VersionString=$(VERSION)" -o $(NAME)$(ext) ./cmd
20 |
21 | release: check-env-release
22 | mkdir -p $(BUILDDIR)
23 | cp LICENSE $(BUILDDIR)/
24 | cp README.md $(BUILDDIR)/
25 | CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags "-X main.VersionString=$(VERSION)" -o $(BUILDDIR)/$(NAME)$(ext) ./cmd/
26 | cd $(BASE_BUILDDIR) ; $(archiveCmd)
27 |
28 | test:
29 | go test -race -v -bench=. ./...
30 |
31 | clean:
32 | go clean
33 | rm -rf $(BASE_BUILDDIR)
34 |
35 | check-env-release:
36 | @ if [ "$(GOOS)" = "" ]; then \
37 | echo "Environment variable GOOS not set"; \
38 | exit 1; \
39 | fi
40 | @ if [ "$(GOARCH)" = "" ]; then \
41 | echo "Environment variable GOOS not set"; \
42 | exit 1; \
43 | fi
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://codecov.io/github/ameshkov/dnscrypt?branch=master)
2 | [](https://goreportcard.com/report/ameshkov/dnscrypt)
3 | [](https://godoc.org/github.com/ameshkov/dnscrypt)
4 |
5 | # DNSCrypt Go
6 |
7 | Golang-implementation of the [DNSCrypt v2 protocol](https://dnscrypt.info/protocol).
8 |
9 | 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.
10 |
11 | * [Command-line tool](#commandline)
12 | * [How to install](#install)
13 | * [How to configure](#configure)
14 | * [Converting dnscrypt-wrapper configuration](#convertfromwrapper)
15 | * [Running a server](#runningserver)
16 | * [Making lookups](#lookup)
17 | * [Programming interface](#api)
18 | * [Client](#client)
19 | * [Server](#server)
20 |
21 | ## Command-line tool
22 |
23 | `dnscrypt` is a helper tool that can work as a DNSCrypt client or server.
24 |
25 | 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.
26 |
27 |
28 | ### How to install
29 |
30 | Download and unpack an archive for your platform from the [latest release](https://github.com/ameshkov/dnscrypt/releases).
31 |
32 | Homebrew:
33 | ```
34 | brew install ameshkov/tap/dnscrypt
35 | ```
36 |
37 | ### How to configure
38 |
39 | Generate a configuration file for running a DNSCrypt server:
40 |
41 | ```
42 | ./dnscrypt generate
43 |
44 | [generate command options]
45 | -p, --provider-name= DNSCrypt provider name. Param is required.
46 | -o, --out= Path to the resulting config file. Param is required.
47 | -k, --private-key= Private key (hex-encoded)
48 | -t, --ttl= Certificate time-to-live (seconds)
49 | ```
50 |
51 | It will generate a configuration file that looks like this:
52 |
53 | ```yaml
54 | provider_name: 2.dnscrypt-cert.example.org
55 | public_key: F11DDBCC4817E543845FDDD4CB881849B64226F3DE397625669D87B919BC4FB0
56 | private_key: 5752095FFA56D963569951AFE70FE1690F378D13D8AD6F8054DFAA100907F8B6F11DDBCC4817E543845FDDD4CB881849B64226F3DE397625669D87B919BC4FB0
57 | resolver_secret: 9E46E79FEB3AB3D45F4EB3EA957DEAF5D9639A0179F1850AFABA7E58F87C74C4
58 | resolver_public: 9327C5E64783E19C339BD6B680A56DB85521CC6E4E0CA5DF5274E2D3CE026C6B
59 | es_version: 1
60 | certificate_ttl: 0s
61 | ```
62 |
63 | * `provider_name` - DNSCrypt resolver name.
64 | * `public_key`, `private_key` - keypair that is used by the DNSCrypt resolver to sign the certificate.
65 | * `resolver_secret`, `resolver_public` - keypair that is used by the DNSCrypt resolver to encrypt and decrypt messages.
66 | * `es_version` - crypto to use. Can be `1` (XSalsa20Poly1305) or `2` (XChacha20Poly1305).
67 | * `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.
68 |
69 | #### Converting [dnscrypt-wrapper](https://github.com/cofyc/dnscrypt-wrapper) configuration
70 |
71 | Also, to create a configuration, you can use the keys generated using [dnscrypt-wrapper](https://github.com/cofyc/dnscrypt-wrapper) by running the command:
72 |
73 | ```
74 | ./dnscrypt convert-dnscrypt-wrapper
75 |
76 | [convert-dnscrypt-wrapper command options]
77 | -p, --private-key= Path to the DNSCrypt resolver private key file that is used for signing certificates. Param is required.
78 | -r, --resolver-secret= Path to the Short-term privacy key file for encrypting/decrypting DNS queries. If not specified, resolver_secret and resolver_public will be randomly generated.
79 | -n, --provider-name= DNSCrypt provider name. Param is required.
80 | -o, --out= Path to the resulting config file. Param is required.
81 | -t, --ttl= Certificate time-to-live (seconds)
82 | ```
83 |
84 |
85 | ### Running a server
86 |
87 | This configuration file can be used to run a DNSCrypt forwarding server:
88 |
89 | ```
90 | ./dnscrypt server
91 |
92 | [server command options]
93 | -c, --config= Path to the DNSCrypt configuration file. Param is required.
94 | -f, --forward= Forwards DNS queries to the specified address (default: 94.140.14.140:53)
95 | -l, --listen= Listening addresses (default: 0.0.0.0)
96 | -p, --port= Listening ports (default: 443)
97 | ```
98 |
99 | 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`:
100 |
101 | ```
102 | sdns://AQcAAAAAAAAADTEyNy4wLjAuMTo0NDMg8R3bzEgX5UOEX93Uy4gYSbZCJvPeOXYlZp2HuRm8T7AbMi5kbnNjcnlwdC1jZXJ0LmV4YW1wbGUub3Jn
103 | ```
104 |
105 | ### Making lookups
106 |
107 | You can use that stamp to send a DNSCrypt request to your server:
108 |
109 | ```
110 | ./dnscrypt lookup-stamp
111 |
112 | [lookup-stamp command options]
113 | -n, --network= network type (tcp/udp) (default: udp)
114 | -s, --stamp= DNSCrypt resolver stamp. Param is required.
115 | -d, --domain= Domain to resolve. Param is required.
116 | -t, --type= DNS query type (default: A)
117 | ```
118 |
119 | You can also send a DNSCrypt request using a command that does not require stamps:
120 |
121 | ```
122 | ./dnscrypt lookup \
123 |
124 | [lookup command options]
125 | -n, --network= network type (tcp/udp) (default: udp)
126 | -p, --provider-name= DNSCrypt resolver provider name. Param is required.
127 | -k, --public-key= DNSCrypt resolver public key. Param is required.
128 | -a, --addr= Resolver address (IP[:port]). By default, the port is 443. Param is required.
129 | -d, --domain= Domain to resolve. Param is required.
130 | -t, --type= DNS query type (default: A)
131 | ```
132 |
133 | ## Programming interface
134 |
135 | ### Client
136 |
137 | ```go
138 | import (
139 | "github.com/ameshkov/dnscrypt/v2"
140 | )
141 |
142 | // AdGuard DNS stamp
143 | stampStr := "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
144 |
145 | // Initializing the DNSCrypt client
146 | c := dnscrypt.Client{Net: "udp", Timeout: 10 * time.Second}
147 |
148 | // Fetching and validating the server certificate
149 | resolverInfo, err := c.Dial(stampStr)
150 | if err != nil {
151 | return err
152 | }
153 |
154 | // Create a DNS request
155 | req := dns.Msg{}
156 | req.Id = dns.Id()
157 | req.RecursionDesired = true
158 | req.Question = []dns.Question{
159 | {
160 | Name: "google-public-dns-a.google.com.",
161 | Qtype: dns.TypeA,
162 | Qclass: dns.ClassINET,
163 | },
164 | }
165 |
166 | // Get the DNS response
167 | reply, err := c.Exchange(&req, resolverInfo)
168 | ```
169 |
170 | ## Server
171 |
172 | ```go
173 | import (
174 | "github.com/ameshkov/dnscrypt/v2"
175 | )
176 |
177 | // Prepare the test DNSCrypt server config
178 | rc, err := dnscrypt.GenerateResolverConfig("example.org", nil)
179 | if err != nil {
180 | return err
181 | }
182 |
183 | cert, err := rc.CreateCert()
184 | if err != nil {
185 | return err
186 | }
187 |
188 | s := &dnscrypt.Server{
189 | ProviderName: rc.ProviderName,
190 | ResolverCert: cert,
191 | Handler: dnscrypt.DefaultHandler,
192 | }
193 |
194 | // Prepare TCP listener
195 | tcpConn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4zero, Port: 443})
196 | if err != nil {
197 | return err
198 | }
199 |
200 | // Prepare UDP listener
201 | udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 443})
202 | if err != nil {
203 | return err
204 | }
205 |
206 | // Start the server
207 | go s.ServeUDP(udpConn)
208 | go s.ServeTCP(tcpConn)
209 | ```
210 |
--------------------------------------------------------------------------------
/cert.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "bytes"
5 | "crypto/ed25519"
6 | "encoding/binary"
7 | "fmt"
8 | "time"
9 | )
10 |
11 | // Cert is a DNSCrypt server certificate
12 | // See ResolverConfig for more info on how to create one
13 | type Cert struct {
14 | // Serial is a 4 byte serial number in big-endian format. If more than
15 | // one certificates are valid, the client must prefer the certificate
16 | // with a higher serial number.
17 | Serial uint32
18 |
19 | // ::= the cryptographic construction to use with this
20 | // certificate.
21 | // For X25519-XSalsa20Poly1305, must be 0x00 0x01.
22 | // For X25519-XChacha20Poly1305, must be 0x00 0x02.
23 | EsVersion CryptoConstruction
24 |
25 | // Signature is a 64-byte signature of (
26 | // ) using the Ed25519 algorithm and the
27 | // provider secret key. Ed25519 must be used in this version of the
28 | // protocol.
29 | Signature [ed25519.SignatureSize]byte
30 |
31 | // ResolverPk is the resolver's short-term public key, which is 32 bytes when using X25519.
32 | // This key is used to encrypt/decrypt DNS queries
33 | ResolverPk [keySize]byte
34 |
35 | // ResolverSk is the resolver's short-term private key, which is 32 bytes when using X25519.
36 | // Note that it's only used in the server implementation and never serialized/deserialized.
37 | // This key is used to encrypt/decrypt DNS queries
38 | ResolverSk [keySize]byte
39 |
40 | // ClientMagic is the first 8 bytes of a client query that is to be built
41 | // using the information from this certificate. It may be a truncated
42 | // public key. Two valid certificates cannot share the same .
43 | ClientMagic [clientMagicSize]byte
44 |
45 | // NotAfter is the date the certificate is valid from, as a big-endian
46 | // 4-byte unsigned Unix timestamp.
47 | NotBefore uint32
48 |
49 | // NotAfter is the date the certificate is valid until (inclusive), as a
50 | // big-endian 4-byte unsigned Unix timestamp.
51 | NotAfter uint32
52 | }
53 |
54 | // Serialize serializes the cert to bytes
55 | // ::=
56 | //
57 | //
58 | // Certificates made of these information, without extensions, are 116 bytes
59 | // long. With the addition of the cert-magic, es-version and
60 | // protocol-minor-version, the record is 124 bytes long.
61 | func (c *Cert) Serialize() ([]byte, error) {
62 | // validate
63 | if c.EsVersion == UndefinedConstruction {
64 | return nil, ErrEsVersion
65 | }
66 |
67 | if !c.VerifyDate() {
68 | return nil, ErrInvalidDate
69 | }
70 |
71 | // start serializing
72 | b := make([]byte, 124)
73 |
74 | //
75 | copy(b[:4], certMagic[:])
76 | //
77 | binary.BigEndian.PutUint16(b[4:6], uint16(c.EsVersion))
78 | // - always 0x00 0x00
79 | copy(b[6:8], []byte{0, 0})
80 | //
81 | copy(b[8:72], c.Signature[:ed25519.SignatureSize])
82 | // signed: ( )
83 | c.writeSigned(b[72:])
84 |
85 | // done
86 | return b, nil
87 | }
88 |
89 | // Deserialize deserializes certificate from a byte array
90 | // ::=
91 | //
92 | //
93 | func (c *Cert) Deserialize(b []byte) error {
94 | if len(b) < 124 {
95 | return ErrCertTooShort
96 | }
97 |
98 | //
99 | if !bytes.Equal(b[:4], certMagic[:4]) {
100 | return ErrCertMagic
101 | }
102 |
103 | //
104 | switch esVersion := binary.BigEndian.Uint16(b[4:6]); esVersion {
105 | case uint16(XSalsa20Poly1305):
106 | c.EsVersion = XSalsa20Poly1305
107 | case uint16(XChacha20Poly1305):
108 | c.EsVersion = XChacha20Poly1305
109 | default:
110 | return ErrEsVersion
111 | }
112 |
113 | // Ignore 6:8,
114 | //
115 | copy(c.Signature[:], b[8:72])
116 | //
117 | copy(c.ResolverPk[:], b[72:104])
118 | //
119 | copy(c.ClientMagic[:], b[104:112])
120 | //
121 | c.Serial = binary.BigEndian.Uint32(b[112:116])
122 | //
123 | c.NotBefore = binary.BigEndian.Uint32(b[116:120])
124 | c.NotAfter = binary.BigEndian.Uint32(b[120:124])
125 |
126 | // Deserialized with no issues
127 | return nil
128 | }
129 |
130 | // VerifyDate checks that the cert is valid at this moment
131 | func (c *Cert) VerifyDate() bool {
132 | if c.NotBefore >= c.NotAfter {
133 | return false
134 | }
135 | now := uint32(time.Now().Unix())
136 | if now > c.NotAfter || now < c.NotBefore {
137 | return false
138 | }
139 | return true
140 | }
141 |
142 | // VerifySignature checks if the cert is properly signed with the specified signature
143 | func (c *Cert) VerifySignature(publicKey ed25519.PublicKey) bool {
144 | b := make([]byte, 52)
145 | c.writeSigned(b)
146 | return ed25519.Verify(publicKey, b, c.Signature[:])
147 | }
148 |
149 | // Sign creates cert.Signature
150 | func (c *Cert) Sign(privateKey ed25519.PrivateKey) {
151 | b := make([]byte, 52)
152 | c.writeSigned(b)
153 | signature := ed25519.Sign(privateKey, b)
154 | copy(c.Signature[:64], signature[:64])
155 | }
156 |
157 | // String Cert's string representation
158 | func (c *Cert) String() string {
159 | return fmt.Sprintf("Certificate Serial=%d NotBefore=%s NotAfter=%s EsVersion=%s",
160 | c.Serial, time.Unix(int64(c.NotBefore), 0).String(),
161 | time.Unix(int64(c.NotAfter), 0).String(), c.EsVersion.String())
162 | }
163 |
164 | // writeSigned writes ( )
165 | func (c *Cert) writeSigned(dst []byte) {
166 | //
167 | copy(dst[:32], c.ResolverPk[:keySize])
168 | //
169 | copy(dst[32:40], c.ClientMagic[:clientMagicSize])
170 | //
171 | binary.BigEndian.PutUint32(dst[40:44], c.Serial)
172 | //
173 | binary.BigEndian.PutUint32(dst[44:48], c.NotBefore)
174 | //
175 | binary.BigEndian.PutUint32(dst[48:52], c.NotAfter)
176 | }
177 |
--------------------------------------------------------------------------------
/cert_test.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "bytes"
5 | "crypto/ed25519"
6 | "crypto/rand"
7 | "os"
8 | "testing"
9 | "time"
10 |
11 | "github.com/stretchr/testify/require"
12 | )
13 |
14 | func TestCertSerialize(t *testing.T) {
15 | cert, publicKey, _ := generateValidCert(t)
16 |
17 | // not empty anymore
18 | require.False(t, bytes.Equal(cert.Signature[:], make([]byte, 64)))
19 |
20 | // verify the signature
21 | require.True(t, cert.VerifySignature(publicKey))
22 |
23 | // serialize
24 | b, err := cert.Serialize()
25 | require.NoError(t, err)
26 | require.Equal(t, 124, len(b))
27 |
28 | // check that we can deserialize it
29 | cert2 := Cert{}
30 | err = cert2.Deserialize(b)
31 | require.NoError(t, err)
32 | require.Equal(t, cert.Serial, cert2.Serial)
33 | require.Equal(t, cert.NotBefore, cert2.NotBefore)
34 | require.Equal(t, cert.NotAfter, cert2.NotAfter)
35 | require.Equal(t, cert.EsVersion, cert2.EsVersion)
36 | require.True(t, bytes.Equal(cert.ClientMagic[:], cert2.ClientMagic[:]))
37 | require.True(t, bytes.Equal(cert.ResolverPk[:], cert2.ResolverPk[:]))
38 | require.True(t, bytes.Equal(cert.Signature[:], cert2.Signature[:]))
39 | }
40 |
41 | func TestCertDeserialize(t *testing.T) {
42 | // dig -t txt 2.dnscrypt-cert.opendns.com. -p 443 @208.67.220.220
43 | certBytes, err := os.ReadFile("testdata/dnscrypt-cert.opendns.txt")
44 | require.NoError(t, err)
45 |
46 | b, err := unpackTxtString(string(certBytes))
47 | require.NoError(t, err)
48 |
49 | cert := &Cert{}
50 | err = cert.Deserialize(b)
51 | require.NoError(t, err)
52 | require.Equal(t, uint32(1574811744), cert.Serial)
53 | require.Equal(t, XSalsa20Poly1305, cert.EsVersion)
54 | require.Equal(t, uint32(1574811744), cert.NotBefore)
55 | require.Equal(t, uint32(1606347744), cert.NotAfter)
56 | }
57 |
58 | func generateValidCert(t *testing.T) (*Cert, ed25519.PublicKey, ed25519.PrivateKey) {
59 | cert := &Cert{
60 | Serial: 1,
61 | NotAfter: uint32(time.Now().Add(1 * time.Hour).Unix()),
62 | NotBefore: uint32(time.Now().Add(-1 * time.Hour).Unix()),
63 | EsVersion: XChacha20Poly1305,
64 | }
65 |
66 | // generate short-term resolver private key
67 | resolverSk, resolverPk := generateRandomKeyPair()
68 | copy(cert.ResolverPk[:], resolverPk[:])
69 | copy(cert.ResolverSk[:], resolverSk[:])
70 |
71 | // empty at first
72 | require.True(t, bytes.Equal(cert.Signature[:], make([]byte, 64)))
73 |
74 | // generate private key
75 | publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
76 | require.NoError(t, err)
77 |
78 | // sign the data
79 | cert.Sign(privateKey)
80 |
81 | return cert, publicKey, privateKey
82 | }
83 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "crypto/ed25519"
5 | "encoding/binary"
6 | "fmt"
7 | "log/slog"
8 | "net"
9 | "strings"
10 | "time"
11 |
12 | "github.com/AdguardTeam/golibs/errors"
13 | "github.com/AdguardTeam/golibs/logutil/slogutil"
14 | "github.com/ameshkov/dnsstamps"
15 | "github.com/miekg/dns"
16 | )
17 |
18 | // Client is a DNSCrypt resolver client
19 | type Client struct {
20 | Net string // protocol (can be "udp" or "tcp", by default - "udp")
21 | Timeout time.Duration // read/write timeout
22 |
23 | // Logger is a logger instance for Client. If not set, slog.Default() will
24 | // be used.
25 | Logger *slog.Logger
26 |
27 | // UDPSize is the maximum size of a DNS response (or query) this client can
28 | // send or receive. If not set, we use dns.MinMsgSize by default.
29 | UDPSize int
30 | }
31 |
32 | // ResolverInfo contains DNSCrypt resolver information necessary for decryption/encryption
33 | type ResolverInfo struct {
34 | SecretKey [keySize]byte // Client short-term secret key
35 | PublicKey [keySize]byte // Client short-term public key
36 |
37 | ServerPublicKey ed25519.PublicKey // Resolver public key (this key is used to validate cert signature)
38 | ServerAddress string // Server IP address
39 | ProviderName string // Provider name
40 |
41 | ResolverCert *Cert // Certificate info (obtained with the first unencrypted DNS request)
42 | SharedKey [keySize]byte // Shared key that is to be used to encrypt/decrypt messages
43 | }
44 |
45 | // Dial fetches and validates DNSCrypt certificate from the given server
46 | // Data received during this call is then used for DNS requests encryption/decryption
47 | // stampStr is an sdns:// address which is parsed using go-dnsstamps package
48 | func (c *Client) Dial(stampStr string) (*ResolverInfo, error) {
49 | stamp, err := dnsstamps.NewServerStampFromString(stampStr)
50 | if err != nil {
51 | // Invalid SDNS stamp
52 | return nil, err
53 | }
54 |
55 | if stamp.Proto != dnsstamps.StampProtoTypeDNSCrypt {
56 | return nil, ErrInvalidDNSStamp
57 | }
58 |
59 | return c.DialStamp(stamp)
60 | }
61 |
62 | // DialStamp fetches and validates DNSCrypt certificate from the given server
63 | // Data received during this call is then used for DNS requests encryption/decryption
64 | func (c *Client) DialStamp(stamp dnsstamps.ServerStamp) (*ResolverInfo, error) {
65 | resolverInfo := &ResolverInfo{}
66 |
67 | // Generate the secret/public pair
68 | resolverInfo.SecretKey, resolverInfo.PublicKey = generateRandomKeyPair()
69 |
70 | // Set the provider properties
71 | resolverInfo.ServerPublicKey = stamp.ServerPk
72 | resolverInfo.ServerAddress = stamp.ServerAddrStr
73 | resolverInfo.ProviderName = stamp.ProviderName
74 |
75 | cert, err := c.fetchCert(stamp)
76 | if err != nil {
77 | return nil, err
78 | }
79 |
80 | resolverInfo.ResolverCert = cert
81 |
82 | // Compute shared key that we'll use to encrypt/decrypt messages
83 | sharedKey, err := computeSharedKey(cert.EsVersion, &resolverInfo.SecretKey, &cert.ResolverPk)
84 | if err != nil {
85 | return nil, err
86 | }
87 |
88 | resolverInfo.SharedKey = sharedKey
89 |
90 | return resolverInfo, nil
91 | }
92 |
93 | // Exchange performs a synchronous DNS query to the specified DNSCrypt server and returns a DNS response.
94 | // This method creates a new network connection for every call so avoid using it for TCP.
95 | // DNSCrypt cert needs to be fetched and validated prior to this call using the c.DialStamp method.
96 | func (c *Client) Exchange(m *dns.Msg, resolverInfo *ResolverInfo) (resp *dns.Msg, err error) {
97 | network := "udp"
98 | if c.Net == "tcp" {
99 | network = "tcp"
100 | }
101 |
102 | conn, err := net.Dial(network, resolverInfo.ServerAddress)
103 | if err != nil {
104 | return nil, fmt.Errorf("dialing: %w", err)
105 | }
106 | defer func() { err = errors.WithDeferred(err, conn.Close()) }()
107 |
108 | resp, err = c.ExchangeConn(conn, m, resolverInfo)
109 | if err != nil {
110 | return nil, fmt.Errorf("exchanging: %w", err)
111 | }
112 |
113 | return resp, nil
114 | }
115 |
116 | // ExchangeConn performs a synchronous DNS query to the specified DNSCrypt server and returns a DNS response.
117 | // DNSCrypt server information needs to be fetched and validated prior to this call using the c.DialStamp method
118 | func (c *Client) ExchangeConn(conn net.Conn, m *dns.Msg, resolverInfo *ResolverInfo) (*dns.Msg, error) {
119 | query, err := c.encrypt(m, resolverInfo)
120 | if err != nil {
121 | return nil, err
122 | }
123 |
124 | err = c.writeQuery(conn, query)
125 | if err != nil {
126 | return nil, err
127 | }
128 |
129 | b, err := c.readResponse(conn)
130 | if err != nil {
131 | return nil, err
132 | }
133 |
134 | res, err := c.decrypt(b, resolverInfo)
135 | if err != nil {
136 | return nil, err
137 | }
138 |
139 | return res, nil
140 | }
141 |
142 | // writeQuery writes query to the network connection
143 | // depending on the protocol we may write a 2-byte prefix or not
144 | func (c *Client) writeQuery(conn net.Conn, query []byte) error {
145 | var err error
146 |
147 | if c.Timeout > 0 {
148 | _ = conn.SetWriteDeadline(time.Now().Add(c.Timeout))
149 | }
150 |
151 | // Write to the connection
152 | if _, ok := conn.(*net.TCPConn); ok {
153 | l := make([]byte, 2)
154 | binary.BigEndian.PutUint16(l, uint16(len(query)))
155 | _, err = (&net.Buffers{l, query}).WriteTo(conn)
156 | } else {
157 | _, err = conn.Write(query)
158 | }
159 |
160 | return err
161 | }
162 |
163 | // readResponse reads response from the network connection
164 | // depending on the protocol, we may read a 2-byte prefix or not
165 | func (c *Client) readResponse(conn net.Conn) ([]byte, error) {
166 | if c.Timeout > 0 {
167 | _ = conn.SetReadDeadline(time.Now().Add(c.Timeout))
168 | }
169 |
170 | proto := "udp"
171 | if _, ok := conn.(*net.TCPConn); ok {
172 | proto = "tcp"
173 | }
174 |
175 | if proto == "udp" {
176 | bufSize := c.UDPSize
177 | if bufSize == 0 {
178 | bufSize = dns.MinMsgSize
179 | }
180 | response := make([]byte, bufSize)
181 | n, err := conn.Read(response)
182 | if err != nil {
183 | return nil, err
184 | }
185 | return response[:n], nil
186 | }
187 |
188 | // If we got here, this is a TCP connection
189 | // so we should read a 2-byte prefix first
190 | return readPrefixed(conn)
191 | }
192 |
193 | // encrypt encrypts a DNS message using shared key from the resolver info
194 | func (c *Client) encrypt(m *dns.Msg, resolverInfo *ResolverInfo) ([]byte, error) {
195 | q := EncryptedQuery{
196 | EsVersion: resolverInfo.ResolverCert.EsVersion,
197 | ClientMagic: resolverInfo.ResolverCert.ClientMagic,
198 | ClientPk: resolverInfo.PublicKey,
199 | }
200 | query, err := m.Pack()
201 | if err != nil {
202 | return nil, err
203 | }
204 | b, err := q.Encrypt(query, resolverInfo.SharedKey)
205 | if len(b) > c.maxQuerySize() {
206 | return nil, ErrQueryTooLarge
207 | }
208 |
209 | return b, err
210 | }
211 |
212 | // decrypts decrypts a DNS message using a shared key from the resolver info
213 | func (c *Client) decrypt(b []byte, resolverInfo *ResolverInfo) (*dns.Msg, error) {
214 | dr := EncryptedResponse{
215 | EsVersion: resolverInfo.ResolverCert.EsVersion,
216 | }
217 | msg, err := dr.Decrypt(b, resolverInfo.SharedKey)
218 | if err != nil {
219 | return nil, err
220 | }
221 |
222 | res := new(dns.Msg)
223 | err = res.Unpack(msg)
224 | if err != nil {
225 | return nil, err
226 | }
227 | return res, nil
228 | }
229 |
230 | // fetchCert loads DNSCrypt cert from the specified server
231 | func (c *Client) fetchCert(stamp dnsstamps.ServerStamp) (cert *Cert, err error) {
232 | providerName := stamp.ProviderName
233 | if !strings.HasSuffix(providerName, ".") {
234 | providerName = providerName + "."
235 | }
236 |
237 | query := new(dns.Msg)
238 | query.SetQuestion(providerName, dns.TypeTXT)
239 | // use 1252 as a UDPSize for this client to make sure the buffer is not too small
240 | client := dns.Client{Net: c.Net, UDPSize: uint16(1252), Timeout: c.Timeout}
241 | r, _, err := client.Exchange(query, stamp.ServerAddrStr)
242 | if err != nil {
243 | return nil, err
244 | }
245 |
246 | if r.Rcode != dns.RcodeSuccess {
247 | return nil, ErrFailedToFetchCert
248 | }
249 |
250 | currentCert := &Cert{}
251 | foundValid := false
252 | for _, rr := range r.Answer {
253 | txt, ok := rr.(*dns.TXT)
254 | if !ok {
255 | continue
256 | }
257 |
258 | cert, err = c.parseCert(stamp, currentCert, providerName, strings.Join(txt.Txt, ""))
259 | if err != nil {
260 | c.logger().Debug("bad cert", "provider", providerName, slogutil.KeyError, err)
261 |
262 | continue
263 | } else if cert == nil {
264 | // The certificate has been skipped due to Serial or EsVersion.
265 | continue
266 | }
267 |
268 | currentCert = cert
269 | foundValid = true
270 | }
271 |
272 | if foundValid {
273 | return currentCert, nil
274 | } else if err == nil {
275 | err = fmt.Errorf("no valid txt records for provider %q", providerName)
276 | }
277 |
278 | return nil, err
279 | }
280 |
281 | // parseCert parses a certificate from its string form and returns it if it has
282 | // priority over currentCert.
283 | func (c *Client) parseCert(
284 | stamp dnsstamps.ServerStamp,
285 | currentCert *Cert,
286 | providerName string,
287 | certStr string,
288 | ) (cert *Cert, err error) {
289 | certBytes, err := unpackTxtString(certStr)
290 | if err != nil {
291 | return nil, fmt.Errorf("unpacking txt record: %w", err)
292 | }
293 |
294 | cert = &Cert{}
295 | err = cert.Deserialize(certBytes)
296 | if err != nil {
297 | return nil, fmt.Errorf("deserializing cert for: %w", err)
298 | }
299 |
300 | c.logger().Debug(
301 | "fetched certificate",
302 | "provider",
303 | providerName,
304 | "cert_serial",
305 | cert.Serial,
306 | )
307 |
308 | if !cert.VerifyDate() {
309 | return nil, ErrInvalidDate
310 | }
311 |
312 | if !cert.VerifySignature(stamp.ServerPk) {
313 | return nil, ErrInvalidCertSignature
314 | }
315 |
316 | if cert.Serial < currentCert.Serial {
317 | c.logger().Debug(
318 | "cert superseded by a previous certificate",
319 | "provider",
320 | providerName,
321 | "cert_serial",
322 | cert.Serial,
323 | )
324 |
325 | return nil, nil
326 | }
327 |
328 | if cert.Serial > currentCert.Serial {
329 | return cert, nil
330 | }
331 |
332 | if cert.EsVersion <= currentCert.EsVersion {
333 | c.logger().Debug(
334 | "keeping the current cert es version",
335 | "provider",
336 | providerName,
337 | )
338 |
339 | return nil, nil
340 | }
341 |
342 | c.logger().Debug(
343 | "upgrading the construction",
344 | "provider",
345 | providerName,
346 | "es_version",
347 | currentCert.EsVersion,
348 | "new_es_version",
349 | cert.EsVersion,
350 | )
351 |
352 | return cert, nil
353 | }
354 |
355 | func (c *Client) maxQuerySize() int {
356 | if c.Net == "tcp" {
357 | return dns.MaxMsgSize
358 | }
359 |
360 | if c.UDPSize > 0 {
361 | return c.UDPSize
362 | }
363 |
364 | return dns.MinMsgSize
365 | }
366 |
367 | func (c *Client) logger() (l *slog.Logger) {
368 | if c.Logger == nil {
369 | return slog.Default()
370 | }
371 |
372 | return c.Logger
373 | }
374 |
--------------------------------------------------------------------------------
/client_test.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "net"
5 | "os"
6 | "testing"
7 | "time"
8 |
9 | "github.com/ameshkov/dnsstamps"
10 | "github.com/miekg/dns"
11 | "github.com/stretchr/testify/require"
12 | )
13 |
14 | func TestParseStamp(t *testing.T) {
15 | // Google DoH
16 | stampStr := "sdns://AgUAAAAAAAAAAAAOZG5zLmdvb2dsZS5jb20NL2V4cGVyaW1lbnRhbA"
17 | stamp, err := dnsstamps.NewServerStampFromString(stampStr)
18 |
19 | if err != nil || stamp.ProviderName == "" {
20 | t.Fatalf("Could not parse stamp %s: %s", stampStr, err)
21 | }
22 |
23 | require.Equal(t, stampStr, stamp.String())
24 | require.Equal(t, dnsstamps.StampProtoTypeDoH, stamp.Proto)
25 | require.Equal(t, "dns.google.com", stamp.ProviderName)
26 | require.Equal(t, "/experimental", stamp.Path)
27 |
28 | // AdGuard DNSCrypt
29 | stampStr = "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
30 | stamp, err = dnsstamps.NewServerStampFromString(stampStr)
31 |
32 | if err != nil || stamp.ProviderName == "" {
33 | t.Fatalf("Could not parse stamp %s: %s", stampStr, err)
34 | }
35 |
36 | require.Equal(t, stampStr, stamp.String())
37 | require.Equal(t, dnsstamps.StampProtoTypeDNSCrypt, stamp.Proto)
38 | require.Equal(t, "2.dnscrypt.default.ns1.adguard.com", stamp.ProviderName)
39 | require.Equal(t, "", stamp.Path)
40 | require.Equal(t, "94.140.14.14:5443", stamp.ServerAddrStr)
41 | require.Equal(t, keySize, len(stamp.ServerPk))
42 | }
43 |
44 | func TestInvalidStamp(t *testing.T) {
45 | client := Client{}
46 | _, err := client.Dial("sdns://AQIAAAAAAAAAFDE")
47 | require.NotNil(t, err)
48 | }
49 |
50 | func TestTimeoutOnDialError(t *testing.T) {
51 | // AdGuard DNS pointing to a wrong IP
52 | stampStr := "sdns://AQIAAAAAAAAADDguOC44Ljg6NTQ0MyDRK0fyUtzywrv4mRCG6vec5EldixbIoMQyLlLKPzkIcyIyLmRuc2NyeXB0LmRlZmF1bHQubnMxLmFkZ3VhcmQuY29t"
53 | client := Client{Timeout: 300 * time.Millisecond}
54 |
55 | _, err := client.Dial(stampStr)
56 | require.NotNil(t, err)
57 | require.True(t, os.IsTimeout(err))
58 | }
59 |
60 | func TestTimeoutOnDialExchange(t *testing.T) {
61 | // AdGuard DNS
62 | stampStr := "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
63 | client := Client{Timeout: 300 * time.Millisecond}
64 |
65 | serverInfo, err := client.Dial(stampStr)
66 | require.NoError(t, err)
67 |
68 | // Point it to an IP where there's no DNSCrypt server
69 | serverInfo.ServerAddress = "8.8.8.8:5443"
70 | req := createTestMessage()
71 |
72 | // Do exchange
73 | _, err = client.Exchange(req, serverInfo)
74 |
75 | // Check error
76 | require.NotNil(t, err)
77 | require.ErrorIs(t, err, os.ErrDeadlineExceeded)
78 | }
79 |
80 | func TestFetchCertPublicResolvers(t *testing.T) {
81 | testCases := []struct {
82 | name string
83 | stampStr string
84 | }{
85 | {
86 | name: "AdGuard DNS",
87 | stampStr: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
88 | },
89 | {
90 | name: "AdGuard DNS Family",
91 | stampStr: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
92 | },
93 | {
94 | name: "AdGuard DNS Unfiltered",
95 | stampStr: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
96 | },
97 | {
98 | name: "Cisco OpenDNS",
99 | stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
100 | },
101 | {
102 | name: "Cisco OpenDNS Family Shield",
103 | stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMTIzILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
104 | },
105 | {
106 | name: "Quad9",
107 | stampStr: "sdns://AQYAAAAAAAAAEzE0OS4xMTIuMTEyLjEwOjg0NDMgZ8hHuMh1jNEgJFVDvnVnRt803x2EwAuMRwNo34Idhj4ZMi5kbnNjcnlwdC1jZXJ0LnF1YWQ5Lm5ldA",
108 | },
109 | }
110 |
111 | for _, tc := range testCases {
112 | t.Run(tc.name, func(t *testing.T) {
113 | stamp, err := dnsstamps.NewServerStampFromString(tc.stampStr)
114 | require.NoError(t, err)
115 |
116 | c := &Client{
117 | Net: "udp",
118 | Timeout: time.Second * 5,
119 | }
120 | resolverInfo, err := c.DialStamp(stamp)
121 | require.NoError(t, err)
122 | require.NotNil(t, resolverInfo)
123 | require.True(t, resolverInfo.ResolverCert.VerifyDate())
124 | require.True(t, resolverInfo.ResolverCert.VerifySignature(stamp.ServerPk))
125 | })
126 | }
127 | }
128 |
129 | func TestExchangePublicResolvers(t *testing.T) {
130 | stamps := []struct {
131 | stampStr string
132 | }{
133 | {
134 | // AdGuard DNS
135 | stampStr: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
136 | },
137 | {
138 | // AdGuard DNS Family
139 | stampStr: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNTo1NDQzILgxXdexS27jIKRw3C7Wsao5jMnlhvhdRUXWuMm1AFq6ITIuZG5zY3J5cHQuZmFtaWx5Lm5zMS5hZGd1YXJkLmNvbQ",
140 | },
141 | {
142 | // AdGuard DNS Unfiltered
143 | stampStr: "sdns://AQMAAAAAAAAAEjk0LjE0MC4xNC4xNDA6NTQ0MyC16ETWuDo-PhJo62gfvqcN48X6aNvWiBQdvy7AZrLa-iUyLmRuc2NyeXB0LnVuZmlsdGVyZWQubnMxLmFkZ3VhcmQuY29t",
144 | },
145 | {
146 | // Cisco OpenDNS
147 | stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
148 | },
149 | {
150 | // Cisco OpenDNS Family Shield
151 | stampStr: "sdns://AQAAAAAAAAAADjIwOC42Ny4yMjAuMTIzILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ",
152 | },
153 | }
154 |
155 | for _, test := range stamps {
156 | stamp, err := dnsstamps.NewServerStampFromString(test.stampStr)
157 | require.NoError(t, err)
158 |
159 | t.Run(stamp.ProviderName, func(t *testing.T) {
160 | checkDNSCryptServer(t, test.stampStr, "udp")
161 | checkDNSCryptServer(t, test.stampStr, "tcp")
162 | })
163 | }
164 | }
165 |
166 | func checkDNSCryptServer(t *testing.T, stampStr string, network string) {
167 | client := Client{Net: network, Timeout: 10 * time.Second}
168 | resolverInfo, err := client.Dial(stampStr)
169 | require.NoError(t, err)
170 |
171 | req := createTestMessage()
172 |
173 | reply, err := client.Exchange(req, resolverInfo)
174 | require.NoError(t, err)
175 | assertTestMessageResponse(t, reply)
176 | }
177 |
178 | func createTestMessage() *dns.Msg {
179 | req := dns.Msg{}
180 | req.Id = dns.Id()
181 | req.RecursionDesired = true
182 | req.Question = []dns.Question{
183 | {Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
184 | }
185 | return &req
186 | }
187 |
188 | func assertTestMessageResponse(t require.TestingT, reply *dns.Msg) {
189 | require.NotNil(t, reply)
190 | require.Equal(t, 1, len(reply.Answer))
191 | a, ok := reply.Answer[0].(*dns.A)
192 | require.True(t, ok)
193 | require.Equal(t, net.IPv4(8, 8, 8, 8).To4(), a.A.To4())
194 | }
195 |
--------------------------------------------------------------------------------
/cmd/convert_dnscrypt_wrapper.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/ed25519"
5 | "fmt"
6 | "os"
7 | "time"
8 |
9 | "github.com/AdguardTeam/golibs/log"
10 | "github.com/ameshkov/dnscrypt/v2"
11 | "golang.org/x/crypto/curve25519"
12 | "gopkg.in/yaml.v3"
13 | )
14 |
15 | // ConvertWrapperArgs is the "convert-dnscrypt-wrapper" command arguments structure
16 | type ConvertWrapperArgs struct {
17 | PrivateKeyFile string `short:"p" long:"private-key" description:"Path to the DNSCrypt resolver private key file that is used for signing certificates. Param is required." required:"true"`
18 | ResolverSkFile string `short:"r" long:"resolver-secret" description:"Path to the Short-term privacy key file for encrypting/decrypting DNS queries. If not specified, resolver_secret and resolver_public will be randomly generated."`
19 | ProviderName string `short:"n" long:"provider-name" description:"DNSCrypt provider name. Param is required." required:"true"`
20 | Out string `short:"o" long:"out" description:"Path to the resulting config file. Param is required." required:"true"`
21 | CertificateTTL int `short:"t" long:"ttl" description:"Certificate time-to-live (seconds)"`
22 | }
23 |
24 | // convertWrapper generates DNSCrypt configuration from both dnscrypt and server private keys
25 | func convertWrapper(args ConvertWrapperArgs) {
26 | log.Info("Generating configuration for %s", args.ProviderName)
27 |
28 | var rc = dnscrypt.ResolverConfig{
29 | EsVersion: dnscrypt.XSalsa20Poly1305,
30 | CertificateTTL: time.Duration(args.CertificateTTL) * time.Second,
31 | ProviderName: args.ProviderName,
32 | }
33 |
34 | // make PrivateKey
35 | var privateKey ed25519.PrivateKey
36 | privateKey = getFileContent(args.PrivateKeyFile)
37 | if len(privateKey) != ed25519.PrivateKeySize {
38 | log.Fatal("Invalid private key.")
39 | }
40 | rc.PrivateKey = dnscrypt.HexEncodeKey(privateKey)
41 |
42 | // make PublicKey
43 | publicKey := privateKey.Public().(ed25519.PublicKey)
44 | rc.PublicKey = dnscrypt.HexEncodeKey(publicKey)
45 |
46 | // make ResolverSk
47 | var resolverSecret ed25519.PrivateKey
48 | resolverSecret = getFileContent(args.ResolverSkFile)
49 | if len(resolverSecret) != 32 {
50 | log.Fatal("Invalid resolver secret key.")
51 | }
52 | rc.ResolverSk = dnscrypt.HexEncodeKey(resolverSecret)
53 |
54 | // make ResolverPk
55 | resolverPublic := getResolverPk(resolverSecret)
56 | rc.ResolverPk = dnscrypt.HexEncodeKey(resolverPublic)
57 |
58 | if err := validateRc(rc, publicKey); err != nil {
59 | log.Fatalf("Failed to validate resolver config, err: %s", err.Error())
60 | }
61 |
62 | out, err := yaml.Marshal(rc)
63 | if err != nil {
64 | log.Fatalf("Failed to marshall output config, err: %s", err.Error())
65 | }
66 |
67 | err = os.WriteFile(args.Out, out, 0600)
68 | if err != nil {
69 | log.Fatalf("Failed to write file, err: %s", err.Error())
70 | }
71 | }
72 |
73 | // validateRc verifies that the certificate is correctly
74 | // created and validated for this resolver config. if rc valid returns nil.
75 | func validateRc(rc dnscrypt.ResolverConfig, publicKey ed25519.PublicKey) error {
76 | cert, err := rc.CreateCert()
77 | if err != nil {
78 | return fmt.Errorf("failed to validate cert, err: %s", err.Error())
79 | }
80 | if cert == nil {
81 | return fmt.Errorf("created cert is empty")
82 | }
83 | if !cert.VerifyDate() {
84 | return fmt.Errorf("cert date is not valid")
85 | }
86 | if !cert.VerifySignature(publicKey) {
87 | return fmt.Errorf("cert signed incorrectly")
88 | }
89 | return nil
90 | }
91 |
92 | // getResolverPk calculates public key from private key
93 | func getResolverPk(private ed25519.PrivateKey) ed25519.PublicKey {
94 | resolverSk := [32]byte{}
95 | resolverPk := [32]byte{}
96 | copy(resolverSk[:], private)
97 | curve25519.ScalarBaseMult(&resolverPk, &resolverSk)
98 | return resolverPk[:]
99 | }
100 |
101 | func getFileContent(fname string) []byte {
102 | bytes, err := os.ReadFile(fname)
103 | if err != nil {
104 | log.Fatalf("Fail read key file %s, err: %s", fname, err.Error())
105 | }
106 | return bytes
107 | }
108 |
--------------------------------------------------------------------------------
/cmd/generate.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/AdguardTeam/golibs/log"
7 | "github.com/ameshkov/dnscrypt/v2"
8 | "gopkg.in/yaml.v3"
9 | )
10 |
11 | // GenerateArgs is the "generate" command arguments structure
12 | type GenerateArgs struct {
13 | ProviderName string `short:"p" long:"provider-name" description:"DNSCrypt provider name. Param is required." required:"true"`
14 | Out string `short:"o" long:"out" description:"Path to the resulting config file. Param is required." required:"true"`
15 | PrivateKey string `short:"k" long:"private-key" description:"Private key (hex-encoded)"`
16 | CertificateTTL int `short:"t" long:"ttl" description:"Certificate time-to-live (seconds)"`
17 | }
18 |
19 | // generate generates a DNSCrypt server configuration
20 | func generate(args GenerateArgs) {
21 | log.Info("Generating configuration for %s", args.ProviderName)
22 |
23 | var privateKey []byte
24 | var err error
25 | if args.PrivateKey != "" {
26 | privateKey, err = dnscrypt.HexDecodeKey(args.PrivateKey)
27 | if err != nil {
28 | log.Fatalf("failed to generate private key: %v", err)
29 | }
30 | }
31 |
32 | rc, err := dnscrypt.GenerateResolverConfig(args.ProviderName, privateKey)
33 | if err != nil {
34 | log.Fatalf("failed to generate resolver config: %v", err)
35 | }
36 |
37 | b, err := yaml.Marshal(rc)
38 | if err != nil {
39 | log.Fatalf("failed to serialize to yaml: %v", err)
40 | }
41 |
42 | // nolint
43 | err = os.WriteFile(args.Out, b, 0600)
44 | if err != nil {
45 | log.Fatalf("failed to save %s: %v", args.Out, err)
46 | }
47 |
48 | log.Info("Configuration has been written to %s", args.Out)
49 | log.Info("Go to https://dnscrypt.info/stamps to generate an SDNS stamp")
50 | log.Info("You can run a DNSCrypt server using the following command:")
51 | log.Info("dnscrypt server -c %s -f 8.8.8.8", args.Out)
52 | }
53 |
--------------------------------------------------------------------------------
/cmd/lookup.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "strings"
7 | "time"
8 |
9 | "github.com/AdguardTeam/golibs/log"
10 | "github.com/ameshkov/dnscrypt/v2"
11 | "github.com/ameshkov/dnsstamps"
12 | "github.com/miekg/dns"
13 | )
14 |
15 | // LookupStampArgs is the "lookup-stamp" command arguments structure
16 | type LookupStampArgs struct {
17 | Network string `short:"n" long:"network" description:"network type (tcp/udp)" default:"udp"`
18 | Stamp string `short:"s" long:"stamp" description:"DNSCrypt resolver stamp. Param is required." required:"true"`
19 | Domain string `short:"d" long:"domain" description:"Domain to resolve. Param is required." required:"true"`
20 | Type string `short:"t" long:"type" description:"DNS query type" default:"A"`
21 | }
22 |
23 | // LookupArgs is the "lookup" command arguments structure
24 | type LookupArgs struct {
25 | Network string `short:"n" long:"network" description:"network type (tcp/udp)" default:"udp"`
26 | ProviderName string `short:"p" long:"provider-name" description:"DNSCrypt resolver provider name. Param is required." required:"true"`
27 | PublicKey string `short:"k" long:"public-key" description:"DNSCrypt resolver public key. Param is required." required:"true"`
28 | ServerAddr string `short:"a" long:"addr" description:"Resolver address (IP[:port]). By default, the port is 443. Param is required." required:"true"`
29 | Domain string `short:"d" long:"domain" description:"Domain to resolve. Param is required." required:"true"`
30 | Type string `short:"t" long:"type" description:"DNS query type" default:"A"`
31 | }
32 |
33 | // LookupResult is the lookup result that contains the cert info and the query response
34 | type LookupResult struct {
35 | Certificate struct {
36 | Serial uint32 `json:"serial"`
37 | EsVersion string `json:"encryption"`
38 | NotAfter time.Time `json:"not_after"`
39 | NotBefore time.Time `json:"not_before"`
40 | } `json:"certificate"`
41 |
42 | Reply *dns.Msg `json:"reply"`
43 | }
44 |
45 | // lookup performs a DNS lookup, prints DNSCrypt info and lookup results
46 | func lookup(args LookupArgs) {
47 | serverPk, err := dnscrypt.HexDecodeKey(args.PublicKey)
48 | if err != nil {
49 | log.Fatalf("invalid resolver public key: %v", err)
50 | }
51 |
52 | stamp := dnsstamps.ServerStamp{
53 | ProviderName: args.ProviderName,
54 | ServerPk: serverPk,
55 | ServerAddrStr: args.ServerAddr,
56 | Proto: dnsstamps.StampProtoTypeDNSCrypt,
57 | }
58 |
59 | lookupStamp(LookupStampArgs{
60 | Network: args.Network,
61 | Stamp: stamp.String(),
62 | Domain: args.Domain,
63 | Type: args.Type,
64 | })
65 | }
66 |
67 | // lookupStamp performs a DNS lookup, prints DNSCrypt cert info and lookup results
68 | func lookupStamp(args LookupStampArgs) {
69 | c := &dnscrypt.Client{
70 | Net: args.Network,
71 | Timeout: 10 * time.Second,
72 | }
73 | ri, err := c.Dial(args.Stamp)
74 |
75 | if err != nil {
76 | log.Fatalf("failed to establish connection with the server: %v", err)
77 | }
78 |
79 | res := LookupResult{}
80 | res.Certificate.Serial = ri.ResolverCert.Serial
81 | res.Certificate.NotAfter = time.Unix(int64(ri.ResolverCert.NotAfter), 0)
82 | res.Certificate.NotBefore = time.Unix(int64(ri.ResolverCert.NotBefore), 0)
83 | res.Certificate.EsVersion = ri.ResolverCert.EsVersion.String()
84 |
85 | dnsType, ok := dns.StringToType[strings.ToUpper(args.Type)]
86 | if !ok {
87 | log.Fatalf("invalid type %s", args.Type)
88 | }
89 |
90 | req := &dns.Msg{}
91 | req.Id = dns.Id()
92 | req.RecursionDesired = true
93 | req.Question = []dns.Question{
94 | {
95 | Name: dns.Fqdn(args.Domain),
96 | Qtype: dnsType,
97 | Qclass: dns.ClassINET,
98 | },
99 | }
100 |
101 | reply, err := c.Exchange(req, ri)
102 | if err != nil {
103 | log.Fatalf("failed to resolve %s %s", args.Type, args.Domain)
104 | }
105 |
106 | res.Reply = reply
107 | b, err := json.MarshalIndent(res, "", " ")
108 | if err != nil {
109 | log.Fatalf("failed to marshal result to json: %v", err)
110 | }
111 |
112 | _, _ = os.Stdout.WriteString(string(b) + "\n")
113 | }
114 |
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
1 | // Package main is responsible for the command-line interface.
2 | package main
3 |
4 | import (
5 | "os"
6 |
7 | "github.com/AdguardTeam/golibs/log"
8 | goFlags "github.com/jessevdk/go-flags"
9 | )
10 |
11 | // Options - command-line options
12 | type Options struct {
13 | Generate GenerateArgs `command:"generate" description:"Generates DNSCrypt server configuration"`
14 | LookupStamp LookupStampArgs `command:"lookup-stamp" description:"Performs a DNSCrypt lookup for the specified domain using an sdns:// stamp"`
15 | Lookup LookupArgs `command:"lookup" description:"Performs a DNSCrypt lookup for the specified domain"`
16 | Server ServerArgs `command:"server" description:"Runs a DNSCrypt resolver"`
17 | ConvertWrapper ConvertWrapperArgs `command:"convert-dnscrypt-wrapper" description:"Converting keys generated with dnscrypt-wrapper to yaml config"`
18 | Version struct {
19 | } `command:"version" description:"Prints version"`
20 | }
21 |
22 | // VersionString will be set through ldflags, contains current version
23 | var VersionString = "1.0"
24 |
25 | func main() {
26 | var opts Options
27 |
28 | var parser = goFlags.NewParser(&opts, goFlags.Default)
29 | _, err := parser.Parse()
30 | if err != nil {
31 | if flagsErr, ok := err.(*goFlags.Error); ok && flagsErr.Type == goFlags.ErrHelp {
32 | os.Exit(0)
33 | }
34 |
35 | os.Exit(1)
36 | }
37 |
38 | switch parser.Active.Name {
39 | case "version":
40 | log.Printf("dnscrypt version %s\n", VersionString)
41 | case "generate":
42 | generate(opts.Generate)
43 | case "lookup-stamp":
44 | lookupStamp(opts.LookupStamp)
45 | case "lookup":
46 | lookup(opts.Lookup)
47 | case "server":
48 | server(opts.Server)
49 | case "convert-dnscrypt-wrapper":
50 | convertWrapper(opts.ConvertWrapper)
51 | default:
52 | log.Fatalf("unknown command %s", parser.Active.Name)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/cmd/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 |
9 | "github.com/AdguardTeam/golibs/log"
10 | "github.com/ameshkov/dnscrypt/v2"
11 | "github.com/miekg/dns"
12 | "gopkg.in/yaml.v3"
13 | )
14 |
15 | // ServerArgs is the "server" command arguments
16 | type ServerArgs struct {
17 | Config string `short:"c" long:"config" description:"Path to the DNSCrypt configuration file. Param is required." required:"true"`
18 | Forward string `short:"f" long:"forward" description:"Forwards DNS queries to the specified address" default:"94.140.14.140:53"`
19 | ListenAddrs []string `short:"l" long:"listen" description:"Listening addresses" default:"0.0.0.0"`
20 | ListenPorts []int `short:"p" long:"port" description:"Listening ports" default:"443"`
21 | }
22 |
23 | // server runs a DNSCrypt server
24 | func server(args ServerArgs) {
25 | log.Info("Starting DNSCrypt server")
26 |
27 | b, err := os.ReadFile(args.Config)
28 | if err != nil {
29 | log.Fatalf("failed to read the configuration: %v", err)
30 | }
31 |
32 | rc := dnscrypt.ResolverConfig{}
33 | err = yaml.Unmarshal(b, &rc)
34 | if err != nil {
35 | log.Fatalf("failed to deserialize configuration: %v", err)
36 | }
37 |
38 | cert, err := rc.CreateCert()
39 | if err != nil {
40 | log.Fatalf("failed to generate certificate: %v", err)
41 | }
42 |
43 | s := &dnscrypt.Server{
44 | ProviderName: rc.ProviderName,
45 | ResolverCert: cert,
46 | Handler: &forwardHandler{addr: args.Forward},
47 | }
48 |
49 | tcp, udp := createListeners(args)
50 | for _, t := range tcp {
51 | log.Info("Listening to tcp://%s", t.Addr().String())
52 | listen := t
53 | go func() { _ = s.ServeTCP(listen) }()
54 | }
55 | for _, u := range udp {
56 | log.Info("Listening to udp://%s", u.LocalAddr().String())
57 | listen := u
58 | go func() { _ = s.ServeUDP(listen) }()
59 | }
60 |
61 | signalChannel := make(chan os.Signal, 1)
62 | signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)
63 | <-signalChannel
64 |
65 | log.Info("Closing all listeners")
66 | for _, t := range tcp {
67 | _ = t.Close()
68 | }
69 | for _, u := range udp {
70 | _ = u.Close()
71 | }
72 | }
73 |
74 | // createListeners creates listeners for our server
75 | func createListeners(args ServerArgs) (tcp []net.Listener, udp []*net.UDPConn) {
76 | for _, addr := range args.ListenAddrs {
77 | ip := net.ParseIP(addr)
78 | if ip == nil {
79 | log.Fatalf("invalid listen address: %s", addr)
80 | }
81 |
82 | for _, port := range args.ListenPorts {
83 | tcpListen, err := net.ListenTCP("tcp", &net.TCPAddr{IP: ip, Port: port})
84 | if err != nil {
85 | log.Fatalf("failed to start TCP listener: %v", err)
86 | }
87 | udpListen, err := net.ListenUDP("udp", &net.UDPAddr{IP: ip, Port: port})
88 | if err != nil {
89 | log.Fatalf("failed to start UDP listener: %v", err)
90 | }
91 | tcp = append(tcp, tcpListen)
92 | udp = append(udp, udpListen)
93 | }
94 | }
95 |
96 | return
97 | }
98 |
99 | type forwardHandler struct {
100 | addr string
101 | }
102 |
103 | // type check
104 | var _ dnscrypt.Handler = &forwardHandler{}
105 |
106 | // ServeDNS implements Handler interface
107 | func (f *forwardHandler) ServeDNS(rw dnscrypt.ResponseWriter, r *dns.Msg) error {
108 | res, err := dns.Exchange(r, f.addr)
109 | if err != nil {
110 | return err
111 | }
112 | return rw.WriteMsg(res)
113 | }
114 |
--------------------------------------------------------------------------------
/constants.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | // Error represents a dnscrypt error.
4 | type Error string
5 |
6 | func (e Error) Error() string { return "dnscrypt: " + string(e) }
7 |
8 | const (
9 | // ErrTooShort means that the DNS query is shorter than possible
10 | ErrTooShort = Error("message is too short")
11 |
12 | // ErrQueryTooLarge means that the DNS query is larger than max allowed size
13 | ErrQueryTooLarge = Error("DNSCrypt query is too large")
14 |
15 | // ErrEsVersion means that the cert contains unsupported es-version
16 | ErrEsVersion = Error("unsupported es-version")
17 |
18 | // ErrInvalidDate means that the cert is not valid for the current time
19 | ErrInvalidDate = Error("cert has invalid ts-start or ts-end")
20 |
21 | // ErrInvalidCertSignature means that the cert has invalid signature
22 | ErrInvalidCertSignature = Error("cert has invalid signature")
23 |
24 | // ErrInvalidQuery means that it failed to decrypt a DNSCrypt query
25 | ErrInvalidQuery = Error("DNSCrypt query is invalid and cannot be decrypted")
26 |
27 | // ErrInvalidClientMagic means that client-magic does not match
28 | ErrInvalidClientMagic = Error("DNSCrypt query contains invalid client magic")
29 |
30 | // ErrInvalidResolverMagic means that server-magic does not match
31 | ErrInvalidResolverMagic = Error("DNSCrypt response contains invalid resolver magic")
32 |
33 | // ErrInvalidResponse means that it failed to decrypt a DNSCrypt response
34 | ErrInvalidResponse = Error("DNSCrypt response is invalid and cannot be decrypted")
35 |
36 | // ErrInvalidPadding means that it failed to unpad a query
37 | ErrInvalidPadding = Error("invalid padding")
38 |
39 | // ErrInvalidDNSStamp means an invalid DNS stamp
40 | ErrInvalidDNSStamp = Error("invalid DNS stamp")
41 |
42 | // ErrFailedToFetchCert means that it failed to fetch DNSCrypt certificate
43 | ErrFailedToFetchCert = Error("failed to fetch DNSCrypt certificate")
44 |
45 | // ErrCertTooShort means that it failed to deserialize cert, too short
46 | ErrCertTooShort = Error("cert is too short")
47 |
48 | // ErrCertMagic means an invalid cert magic
49 | ErrCertMagic = Error("invalid cert magic")
50 |
51 | // ErrServerConfig means that it failed to start the DNSCrypt server - invalid configuration
52 | ErrServerConfig = Error("invalid server configuration")
53 |
54 | // ErrServerNotStarted is returned if there's nothing to shutdown
55 | ErrServerNotStarted = Error("server is not started")
56 | )
57 |
58 | const (
59 | // is a variable length, initially set to 256 bytes, and
60 | // must be a multiple of 64 bytes. (see https://dnscrypt.info/protocol)
61 | // Some servers do not work if padded length is less than 256. Example: Quad9
62 | minUDPQuestionSize = 256
63 |
64 | // Minimum possible DNS packet size
65 | minDNSPacketSize = 12 + 5
66 |
67 | // See 11. Authenticated encryption and key exchange algorithm
68 | // The public and secret keys are 32 bytes long in storage
69 | keySize = 32
70 |
71 | // size of the shared key used to encrypt/decrypt messages
72 | sharedKeySize = 32
73 |
74 | // ClientMagic is the first 8 bytes of a client query that is to be built
75 | // using the information from this certificate. It may be a truncated
76 | // public key. Two valid certificates cannot share the same .
77 | clientMagicSize = 8
78 |
79 | // When using X25519-XSalsa20Poly1305, this construction requires a 24 bytes
80 | // nonce, that must not be reused for a given shared secret.
81 | nonceSize = 24
82 |
83 | // the first 8 bytes of every dnscrypt response. must match resolverMagic.
84 | resolverMagicSize = 8
85 | )
86 |
87 | var (
88 | // certMagic is a bytes sequence that must be in the beginning of the serialized cert
89 | certMagic = [4]byte{0x44, 0x4e, 0x53, 0x43}
90 |
91 | // resolverMagic is a byte sequence that must be in the beginning of every response
92 | resolverMagic = []byte{0x72, 0x36, 0x66, 0x6e, 0x76, 0x57, 0x6a, 0x38}
93 | )
94 |
95 | // CryptoConstruction represents the encryption algorithm (either XSalsa20Poly1305 or XChacha20Poly1305)
96 | type CryptoConstruction uint16
97 |
98 | const (
99 | // UndefinedConstruction is the default value for empty CertInfo only
100 | UndefinedConstruction CryptoConstruction = iota
101 | // XSalsa20Poly1305 encryption
102 | XSalsa20Poly1305 CryptoConstruction = 0x0001
103 | // XChacha20Poly1305 encryption
104 | XChacha20Poly1305 CryptoConstruction = 0x0002
105 | )
106 |
107 | func (c CryptoConstruction) String() string {
108 | switch c {
109 | case XChacha20Poly1305:
110 | return "XChacha20Poly1305"
111 | case XSalsa20Poly1305:
112 | return "XSalsa20Poly1305"
113 | default:
114 | return "Unknown"
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | 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.
3 |
4 | Here's how to create a simple DNSCrypt client:
5 |
6 | // AdGuard DNS stamp
7 | stampStr := "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
8 |
9 | // Initializing the DNSCrypt client
10 | c := dnscrypt.Client{Net: "udp", Timeout: 10 * time.Second}
11 |
12 | // Fetching and validating the server certificate
13 | resolverInfo, err := c.Dial(stampStr)
14 | if err != nil {
15 | return err
16 | }
17 |
18 | // Create a DNS request
19 | req := dns.Msg{}
20 | req.Id = dns.Id()
21 | req.RecursionDesired = true
22 | req.Question = []dns.Question{
23 | {Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
24 | }
25 |
26 | // Get the DNS response
27 | reply, err := c.Exchange(&req, resolverInfo)
28 |
29 | Here's how to run a DNSCrypt resolver:
30 |
31 | // Prepare the test DNSCrypt server config
32 | rc, err := dnscrypt.GenerateResolverConfig("example.org", nil)
33 | if err != nil {
34 | return err
35 | }
36 |
37 | cert, err := rc.CreateCert()
38 | if err != nil {
39 | return err
40 | }
41 |
42 | s := &dnscrypt.Server{
43 | ProviderName: rc.ProviderName,
44 | ResolverCert: cert,
45 | Handler: dnscrypt.DefaultHandler,
46 | }
47 |
48 | // Prepare TCP listener
49 | tcpConn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4zero, Port: 443})
50 | if err != nil {
51 | return err
52 | }
53 |
54 | // Prepare UDP listener
55 | udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 443})
56 | if err != nil {
57 | return err
58 | }
59 |
60 | // Start the server
61 | go s.ServeUDP(udpConn)
62 | go s.ServeTCP(tcpConn)
63 | */
64 | package dnscrypt
65 |
--------------------------------------------------------------------------------
/encrypted_query.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "math/rand"
7 | "time"
8 |
9 | "github.com/ameshkov/dnscrypt/v2/xsecretbox"
10 | "golang.org/x/crypto/nacl/secretbox"
11 | )
12 |
13 | // EncryptedQuery is a structure for encrypting and decrypting client queries
14 | //
15 | // ::=
16 | // ::= AE( , )
17 | type EncryptedQuery struct {
18 | // EsVersion is the encryption to use
19 | EsVersion CryptoConstruction
20 |
21 | // ClientMagic is a 8 byte identifier for the resolver certificate
22 | // chosen by the client.
23 | ClientMagic [clientMagicSize]byte
24 |
25 | // ClientPk is the client's public key
26 | ClientPk [keySize]byte
27 |
28 | // With a 24 bytes nonce, a question sent by a DNSCrypt client must be
29 | // encrypted using the shared secret, and a nonce constructed as follows:
30 | // 12 bytes chosen by the client followed by 12 NUL (0) bytes.
31 | //
32 | // The client's half of the nonce can include a timestamp in addition to a
33 | // counter or to random bytes, so that when a response is received, the
34 | // client can use this timestamp to immediately discard responses to
35 | // queries that have been sent too long ago, or dated in the future.
36 | Nonce [nonceSize]byte
37 | }
38 |
39 | // Encrypt encrypts the specified DNS query, returns encrypted data ready to be sent.
40 | //
41 | // Note that this method will generate a random nonce automatically.
42 | //
43 | // The following fields must be set before calling this method:
44 | // * EsVersion -- to encrypt the query
45 | // * ClientMagic -- to send it with the query
46 | // * ClientPk -- to send it with the query
47 | func (q *EncryptedQuery) Encrypt(packet []byte, sharedKey [sharedKeySize]byte) ([]byte, error) {
48 | var query []byte
49 |
50 | // Step 1: generate nonce
51 | binary.BigEndian.PutUint64(q.Nonce[:8], uint64(time.Now().UnixNano()))
52 | _, _ = rand.Read(q.Nonce[8:12])
53 |
54 | // Unencrypted part of the query:
55 | //
56 | query = append(query, q.ClientMagic[:]...)
57 | query = append(query, q.ClientPk[:]...)
58 | query = append(query, q.Nonce[:nonceSize/2]...)
59 |
60 | //
61 | padded := pad(packet)
62 |
63 | //
64 | nonce := q.Nonce
65 | if q.EsVersion == XChacha20Poly1305 {
66 | query = xsecretbox.Seal(query, nonce[:], padded, sharedKey[:])
67 | } else if q.EsVersion == XSalsa20Poly1305 {
68 | var xsalsaNonce [nonceSize]byte
69 | copy(xsalsaNonce[:], nonce[:])
70 | query = secretbox.Seal(query, padded, &xsalsaNonce, &sharedKey)
71 | } else {
72 | return nil, ErrEsVersion
73 | }
74 |
75 | return query, nil
76 | }
77 |
78 | // Decrypt decrypts the client query, returns decrypted DNS packet.
79 | //
80 | // Please note, that before calling this method the following fields must be set:
81 | // * ClientMagic -- to verify the query
82 | // * EsVersion -- to decrypt
83 | func (q *EncryptedQuery) Decrypt(query []byte, serverSecretKey [keySize]byte) ([]byte, error) {
84 | headerLength := clientMagicSize + keySize + nonceSize/2
85 | if len(query) < headerLength+xsecretbox.TagSize+minDNSPacketSize {
86 | return nil, ErrInvalidQuery
87 | }
88 |
89 | // read and verify
90 | clientMagic := [clientMagicSize]byte{}
91 | copy(clientMagic[:], query[:clientMagicSize])
92 | if !bytes.Equal(clientMagic[:], q.ClientMagic[:]) {
93 | return nil, ErrInvalidClientMagic
94 | }
95 |
96 | // read
97 | idx := clientMagicSize
98 | copy(q.ClientPk[:keySize], query[idx:idx+keySize])
99 |
100 | // generate server shared key
101 | sharedKey, err := computeSharedKey(q.EsVersion, &serverSecretKey, &q.ClientPk)
102 | if err != nil {
103 | return nil, err
104 | }
105 |
106 | // read
107 | idx = idx + keySize
108 | copy(q.Nonce[:nonceSize/2], query[idx:idx+nonceSize/2])
109 |
110 | // read and decrypt
111 | idx = idx + nonceSize/2
112 | encryptedQuery := query[idx:]
113 | var packet []byte
114 | if q.EsVersion == XChacha20Poly1305 {
115 | packet, err = xsecretbox.Open(nil, q.Nonce[:], encryptedQuery, sharedKey[:])
116 | if err != nil {
117 | return nil, ErrInvalidQuery
118 | }
119 | } else if q.EsVersion == XSalsa20Poly1305 {
120 | var xsalsaServerNonce [24]byte
121 | copy(xsalsaServerNonce[:], q.Nonce[:])
122 | var ok bool
123 | packet, ok = secretbox.Open(nil, encryptedQuery, &xsalsaServerNonce, &sharedKey)
124 | if !ok {
125 | return nil, ErrInvalidQuery
126 | }
127 | } else {
128 | return nil, ErrEsVersion
129 | }
130 |
131 | packet, err = unpad(packet)
132 | if err != nil {
133 | return nil, ErrInvalidPadding
134 | }
135 |
136 | return packet, nil
137 | }
138 |
--------------------------------------------------------------------------------
/encrypted_query_test.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "bytes"
5 | "crypto/rand"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestDNSCryptQueryEncryptDecryptXSalsa20Poly1305(t *testing.T) {
12 | testDNSCryptQueryEncryptDecrypt(t, XSalsa20Poly1305)
13 | }
14 |
15 | func TestDNSCryptQueryEncryptDecryptXChacha20Poly1305(t *testing.T) {
16 | testDNSCryptQueryEncryptDecrypt(t, XChacha20Poly1305)
17 | }
18 |
19 | func testDNSCryptQueryEncryptDecrypt(t *testing.T, esVersion CryptoConstruction) {
20 | // Generate the secret/public pairs
21 | clientSecretKey, clientPublicKey := generateRandomKeyPair()
22 | serverSecretKey, serverPublicKey := generateRandomKeyPair()
23 |
24 | // Generate client shared key
25 | clientSharedKey, err := computeSharedKey(esVersion, &clientSecretKey, &serverPublicKey)
26 | require.NoError(t, err)
27 |
28 | clientMagic := [clientMagicSize]byte{}
29 | _, _ = rand.Read(clientMagic[:])
30 |
31 | q1 := EncryptedQuery{
32 | EsVersion: esVersion,
33 | ClientPk: clientPublicKey,
34 | ClientMagic: clientMagic,
35 | }
36 |
37 | // Generate random packet
38 | packet := make([]byte, 100)
39 | _, _ = rand.Read(packet[:])
40 |
41 | // Encrypt it
42 | encrypted, err := q1.Encrypt(packet, clientSharedKey)
43 | require.NoError(t, err)
44 |
45 | // Now let's try decrypting it
46 | q2 := EncryptedQuery{
47 | EsVersion: esVersion,
48 | ClientMagic: clientMagic,
49 | }
50 |
51 | // Decrypt it
52 | decrypted, err := q2.Decrypt(encrypted, serverSecretKey)
53 | require.NoError(t, err)
54 |
55 | // Check that packet is the same
56 | require.True(t, bytes.Equal(packet, decrypted))
57 | }
58 |
--------------------------------------------------------------------------------
/encrypted_response.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "math/rand"
7 | "time"
8 |
9 | "github.com/ameshkov/dnscrypt/v2/xsecretbox"
10 | "golang.org/x/crypto/nacl/secretbox"
11 | )
12 |
13 | // EncryptedResponse is a structure for encrypting/decrypting server responses
14 | //
15 | // ::=
16 | // ::= AE(, , )
17 | type EncryptedResponse struct {
18 | // EsVersion is the encryption to use
19 | EsVersion CryptoConstruction
20 |
21 | // Nonce - ::=
22 | // ::= the nonce sent by the client in the related query.
23 | Nonce [nonceSize]byte
24 | }
25 |
26 | // Encrypt encrypts the server response
27 | //
28 | // EsVersion must be set.
29 | // Nonce needs to be set to "client-nonce".
30 | // This method will generate "resolver-nonce" and set it automatically.
31 | func (r *EncryptedResponse) Encrypt(packet []byte, sharedKey [sharedKeySize]byte) ([]byte, error) {
32 | var response []byte
33 |
34 | // Step 1: generate nonce
35 | _, _ = rand.Read(r.Nonce[12:16])
36 | binary.BigEndian.PutUint64(r.Nonce[16:nonceSize], uint64(time.Now().UnixNano()))
37 |
38 | // Unencrypted part of the query:
39 | response = append(response, resolverMagic[:]...)
40 | response = append(response, r.Nonce[:]...)
41 |
42 | //
43 | padded := pad(packet)
44 |
45 | //
46 | nonce := r.Nonce
47 | if r.EsVersion == XChacha20Poly1305 {
48 | response = xsecretbox.Seal(response, nonce[:], padded, sharedKey[:])
49 | } else if r.EsVersion == XSalsa20Poly1305 {
50 | var xsalsaNonce [nonceSize]byte
51 | copy(xsalsaNonce[:], nonce[:])
52 | response = secretbox.Seal(response, padded, &xsalsaNonce, &sharedKey)
53 | } else {
54 | return nil, ErrEsVersion
55 | }
56 |
57 | return response, nil
58 | }
59 |
60 | // Decrypt decrypts the server response
61 | //
62 | // EsVersion must be set.
63 | func (r *EncryptedResponse) Decrypt(response []byte, sharedKey [sharedKeySize]byte) ([]byte, error) {
64 | headerLength := len(resolverMagic) + nonceSize
65 | if len(response) < headerLength+xsecretbox.TagSize+minDNSPacketSize {
66 | return nil, ErrInvalidResponse
67 | }
68 |
69 | // read and verify
70 | magic := [resolverMagicSize]byte{}
71 | copy(magic[:], response[:resolverMagicSize])
72 | if !bytes.Equal(magic[:], resolverMagic[:]) {
73 | return nil, ErrInvalidResolverMagic
74 | }
75 |
76 | // read nonce
77 | copy(r.Nonce[:], response[resolverMagicSize:nonceSize+resolverMagicSize])
78 |
79 | // read and decrypt
80 | encryptedResponse := response[nonceSize+resolverMagicSize:]
81 | var packet []byte
82 | var err error
83 | if r.EsVersion == XChacha20Poly1305 {
84 | packet, err = xsecretbox.Open(nil, r.Nonce[:], encryptedResponse, sharedKey[:])
85 | if err != nil {
86 | return nil, ErrInvalidResponse
87 | }
88 | } else if r.EsVersion == XSalsa20Poly1305 {
89 | var xsalsaServerNonce [24]byte
90 | copy(xsalsaServerNonce[:], r.Nonce[:])
91 | var ok bool
92 | packet, ok = secretbox.Open(nil, encryptedResponse, &xsalsaServerNonce, &sharedKey)
93 | if !ok {
94 | return nil, ErrInvalidResponse
95 | }
96 | } else {
97 | return nil, ErrEsVersion
98 | }
99 |
100 | packet, err = unpad(packet)
101 | if err != nil {
102 | return nil, ErrInvalidPadding
103 | }
104 |
105 | return packet, nil
106 | }
107 |
--------------------------------------------------------------------------------
/encrypted_response_test.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "bytes"
5 | "math/rand"
6 | "testing"
7 |
8 | "github.com/ameshkov/dnscrypt/v2/xsecretbox"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func TestDNSCryptResponseEncryptDecryptXSalsa20Poly1305(t *testing.T) {
13 | testDNSCryptResponseEncryptDecrypt(t, XSalsa20Poly1305)
14 | }
15 |
16 | func TestDNSCryptResponseEncryptDecryptXChacha20Poly1305(t *testing.T) {
17 | testDNSCryptResponseEncryptDecrypt(t, XChacha20Poly1305)
18 | }
19 |
20 | func testDNSCryptResponseEncryptDecrypt(t *testing.T, esVersion CryptoConstruction) {
21 | // Generate the secret/public pairs
22 | clientSecretKey, clientPublicKey := generateRandomKeyPair()
23 | serverSecretKey, serverPublicKey := generateRandomKeyPair()
24 |
25 | // Generate client shared key
26 | clientSharedKey, err := computeSharedKey(esVersion, &clientSecretKey, &serverPublicKey)
27 | require.NoError(t, err)
28 |
29 | // Generate server shared key
30 | serverSharedKey, err := computeSharedKey(esVersion, &serverSecretKey, &clientPublicKey)
31 | require.NoError(t, err)
32 |
33 | r1 := &EncryptedResponse{
34 | EsVersion: esVersion,
35 | }
36 | // Fill client-nonce
37 | _, _ = rand.Read(r1.Nonce[:nonceSize/12])
38 |
39 | // Generate random packet
40 | packet := make([]byte, 100)
41 | _, _ = rand.Read(packet[:])
42 |
43 | // Encrypt it
44 | encrypted, err := r1.Encrypt(packet, serverSharedKey)
45 | require.NoError(t, err)
46 |
47 | // Now let's try decrypting it
48 | r2 := &EncryptedResponse{
49 | EsVersion: esVersion,
50 | }
51 |
52 | // Decrypt it
53 | decrypted, err := r2.Decrypt(encrypted, clientSharedKey)
54 | require.NoError(t, err)
55 |
56 | // Check that packet is the same
57 | require.True(t, bytes.Equal(packet, decrypted))
58 |
59 | // Now check invalid data (some random stuff)
60 | _, err = r2.Decrypt(packet, clientSharedKey)
61 | require.NotNil(t, err)
62 |
63 | // Empty array
64 | _, err = r2.Decrypt([]byte{}, clientSharedKey)
65 | require.NotNil(t, err)
66 |
67 | // Minimum valid size
68 | b := make([]byte, len(resolverMagic)+nonceSize+xsecretbox.TagSize+minDNSPacketSize)
69 | _, _ = rand.Read(b)
70 | _, err = r2.Decrypt(b, clientSharedKey)
71 | require.NotNil(t, err)
72 | }
73 |
--------------------------------------------------------------------------------
/generate.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "crypto/ed25519"
5 | "crypto/rand"
6 | "encoding/hex"
7 | "strings"
8 | "time"
9 |
10 | "github.com/ameshkov/dnsstamps"
11 | "golang.org/x/crypto/curve25519"
12 | )
13 |
14 | const dnsCryptV2Prefix = "2.dnscrypt-cert."
15 |
16 | // ResolverConfig is the DNSCrypt resolver configuration
17 | type ResolverConfig struct {
18 | // DNSCrypt provider name
19 | ProviderName string `yaml:"provider_name"`
20 |
21 | // PublicKey is the DNSCrypt resolver public key
22 | PublicKey string `yaml:"public_key"`
23 |
24 | // PrivateKey is the DNSCrypt resolver private key
25 | // The main and only purpose of this key is to sign the certificate
26 | PrivateKey string `yaml:"private_key"`
27 |
28 | // ResolverSk is a hex-encoded short-term private key.
29 | // This key is used to encrypt/decrypt DNS queries.
30 | // If not set, we'll generate a new random ResolverSk and ResolverPk.
31 | ResolverSk string `yaml:"resolver_secret"`
32 |
33 | // ResolverPk is a hex-encoded short-term public key corresponding to ResolverSk.
34 | // This key is used to encrypt/decrypt DNS queries.
35 | ResolverPk string `yaml:"resolver_public"`
36 |
37 | // EsVersion is the crypto to use in this resolver
38 | EsVersion CryptoConstruction `yaml:"es_version"`
39 |
40 | // CertificateTTL is the time-to-live value for the certificate that is
41 | // generated using this ResolverConfig.
42 | // If not set, we'll use 1 year by default.
43 | CertificateTTL time.Duration `yaml:"certificate_ttl"`
44 | }
45 |
46 | // CreateCert generates a signed Cert to be used by Server
47 | func (rc *ResolverConfig) CreateCert() (*Cert, error) {
48 | notAfter := time.Now()
49 | if rc.CertificateTTL > 0 {
50 | notAfter = notAfter.Add(rc.CertificateTTL)
51 | } else {
52 | // Default cert validity is 1 year
53 | notAfter = notAfter.Add(time.Hour * 24 * 365)
54 | }
55 |
56 | cert := &Cert{
57 | Serial: uint32(time.Now().Unix()),
58 | NotAfter: uint32(notAfter.Unix()),
59 | NotBefore: uint32(time.Now().Unix()),
60 | EsVersion: rc.EsVersion,
61 | }
62 |
63 | // short-term public key
64 | resolverPk, err := HexDecodeKey(rc.ResolverPk)
65 | if err != nil {
66 | return nil, err
67 | }
68 | // short-term private key
69 | resolverSk, err := HexDecodeKey(rc.ResolverSk)
70 | if err != nil {
71 | return nil, err
72 | }
73 |
74 | if len(resolverPk) != keySize || len(resolverSk) != keySize {
75 | sk, pk := generateRandomKeyPair()
76 | resolverSk = sk[:]
77 | resolverPk = pk[:]
78 | }
79 |
80 | copy(cert.ResolverPk[:], resolverPk[:])
81 | copy(cert.ResolverSk[:], resolverSk)
82 |
83 | // private key
84 | privateKey, err := HexDecodeKey(rc.PrivateKey)
85 | if err != nil {
86 | return nil, err
87 | }
88 |
89 | // sign the data
90 | cert.Sign(privateKey)
91 |
92 | // done
93 | return cert, nil
94 | }
95 |
96 | // CreateStamp generates a DNS stamp for this resolver
97 | func (rc *ResolverConfig) CreateStamp(addr string) (dnsstamps.ServerStamp, error) {
98 | stamp := dnsstamps.ServerStamp{
99 | ProviderName: rc.ProviderName,
100 | Proto: dnsstamps.StampProtoTypeDNSCrypt,
101 | }
102 |
103 | serverPk, err := HexDecodeKey(rc.PublicKey)
104 | if err != nil {
105 | return stamp, err
106 | }
107 |
108 | stamp.ServerPk = serverPk
109 | stamp.ServerAddrStr = addr
110 | return stamp, nil
111 | }
112 |
113 | // GenerateResolverConfig generates resolver configuration for a given provider name.
114 | // providerName is mandatory. If needed, "2.dnscrypt-cert." prefix is added to it.
115 | // privateKey is optional. If not set, it will be generated automatically.
116 | func GenerateResolverConfig(providerName string, privateKey ed25519.PrivateKey) (ResolverConfig, error) {
117 | rc := ResolverConfig{
118 | // Use XSalsa20Poly1305 by default
119 | EsVersion: XSalsa20Poly1305,
120 | }
121 | if !strings.HasPrefix(providerName, dnsCryptV2Prefix) {
122 | providerName = dnsCryptV2Prefix + providerName
123 | }
124 | rc.ProviderName = providerName
125 |
126 | var err error
127 | if privateKey == nil {
128 | // privateKey = gene
129 | _, privateKey, err = ed25519.GenerateKey(rand.Reader)
130 | if err != nil {
131 | return rc, err
132 | }
133 | }
134 | rc.PrivateKey = HexEncodeKey(privateKey)
135 | rc.PublicKey = HexEncodeKey(privateKey.Public().(ed25519.PublicKey))
136 |
137 | resolverSk, resolverPk := generateRandomKeyPair()
138 | rc.ResolverSk = HexEncodeKey(resolverSk[:])
139 | rc.ResolverPk = HexEncodeKey(resolverPk[:])
140 | return rc, nil
141 | }
142 |
143 | // HexEncodeKey encodes a byte slice to a hex-encoded string.
144 | func HexEncodeKey(b []byte) string {
145 | return strings.ToUpper(hex.EncodeToString(b))
146 | }
147 |
148 | // HexDecodeKey decodes a hex-encoded string with (optional) colons
149 | // to a byte array.
150 | func HexDecodeKey(str string) ([]byte, error) {
151 | return hex.DecodeString(strings.ReplaceAll(str, ":", ""))
152 | }
153 |
154 | // generateRandomKeyPair generates a random key-pair
155 | func generateRandomKeyPair() (privateKey [keySize]byte, publicKey [keySize]byte) {
156 | privateKey = [keySize]byte{}
157 | publicKey = [keySize]byte{}
158 |
159 | _, _ = rand.Read(privateKey[:])
160 | curve25519.ScalarBaseMult(&publicKey, &privateKey)
161 | return
162 | }
163 |
--------------------------------------------------------------------------------
/generate_test.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "bytes"
5 | "crypto/ed25519"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestHexEncodeKey(t *testing.T) {
12 | str := HexEncodeKey([]byte{1, 2, 3, 4})
13 | require.Equal(t, "01020304", str)
14 | }
15 |
16 | func TestHexDecodeKey(t *testing.T) {
17 | b, err := HexDecodeKey("01:02:03:04")
18 | require.NoError(t, err)
19 | require.True(t, bytes.Equal(b, []byte{1, 2, 3, 4}))
20 | }
21 |
22 | func TestGenerateResolverConfig(t *testing.T) {
23 | rc, err := GenerateResolverConfig("example.org", nil)
24 | require.NoError(t, err)
25 | require.Equal(t, "2.dnscrypt-cert.example.org", rc.ProviderName)
26 | require.Equal(t, ed25519.PrivateKeySize*2, len(rc.PrivateKey))
27 | require.Equal(t, keySize*2, len(rc.ResolverSk))
28 | require.Equal(t, keySize*2, len(rc.ResolverPk))
29 |
30 | cert, err := rc.CreateCert()
31 | require.NoError(t, err)
32 |
33 | require.True(t, cert.VerifyDate())
34 |
35 | publicKey, err := HexDecodeKey(rc.PublicKey)
36 | require.NoError(t, err)
37 | require.True(t, cert.VerifySignature(publicKey))
38 | }
39 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ameshkov/dnscrypt/v2
2 |
3 | go 1.24.1
4 |
5 | require (
6 | github.com/AdguardTeam/golibs v0.32.7
7 | github.com/ameshkov/dnsstamps v1.0.3
8 | github.com/jessevdk/go-flags v1.6.1
9 | github.com/miekg/dns v1.1.65
10 | github.com/stretchr/testify v1.10.0
11 | golang.org/x/crypto v0.37.0
12 | golang.org/x/net v0.38.0
13 | gopkg.in/yaml.v3 v3.0.1
14 | )
15 |
16 | require (
17 | github.com/davecgh/go-spew v1.1.1 // indirect
18 | github.com/pmezard/go-difflib v1.0.0 // indirect
19 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
20 | golang.org/x/mod v0.24.0 // indirect
21 | golang.org/x/sync v0.13.0 // indirect
22 | golang.org/x/sys v0.32.0 // indirect
23 | golang.org/x/tools v0.31.0 // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/AdguardTeam/golibs v0.32.7 h1:3dmGlAVgmvquCCwHsvEl58KKcRAK3z1UnjMnwSIeDH4=
2 | github.com/AdguardTeam/golibs v0.32.7/go.mod h1:bE8KV1zqTzgZjmjFyBJ9f9O5DEKO717r7e57j1HclJA=
3 | github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
4 | github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
8 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
9 | github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
10 | github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
11 | github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
12 | github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
15 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
16 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
17 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
18 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
19 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
20 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
21 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
22 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
23 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
24 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
25 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
26 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
27 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
28 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
29 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
30 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
34 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
35 |
--------------------------------------------------------------------------------
/handler.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "net"
5 | "time"
6 |
7 | "github.com/miekg/dns"
8 | )
9 |
10 | const defaultTimeout = 10 * time.Second
11 |
12 | // Handler is implemented by any value that implements ServeDNS.
13 | type Handler interface {
14 | ServeDNS(rw ResponseWriter, r *dns.Msg) error
15 | }
16 |
17 | // ResponseWriter is the interface that needs to be implemented for different protocols
18 | type ResponseWriter interface {
19 | LocalAddr() net.Addr // LocalAddr - local socket address
20 | RemoteAddr() net.Addr // RemoteAddr - remote client socket address
21 | WriteMsg(m *dns.Msg) error // WriteMsg - writes response message to the client
22 | }
23 |
24 | // DefaultHandler is the default Handler implementation
25 | // that is used by Server if custom handler is not configured
26 | var DefaultHandler Handler = &defaultHandler{
27 | udpClient: &dns.Client{
28 | Net: "udp",
29 | Timeout: defaultTimeout,
30 | },
31 | tcpClient: &dns.Client{
32 | Net: "tcp",
33 | Timeout: defaultTimeout,
34 | },
35 | addr: "94.140.14.140:53",
36 | }
37 |
38 | type defaultHandler struct {
39 | udpClient *dns.Client
40 | tcpClient *dns.Client
41 | addr string
42 | }
43 |
44 | // ServeDNS implements Handler interface
45 | func (h *defaultHandler) ServeDNS(rw ResponseWriter, r *dns.Msg) error {
46 | // Google DNS
47 | res, _, err := h.udpClient.Exchange(r, h.addr)
48 | if err != nil {
49 | return err
50 | }
51 |
52 | if res.Truncated {
53 | res, _, err = h.tcpClient.Exchange(r, h.addr)
54 | if err != nil {
55 | return err
56 | }
57 | }
58 |
59 | return rw.WriteMsg(res)
60 | }
61 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "context"
5 | "log/slog"
6 | "net"
7 | "strings"
8 | "sync"
9 | "time"
10 |
11 | "github.com/AdguardTeam/golibs/errors"
12 | "github.com/AdguardTeam/golibs/logutil/slogutil"
13 | "github.com/miekg/dns"
14 | )
15 |
16 | // default read timeout for all reads
17 | const defaultReadTimeout = 2 * time.Second
18 |
19 | // in case of TCP we only use defaultReadTimeout for the first read
20 | // then we start using defaultTCPIdleTimeout
21 | const defaultTCPIdleTimeout = 8 * time.Second
22 |
23 | // defaultUDPSize is the default size of the UDP read buffer. The release notes
24 | // for dnscrypt-proxy version 1.1.0-RC1 claim that this size was chosen as the
25 | // maximum one "for compatibility with some scary network setups", and making it
26 | // smaller seems to break things for some people.
27 | //
28 | // See also: https://github.com/AdguardTeam/AdGuardDNS/issues/188.
29 | const defaultUDPSize = 1252
30 |
31 | // helper struct that is used in several SetReadDeadline calls
32 | var longTimeAgo = time.Unix(1, 0)
33 |
34 | // ServerDNSCrypt is an interface for a DNSCrypt server
35 | type ServerDNSCrypt interface {
36 | // ServeTCP listens to TCP connections, queries are then processed by Server.Handler.
37 | // It blocks the calling goroutine and to stop it you need to close the listener
38 | // or call ServerDNSCrypt.Shutdown.
39 | ServeTCP(l net.Listener) error
40 |
41 | // ServeUDP listens to UDP connections, queries are then processed by Server.Handler.
42 | // It blocks the calling goroutine and to stop it you need to close the listener
43 | // or call ServerDNSCrypt.Shutdown.
44 | ServeUDP(l *net.UDPConn) error
45 |
46 | // Shutdown tries to gracefully shutdown the server. It waits until all
47 | // connections are processed and only after that it leaves the method.
48 | // If context deadline is specified, it will exit earlier
49 | // or call ServerDNSCrypt.Shutdown.
50 | Shutdown(ctx context.Context) error
51 | }
52 |
53 | // Server is a simple DNSCrypt server implementation
54 | type Server struct {
55 | // ProviderName is a DNSCrypt provider name
56 | ProviderName string
57 |
58 | // ResolverCert contains resolver certificate.
59 | ResolverCert *Cert
60 |
61 | // UDPSize is the default buffer size to use to read incoming UDP messages.
62 | // If not set it defaults to defaultUDPSize (1252 B).
63 | UDPSize int
64 |
65 | // Handler to invoke. If nil, uses DefaultHandler.
66 | Handler Handler
67 |
68 | // Logger is a logger instance for Server. If not set, slog.Default() will
69 | // be used.
70 | Logger *slog.Logger
71 |
72 | // make sure init is called only once
73 | initOnce sync.Once
74 |
75 | // Shutdown handling
76 | // --
77 | lock sync.RWMutex // protects access to all the fields below
78 | started bool
79 | wg sync.WaitGroup // active workers (servers)
80 | tcpListeners map[net.Listener]struct{} // track active TCP listeners
81 | udpListeners map[*net.UDPConn]struct{} // track active UDP listeners
82 | tcpConns map[net.Conn]struct{} // track active connections
83 | }
84 |
85 | // type check
86 | var _ ServerDNSCrypt = &Server{}
87 |
88 | // prepareShutdown - prepares the server to shutdown:
89 | // unblocks reads from all connections related to this server
90 | // marks the server as stopped
91 | // if the server is not started, returns ErrServerNotStarted
92 | func (s *Server) prepareShutdown() error {
93 | s.lock.Lock()
94 | defer s.lock.Unlock()
95 |
96 | if !s.started {
97 | s.logger().Info("server is not started")
98 |
99 | return ErrServerNotStarted
100 | }
101 |
102 | s.started = false
103 |
104 | // These listeners were passed to us from the outside so we cannot close
105 | // them here - this is up to the calling code to do that. Instead of that,
106 | // we call Set(Read)Deadline to unblock goroutines that are currently
107 | // blocked on reading from those listeners.
108 | // For tcpConns we would like to avoid closing them to be able to process
109 | // queries before shutting everything down.
110 |
111 | // Unblock reads for all active tcpConns
112 | for conn := range s.tcpConns {
113 | _ = conn.SetReadDeadline(longTimeAgo)
114 | }
115 |
116 | // Unblock reads for all active TCP listeners
117 | for l := range s.tcpListeners {
118 | switch v := l.(type) {
119 | case *net.TCPListener:
120 | _ = v.SetDeadline(longTimeAgo)
121 | }
122 | }
123 |
124 | // Unblock reads for all active UDP listeners
125 | for l := range s.udpListeners {
126 | _ = l.SetReadDeadline(longTimeAgo)
127 | }
128 |
129 | return nil
130 | }
131 |
132 | // Shutdown tries to gracefully shutdown the server. It waits until all
133 | // connections are processed and only after that it leaves the method.
134 | // If context deadline is specified, it will exit earlier.
135 | func (s *Server) Shutdown(ctx context.Context) error {
136 | s.logger().Info("shutting down the DNSCrypt server")
137 |
138 | err := s.prepareShutdown()
139 | if err != nil {
140 | return err
141 | }
142 |
143 | // Using this channel to wait until all goroutines finish their work
144 | closed := make(chan struct{})
145 | go func() {
146 | s.wg.Wait()
147 | s.logger().Info("serve goroutines finished their work")
148 | close(closed)
149 | }()
150 |
151 | // Wait for either all goroutines finish their work
152 | // Or for the context deadline
153 | select {
154 | case <-closed:
155 | s.logger().Info("DNSCrypt server has been stopped")
156 | case <-ctx.Done():
157 | s.logger().Info("DNSCrypt server shutdown has timed out")
158 | err = ctx.Err()
159 | }
160 |
161 | return err
162 | }
163 |
164 | // init initializes (lazily) Server properties on startup
165 | // this method is called from Server.ServeTCP and Server.ServeUDP
166 | func (s *Server) init() {
167 | s.tcpConns = map[net.Conn]struct{}{}
168 | s.udpListeners = map[*net.UDPConn]struct{}{}
169 | s.tcpListeners = map[net.Listener]struct{}{}
170 |
171 | if s.UDPSize == 0 {
172 | s.UDPSize = defaultUDPSize
173 | }
174 | }
175 |
176 | // logger returns the logger instance of slog.Default() if it was not
177 | // configured.
178 | func (s *Server) logger() (l *slog.Logger) {
179 | if s.Logger == nil {
180 | return slog.Default()
181 | }
182 |
183 | return s.Logger
184 | }
185 |
186 | // isStarted returns true if the server is processing queries right now
187 | // it means that Server.ServeTCP and/or Server.ServeUDP have been called
188 | func (s *Server) isStarted() bool {
189 | s.lock.RLock()
190 | started := s.started
191 | s.lock.RUnlock()
192 | return started
193 | }
194 |
195 | // serveDNS serves a DNS response
196 | func (s *Server) serveDNS(rw ResponseWriter, r *dns.Msg) error {
197 | if r == nil || len(r.Question) != 1 || r.Response {
198 | return ErrInvalidQuery
199 | }
200 |
201 | s.logger().Debug("handling a DNS query", "question", r.Question[0].Name)
202 |
203 | handler := s.Handler
204 | if handler == nil {
205 | handler = DefaultHandler
206 | }
207 |
208 | err := handler.ServeDNS(rw, r)
209 | if err != nil {
210 | s.logger().Debug("error while handling a DNS query", slogutil.KeyError, err)
211 |
212 | reply := &dns.Msg{}
213 | reply.SetRcode(r, dns.RcodeServerFailure)
214 | _ = rw.WriteMsg(reply)
215 | }
216 |
217 | return nil
218 | }
219 |
220 | // encrypt encrypts DNSCrypt response
221 | func (s *Server) encrypt(m *dns.Msg, q EncryptedQuery) ([]byte, error) {
222 | r := EncryptedResponse{
223 | EsVersion: q.EsVersion,
224 | Nonce: q.Nonce,
225 | }
226 | packet, err := m.Pack()
227 | if err != nil {
228 | return nil, err
229 | }
230 |
231 | sharedKey, err := computeSharedKey(q.EsVersion, &s.ResolverCert.ResolverSk, &q.ClientPk)
232 | if err != nil {
233 | return nil, err
234 | }
235 |
236 | return r.Encrypt(packet, sharedKey)
237 | }
238 |
239 | // decrypt decrypts the incoming message and returns a DNS message to process
240 | func (s *Server) decrypt(b []byte) (*dns.Msg, EncryptedQuery, error) {
241 | q := EncryptedQuery{
242 | EsVersion: s.ResolverCert.EsVersion,
243 | ClientMagic: s.ResolverCert.ClientMagic,
244 | }
245 | msg, err := q.Decrypt(b, s.ResolverCert.ResolverSk)
246 | if err != nil {
247 | // Failed to decrypt, dropping it
248 | return nil, q, err
249 | }
250 |
251 | r := new(dns.Msg)
252 | err = r.Unpack(msg)
253 | if err != nil {
254 | // Invalid DNS message, ignore
255 | return nil, q, err
256 | }
257 |
258 | return r, q, nil
259 | }
260 |
261 | // handleHandshake handles a TXT request that requests certificate data
262 | func (s *Server) handleHandshake(b []byte, certTxt string) ([]byte, error) {
263 | m := new(dns.Msg)
264 | err := m.Unpack(b)
265 | if err != nil {
266 | // Not a handshake, just ignore it
267 | return nil, err
268 | }
269 |
270 | if len(m.Question) != 1 || m.Response {
271 | // Invalid query
272 | return nil, ErrInvalidQuery
273 | }
274 |
275 | q := m.Question[0]
276 | providerName := dns.Fqdn(s.ProviderName)
277 | qName := strings.ToLower(q.Name) // important, may be random case
278 | if q.Qtype != dns.TypeTXT || qName != providerName {
279 | // Invalid provider name or type, doing nothing
280 | return nil, ErrInvalidQuery
281 | }
282 |
283 | reply := new(dns.Msg)
284 | reply.SetReply(m)
285 | txt := &dns.TXT{
286 | Hdr: dns.RR_Header{
287 | Name: q.Name,
288 | Rrtype: dns.TypeTXT,
289 | Ttl: 60, // use 60 seconds by default, but it shouldn't matter
290 | Class: dns.ClassINET,
291 | },
292 | Txt: []string{
293 | certTxt,
294 | },
295 | }
296 | reply.Answer = append(reply.Answer, txt)
297 |
298 | // These bits are important for the old dnscrypt-proxy versions
299 | reply.Authoritative = true
300 | reply.RecursionAvailable = true
301 | return reply.Pack()
302 | }
303 |
304 | // validate checks if the Server config is properly set
305 | func (s *Server) validate() (err error) {
306 | if s.ResolverCert == nil {
307 | return errors.Annotate(ErrServerConfig, "ResolverCert is required")
308 | }
309 |
310 | if !s.ResolverCert.VerifyDate() {
311 | return errors.Annotate(ErrServerConfig, "ResolverCert date is not valid")
312 | }
313 |
314 | if s.ProviderName == "" {
315 | return errors.Annotate(ErrServerConfig, "ProviderName must be set")
316 | }
317 |
318 | return nil
319 | }
320 |
321 | // getCertTXT serializes the cert TXT record that are to be sent to the client
322 | func (s *Server) getCertTXT() (string, error) {
323 | certBuf, err := s.ResolverCert.Serialize()
324 | if err != nil {
325 | return "", err
326 | }
327 | certTxt := packTxtString(certBuf)
328 | return certTxt, nil
329 | }
330 |
--------------------------------------------------------------------------------
/server_bench_test.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "testing"
7 | "time"
8 |
9 | "github.com/ameshkov/dnsstamps"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func BenchmarkServeUDP(b *testing.B) {
14 | benchmarkServe(b, "udp")
15 | }
16 |
17 | func BenchmarkServeTCP(b *testing.B) {
18 | benchmarkServe(b, "tcp")
19 | }
20 |
21 | func benchmarkServe(b *testing.B, network string) {
22 | srv := newTestServer(b, &testHandler{})
23 | b.Cleanup(func() {
24 | err := srv.Close()
25 | require.NoError(b, err)
26 | })
27 |
28 | client := &Client{
29 | Timeout: 1 * time.Second,
30 | Net: network,
31 | }
32 |
33 | serverAddr := fmt.Sprintf("127.0.0.1:%d", srv.UDPAddr().Port)
34 | if network == "tcp" {
35 | serverAddr = fmt.Sprintf("127.0.0.1:%d", srv.TCPAddr().Port)
36 | }
37 |
38 | stamp := dnsstamps.ServerStamp{
39 | ServerAddrStr: serverAddr,
40 | ServerPk: srv.resolverPk,
41 | ProviderName: srv.server.ProviderName,
42 | Proto: dnsstamps.StampProtoTypeDNSCrypt,
43 | }
44 | ri, err := client.DialStamp(stamp)
45 | require.NoError(b, err)
46 | require.NotNil(b, ri)
47 |
48 | conn, err := net.Dial(network, stamp.ServerAddrStr)
49 | require.NoError(b, err)
50 |
51 | b.ResetTimer()
52 | b.ReportAllocs()
53 | for i := 0; i < b.N; i++ {
54 | m := createTestMessage()
55 | res, err := client.ExchangeConn(conn, m, ri)
56 | require.NoError(b, err)
57 | assertTestMessageResponse(b, res)
58 | }
59 | b.StopTimer()
60 | }
61 |
--------------------------------------------------------------------------------
/server_tcp.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "log/slog"
8 | "net"
9 | "sync"
10 | "time"
11 |
12 | "github.com/AdguardTeam/golibs/logutil/slogutil"
13 | "github.com/miekg/dns"
14 | )
15 |
16 | // TCPResponseWriter is the ResponseWriter implementation for TCP
17 | type TCPResponseWriter struct {
18 | tcpConn net.Conn
19 | encrypt encryptionFunc
20 | req *dns.Msg
21 | query EncryptedQuery
22 | logger *slog.Logger
23 | }
24 |
25 | // type check
26 | var _ ResponseWriter = &TCPResponseWriter{}
27 |
28 | // LocalAddr is the server socket local address
29 | func (w *TCPResponseWriter) LocalAddr() net.Addr {
30 | return w.tcpConn.LocalAddr()
31 | }
32 |
33 | // RemoteAddr is the client's address
34 | func (w *TCPResponseWriter) RemoteAddr() net.Addr {
35 | return w.tcpConn.RemoteAddr()
36 | }
37 |
38 | // WriteMsg writes DNS message to the client
39 | func (w *TCPResponseWriter) WriteMsg(m *dns.Msg) error {
40 | normalize("tcp", w.req, m)
41 |
42 | res, err := w.encrypt(m, w.query)
43 | if err != nil {
44 | w.logger.Debug("failed to encrypt the DNS query", slogutil.KeyError, err)
45 |
46 | return err
47 | }
48 |
49 | return writePrefixed(res, w.tcpConn)
50 | }
51 |
52 | // ServeTCP listens to TCP connections, queries are then processed by Server.Handler.
53 | // It blocks the calling goroutine and to stop it you need to close the listener
54 | // or call Server.Shutdown.
55 | func (s *Server) ServeTCP(l net.Listener) error {
56 | err := s.prepareServeTCP(l)
57 | if err != nil {
58 | return err
59 | }
60 |
61 | s.logger().Info("entering DNSCrypt TCP listening loop", "listenAddr", l.Addr())
62 |
63 | // Tracks TCP connection handling goroutines
64 | tcpWg := &sync.WaitGroup{}
65 | defer s.cleanUpTCP(tcpWg, l)
66 |
67 | // Track active goroutine
68 | s.wg.Add(1)
69 |
70 | // Serialize the cert right away and prepare it to be sent to the client
71 | certTxt, err := s.getCertTXT()
72 | if err != nil {
73 | return err
74 | }
75 |
76 | for s.isStarted() {
77 | conn, err := l.Accept()
78 |
79 | // Check the error code and exit loop if necessary
80 | if err != nil {
81 | if !s.isStarted() {
82 | // Stopped gracefully
83 | break
84 | }
85 | var netErr net.Error
86 | if errors.As(err, &netErr) && netErr.Timeout() {
87 | // Note that timeout errors will be here (i.e. hitting ReadDeadline)
88 | continue
89 | }
90 | if isConnClosed(err) {
91 | s.logger().Info("TCP listener closed, exiting loop")
92 | } else {
93 | s.logger().Info("got error when reading from UDP listen", slogutil.KeyError, err)
94 | }
95 | break
96 | }
97 |
98 | // If we got here, the connection is alive
99 | s.lock.Lock()
100 | // Track the connection to allow unblocking reads on shutdown.
101 | s.tcpConns[conn] = struct{}{}
102 | s.lock.Unlock()
103 |
104 | tcpWg.Add(1)
105 | go func() {
106 | // Ignore error here, it is most probably a legit one
107 | // if not, it's written to the debug log
108 | _ = s.handleTCPConnection(conn, certTxt)
109 |
110 | // Clean up
111 | _ = conn.Close()
112 | s.lock.Lock()
113 | delete(s.tcpConns, conn)
114 | s.lock.Unlock()
115 | tcpWg.Done()
116 | }()
117 | }
118 |
119 | return nil
120 | }
121 |
122 | // prepareServeTCP prepares the server and listener to serving DNSCrypt
123 | func (s *Server) prepareServeTCP(l net.Listener) (err error) {
124 | // Check that server is properly configured
125 | err = s.validate()
126 | if err != nil {
127 | return err
128 | }
129 |
130 | // Protect shutdown-related fields
131 | s.lock.Lock()
132 | defer s.lock.Unlock()
133 | s.initOnce.Do(s.init)
134 |
135 | // Mark the server as started if needed
136 | s.started = true
137 |
138 | // Track an active TCP listener
139 | s.tcpListeners[l] = struct{}{}
140 | return nil
141 | }
142 |
143 | // cleanUpTCP waits until all TCP messages before cleaning up
144 | func (s *Server) cleanUpTCP(tcpWg *sync.WaitGroup, l net.Listener) {
145 | // Wait until all TCP connections are processed
146 | tcpWg.Wait()
147 |
148 | // Not using it anymore so can be removed from the active listeners
149 | s.lock.Lock()
150 | delete(s.tcpListeners, l)
151 | s.lock.Unlock()
152 |
153 | // The work is finished
154 | s.wg.Done()
155 | }
156 |
157 | // handleTCPMsg handles a single TCP message. If this method returns error
158 | // the connection will be closed
159 | func (s *Server) handleTCPMsg(b []byte, conn net.Conn, certTxt string) error {
160 | if len(b) < minDNSPacketSize {
161 | // Ignore the packets that are too short
162 | return ErrTooShort
163 | }
164 |
165 | // First of all, check for "ClientMagic" in the incoming query
166 | if !bytes.Equal(b[:clientMagicSize], s.ResolverCert.ClientMagic[:]) {
167 | // If there's no ClientMagic in the packet, we assume this
168 | // is a plain DNS query requesting the certificate data
169 | reply, err := s.handleHandshake(b, certTxt)
170 | if err != nil {
171 | return fmt.Errorf("failed to process a plain DNS query: %w", err)
172 | }
173 | err = writePrefixed(reply, conn)
174 | if err != nil {
175 | return fmt.Errorf("failed to write a response: %w", err)
176 | }
177 | return nil
178 | }
179 |
180 | // If we got here, this is an encrypted DNSCrypt message
181 | // We should decrypt it first to get the plain DNS query
182 | m, q, err := s.decrypt(b)
183 | if err != nil {
184 | return fmt.Errorf("failed to decrypt incoming message: %w", err)
185 | }
186 | rw := &TCPResponseWriter{
187 | tcpConn: conn,
188 | encrypt: s.encrypt,
189 | req: m,
190 | query: q,
191 | logger: s.logger(),
192 | }
193 | err = s.serveDNS(rw, m)
194 | if err != nil {
195 | return fmt.Errorf("failed to process a DNS query: %w", err)
196 | }
197 |
198 | return nil
199 | }
200 |
201 | // handleTCPConnection handles all queries that are coming to the
202 | // specified TCP connection.
203 | func (s *Server) handleTCPConnection(conn net.Conn, certTxt string) error {
204 | timeout := defaultReadTimeout
205 |
206 | for s.isStarted() {
207 | _ = conn.SetReadDeadline(time.Now().Add(timeout))
208 |
209 | b, err := readPrefixed(conn)
210 | if err != nil {
211 | return err
212 | }
213 |
214 | err = s.handleTCPMsg(b, conn, certTxt)
215 | if err != nil {
216 | s.logger().Debug("failed to process a DNS query", slogutil.KeyError, err)
217 |
218 | return err
219 | }
220 |
221 | timeout = defaultTCPIdleTimeout
222 | }
223 |
224 | return nil
225 | }
226 |
--------------------------------------------------------------------------------
/server_test.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "crypto/ed25519"
7 | "fmt"
8 | "net"
9 | "runtime"
10 | "testing"
11 | "time"
12 |
13 | "github.com/ameshkov/dnsstamps"
14 | "github.com/miekg/dns"
15 | "github.com/stretchr/testify/require"
16 | )
17 |
18 | func TestServer_Shutdown(t *testing.T) {
19 | n := runtime.GOMAXPROCS(1)
20 | t.Cleanup(func() {
21 | runtime.GOMAXPROCS(n)
22 | })
23 | srv := newTestServer(t, &testHandler{})
24 | // Serve* methods are called in different goroutines
25 | // give them at least a moment to actually start the server
26 | time.Sleep(10 * time.Millisecond)
27 | require.NoError(t, srv.Close())
28 | }
29 |
30 | func TestServer_UDPServeCert(t *testing.T) {
31 | testServerServeCert(t, "udp")
32 | }
33 |
34 | func TestServer_TCPServeCert(t *testing.T) {
35 | testServerServeCert(t, "tcp")
36 | }
37 |
38 | func TestServer_UDPRespondMessages(t *testing.T) {
39 | testServerRespondMessages(t, "udp")
40 | }
41 |
42 | func TestServer_TCPRespondMessages(t *testing.T) {
43 | testServerRespondMessages(t, "tcp")
44 | }
45 |
46 | func TestServer_ReadTimeout(t *testing.T) {
47 | srv := newTestServer(t, &testHandler{})
48 | t.Cleanup(func() {
49 | require.NoError(t, srv.Close())
50 | })
51 | // Sleep for "defaultReadTimeout" before trying to shutdown the server
52 | // The point is to make sure readTimeout is properly handled by
53 | // the "Serve*" goroutines and they don't finish their work unexpectedly
54 | time.Sleep(defaultReadTimeout)
55 | testThisServerRespondMessages(t, "udp", srv)
56 | testThisServerRespondMessages(t, "tcp", srv)
57 | }
58 |
59 | func TestServer_UDPTruncateMessage(t *testing.T) {
60 | // Create a test server that returns large response which should be
61 | // truncated if sent over UDP
62 | srv := newTestServer(t, &testLargeMsgHandler{})
63 | t.Cleanup(func() {
64 | require.NoError(t, srv.Close())
65 | })
66 |
67 | // Create client and connect
68 | client := &Client{
69 | Timeout: 1 * time.Second,
70 | Net: "udp",
71 | }
72 | serverAddr := fmt.Sprintf("127.0.0.1:%d", srv.UDPAddr().Port)
73 | stamp := dnsstamps.ServerStamp{
74 | ServerAddrStr: serverAddr,
75 | ServerPk: srv.resolverPk,
76 | ProviderName: srv.server.ProviderName,
77 | Proto: dnsstamps.StampProtoTypeDNSCrypt,
78 | }
79 | ri, err := client.DialStamp(stamp)
80 | require.NoError(t, err)
81 | require.NotNil(t, ri)
82 |
83 | // Send a test message and check that the response was truncated
84 | m := createTestMessage()
85 | res, err := client.Exchange(m, ri)
86 | require.NoError(t, err)
87 | require.NotNil(t, res)
88 | require.Equal(t, dns.RcodeSuccess, res.Rcode)
89 | require.Len(t, res.Answer, 0)
90 | require.True(t, res.Truncated)
91 | }
92 |
93 | func TestServer_UDPEDNS0_NoTruncate(t *testing.T) {
94 | // Create a test server that returns large response which should be
95 | // truncated if sent over UDP
96 | // However, when EDNS0 is set with the buffer large enough, there should
97 | // be no truncation
98 | srv := newTestServer(t, &testLargeMsgHandler{})
99 | t.Cleanup(func() {
100 | require.NoError(t, srv.Close())
101 | })
102 |
103 | // Create client and connect
104 | client := &Client{
105 | Timeout: 1 * time.Second,
106 | Net: "udp",
107 | UDPSize: 7000, // make sure the client will be able to read the response
108 | }
109 | serverAddr := fmt.Sprintf("127.0.0.1:%d", srv.UDPAddr().Port)
110 | stamp := dnsstamps.ServerStamp{
111 | ServerAddrStr: serverAddr,
112 | ServerPk: srv.resolverPk,
113 | ProviderName: srv.server.ProviderName,
114 | Proto: dnsstamps.StampProtoTypeDNSCrypt,
115 | }
116 | ri, err := client.DialStamp(stamp)
117 | require.NoError(t, err)
118 | require.NotNil(t, ri)
119 |
120 | // Send a test message with UDP buffer size large enough
121 | // and check that the response was NOT truncated
122 | m := createTestMessage()
123 | m.Extra = append(m.Extra, &dns.OPT{
124 | Hdr: dns.RR_Header{
125 | Name: ".",
126 | Rrtype: dns.TypeOPT,
127 | Class: 2000, // Set large enough UDPSize here
128 | },
129 | })
130 | res, err := client.Exchange(m, ri)
131 | require.NoError(t, err)
132 | require.NotNil(t, res)
133 | require.Equal(t, dns.RcodeSuccess, res.Rcode)
134 | require.Len(t, res.Answer, 64)
135 | require.False(t, res.Truncated)
136 | }
137 |
138 | func testServerServeCert(t *testing.T, network string) {
139 | srv := newTestServer(t, &testHandler{})
140 | t.Cleanup(func() {
141 | require.NoError(t, srv.Close())
142 | })
143 |
144 | client := &Client{
145 | Net: network,
146 | Timeout: 1 * time.Second,
147 | }
148 |
149 | serverAddr := fmt.Sprintf("127.0.0.1:%d", srv.UDPAddr().Port)
150 | if network == "tcp" {
151 | serverAddr = fmt.Sprintf("127.0.0.1:%d", srv.TCPAddr().Port)
152 | }
153 |
154 | stamp := dnsstamps.ServerStamp{
155 | ServerAddrStr: serverAddr,
156 | ServerPk: srv.resolverPk,
157 | ProviderName: srv.server.ProviderName,
158 | Proto: dnsstamps.StampProtoTypeDNSCrypt,
159 | }
160 | ri, err := client.DialStamp(stamp)
161 | require.NoError(t, err)
162 | require.NotNil(t, ri)
163 |
164 | require.Equal(t, ri.ProviderName, srv.server.ProviderName)
165 | require.True(t, bytes.Equal(srv.server.ResolverCert.ClientMagic[:], ri.ResolverCert.ClientMagic[:]))
166 | require.Equal(t, srv.server.ResolverCert.EsVersion, ri.ResolverCert.EsVersion)
167 | require.Equal(t, srv.server.ResolverCert.Signature, ri.ResolverCert.Signature)
168 | require.Equal(t, srv.server.ResolverCert.NotBefore, ri.ResolverCert.NotBefore)
169 | require.Equal(t, srv.server.ResolverCert.NotAfter, ri.ResolverCert.NotAfter)
170 | require.True(t, bytes.Equal(srv.server.ResolverCert.ResolverPk[:], ri.ResolverCert.ResolverPk[:]))
171 | require.True(t, bytes.Equal(srv.server.ResolverCert.ResolverPk[:], ri.ResolverCert.ResolverPk[:]))
172 | }
173 |
174 | func testServerRespondMessages(t *testing.T, network string) {
175 | srv := newTestServer(t, &testHandler{})
176 | t.Cleanup(func() {
177 | require.NoError(t, srv.Close())
178 | })
179 | testThisServerRespondMessages(t, network, srv)
180 | }
181 |
182 | func testThisServerRespondMessages(t *testing.T, network string, srv *testServer) {
183 | client := &Client{
184 | Timeout: 1 * time.Second,
185 | Net: network,
186 | }
187 |
188 | serverAddr := fmt.Sprintf("127.0.0.1:%d", srv.UDPAddr().Port)
189 | if network == "tcp" {
190 | serverAddr = fmt.Sprintf("127.0.0.1:%d", srv.TCPAddr().Port)
191 | }
192 |
193 | stamp := dnsstamps.ServerStamp{
194 | ServerAddrStr: serverAddr,
195 | ServerPk: srv.resolverPk,
196 | ProviderName: srv.server.ProviderName,
197 | Proto: dnsstamps.StampProtoTypeDNSCrypt,
198 | }
199 | ri, err := client.DialStamp(stamp)
200 | require.NoError(t, err)
201 | require.NotNil(t, ri)
202 |
203 | conn, err := net.Dial(network, stamp.ServerAddrStr)
204 | require.NoError(t, err)
205 |
206 | for i := 0; i < 10; i++ {
207 | m := createTestMessage()
208 | res, err := client.ExchangeConn(conn, m, ri)
209 | require.NoError(t, err)
210 | assertTestMessageResponse(t, res)
211 | }
212 | }
213 |
214 | type testServer struct {
215 | server *Server
216 | resolverPk ed25519.PublicKey
217 | udpConn *net.UDPConn
218 | tcpListen net.Listener
219 | }
220 |
221 | func (s *testServer) TCPAddr() *net.TCPAddr {
222 | return s.tcpListen.Addr().(*net.TCPAddr)
223 | }
224 |
225 | func (s *testServer) UDPAddr() *net.UDPAddr {
226 | return s.udpConn.LocalAddr().(*net.UDPAddr)
227 | }
228 |
229 | func (s *testServer) Close() error {
230 | ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
231 | defer cancel()
232 |
233 | err := s.server.Shutdown(ctx)
234 | _ = s.udpConn.Close()
235 | _ = s.tcpListen.Close()
236 |
237 | return err
238 | }
239 |
240 | func newTestServer(t require.TestingT, handler Handler) *testServer {
241 | rc, err := GenerateResolverConfig("example.org", nil)
242 | require.NoError(t, err)
243 | cert, err := rc.CreateCert()
244 | require.NoError(t, err)
245 |
246 | s := &Server{
247 | ProviderName: rc.ProviderName,
248 | ResolverCert: cert,
249 | Handler: handler,
250 | }
251 |
252 | privateKey, err := HexDecodeKey(rc.PrivateKey)
253 | require.NoError(t, err)
254 | publicKey := ed25519.PrivateKey(privateKey).Public().(ed25519.PublicKey)
255 | srv := &testServer{
256 | server: s,
257 | resolverPk: publicKey,
258 | }
259 |
260 | srv.tcpListen, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4zero, Port: 0})
261 | require.NoError(t, err)
262 | srv.udpConn, err = net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
263 | require.NoError(t, err)
264 |
265 | go func() {
266 | _ = s.ServeUDP(srv.udpConn)
267 | }()
268 | go func() {
269 | _ = s.ServeTCP(srv.tcpListen)
270 | }()
271 | return srv
272 | }
273 |
274 | type testHandler struct{}
275 |
276 | // ServeDNS - implements Handler interface
277 | func (h *testHandler) ServeDNS(rw ResponseWriter, r *dns.Msg) error {
278 | res := new(dns.Msg)
279 | res.SetReply(r)
280 |
281 | answer := new(dns.A)
282 | answer.Hdr = dns.RR_Header{
283 | Name: r.Question[0].Name,
284 | Rrtype: dns.TypeA,
285 | Ttl: 300,
286 | Class: dns.ClassINET,
287 | }
288 | // First record is from Google DNS
289 | answer.A = net.IPv4(8, 8, 8, 8)
290 | res.Answer = append(res.Answer, answer)
291 |
292 | return rw.WriteMsg(res)
293 | }
294 |
295 | // testLargeMsgHandler is a handler that returns a huge response
296 | // used for testing messages truncation
297 | type testLargeMsgHandler struct{}
298 |
299 | // ServeDNS - implements Handler interface
300 | func (h *testLargeMsgHandler) ServeDNS(rw ResponseWriter, r *dns.Msg) error {
301 | res := new(dns.Msg)
302 | res.SetReply(r)
303 |
304 | for i := 0; i < 64; i++ {
305 | answer := new(dns.A)
306 | answer.Hdr = dns.RR_Header{
307 | Name: r.Question[0].Name,
308 | Rrtype: dns.TypeA,
309 | Ttl: 300,
310 | Class: dns.ClassINET,
311 | }
312 | answer.A = net.IPv4(127, 0, 0, byte(i))
313 | res.Answer = append(res.Answer, answer)
314 | }
315 |
316 | res.Compress = true
317 | return rw.WriteMsg(res)
318 | }
319 |
--------------------------------------------------------------------------------
/server_udp.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "log/slog"
7 | "net"
8 | "runtime"
9 | "sync"
10 | "time"
11 |
12 | "github.com/AdguardTeam/golibs/logutil/slogutil"
13 | "github.com/miekg/dns"
14 | "golang.org/x/net/ipv4"
15 | "golang.org/x/net/ipv6"
16 | )
17 |
18 | type encryptionFunc func(m *dns.Msg, q EncryptedQuery) ([]byte, error)
19 |
20 | // UDPResponseWriter is the ResponseWriter implementation for UDP
21 | type UDPResponseWriter struct {
22 | udpConn *net.UDPConn // UDP connection
23 | sess *dns.SessionUDP // SessionUDP (necessary to use dns.WriteToSessionUDP)
24 | encrypt encryptionFunc // DNSCrypt encryption function
25 | req *dns.Msg // DNS query that was processed
26 | query EncryptedQuery // DNSCrypt query properties
27 | logger *slog.Logger
28 | }
29 |
30 | // type check
31 | var _ ResponseWriter = &UDPResponseWriter{}
32 |
33 | // LocalAddr is the server socket local address
34 | func (w *UDPResponseWriter) LocalAddr() net.Addr {
35 | return w.udpConn.LocalAddr()
36 | }
37 |
38 | // RemoteAddr is the client's address
39 | func (w *UDPResponseWriter) RemoteAddr() net.Addr {
40 | return w.sess.RemoteAddr()
41 | }
42 |
43 | // WriteMsg writes DNS message to the client
44 | func (w *UDPResponseWriter) WriteMsg(m *dns.Msg) error {
45 | normalize("udp", w.req, m)
46 |
47 | res, err := w.encrypt(m, w.query)
48 | if err != nil {
49 | w.logger.Debug("failed to encrypt DNS query", slogutil.KeyError, err)
50 |
51 | return err
52 | }
53 | _, err = dns.WriteToSessionUDP(w.udpConn, res, w.sess)
54 | return err
55 | }
56 |
57 | // ServeUDP listens to UDP connections, queries are then processed by Server.Handler.
58 | // It blocks the calling goroutine and to stop it you need to close the listener
59 | // or call Server.Shutdown.
60 | func (s *Server) ServeUDP(l *net.UDPConn) error {
61 | err := s.prepareServeUDP(l)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | // Tracks UDP handling goroutines
67 | udpWg := &sync.WaitGroup{}
68 | defer s.cleanUpUDP(udpWg, l)
69 |
70 | // Track active goroutine
71 | s.wg.Add(1)
72 |
73 | s.logger().Info("entering DNSCrypt UDP listening loop", "listen_addr", l.LocalAddr())
74 |
75 | // Serialize the cert right away and prepare it to be sent to the client
76 | certTxt, err := s.getCertTXT()
77 | if err != nil {
78 | return err
79 | }
80 |
81 | for s.isStarted() {
82 | b, sess, err := s.readUDPMsg(l)
83 |
84 | // Check the error code and exit loop if necessary
85 | if err != nil {
86 | if !s.isStarted() {
87 | // Stopped gracefully
88 | return nil
89 | }
90 | var netErr net.Error
91 | if errors.As(err, &netErr) && netErr.Timeout() {
92 | // Note that timeout errors will be here (i.e. hitting ReadDeadline)
93 | continue
94 | }
95 | if isConnClosed(err) {
96 | s.logger().Info("UDP listener closed, exiting loop")
97 | } else {
98 | s.logger().Info("got error when reading from UDP", slogutil.KeyError, err)
99 | }
100 | return err
101 | }
102 |
103 | if len(b) < minDNSPacketSize {
104 | // Ignore the packets that are too short
105 | continue
106 | }
107 |
108 | udpWg.Add(1)
109 | go func() {
110 | s.serveUDPMsg(b, certTxt, sess, l)
111 | udpWg.Done()
112 | }()
113 | }
114 |
115 | return nil
116 | }
117 |
118 | // prepareServeUDP prepares the server and listener to serving DNSCrypt
119 | func (s *Server) prepareServeUDP(l *net.UDPConn) (err error) {
120 | // Check that server is properly configured
121 | err = s.validate()
122 | if err != nil {
123 | return err
124 | }
125 |
126 | // set UDP options to allow receiving OOB data
127 | err = setUDPSocketOptions(l)
128 | if err != nil {
129 | return err
130 | }
131 |
132 | // Protect shutdown-related fields
133 | s.lock.Lock()
134 | defer s.lock.Unlock()
135 | s.initOnce.Do(s.init)
136 |
137 | // Mark the server as started.
138 | // Note that we don't check if it was started before as
139 | // Serve* methods can be called multiple times.
140 | s.started = true
141 |
142 | // Track an active UDP listener
143 | s.udpListeners[l] = struct{}{}
144 | return err
145 | }
146 |
147 | // cleanUpUDP waits until all UDP messages before cleaning up
148 | func (s *Server) cleanUpUDP(udpWg *sync.WaitGroup, l *net.UDPConn) {
149 | // Wait until UDP messages are processed
150 | udpWg.Wait()
151 |
152 | // Not using it anymore so can be removed from the active listeners
153 | s.lock.Lock()
154 | delete(s.udpListeners, l)
155 | s.lock.Unlock()
156 |
157 | // The work is finished
158 | s.wg.Done()
159 | }
160 |
161 | // readUDPMsg reads incoming UDP message
162 | func (s *Server) readUDPMsg(l *net.UDPConn) ([]byte, *dns.SessionUDP, error) {
163 | _ = l.SetReadDeadline(time.Now().Add(defaultReadTimeout))
164 | b := make([]byte, s.UDPSize)
165 | n, sess, err := dns.ReadFromSessionUDP(l, b)
166 | if err != nil {
167 | return nil, nil, err
168 | }
169 |
170 | return b[:n], sess, err
171 | }
172 |
173 | // serveUDPMsg handles incoming DNS message
174 | func (s *Server) serveUDPMsg(b []byte, certTxt string, sess *dns.SessionUDP, l *net.UDPConn) {
175 | // First of all, check for "ClientMagic" in the incoming query
176 | if !bytes.Equal(b[:clientMagicSize], s.ResolverCert.ClientMagic[:]) {
177 | // If there's no ClientMagic in the packet, we assume this
178 | // is a plain DNS query requesting the certificate data
179 | reply, err := s.handleHandshake(b, certTxt)
180 | if err != nil {
181 | s.logger().Debug("failed to process a plain DNS query", slogutil.KeyError, err)
182 | }
183 | if err == nil {
184 | // Ignore errors, we don't care and can't handle them anyway
185 | _, _ = dns.WriteToSessionUDP(l, reply, sess)
186 | }
187 |
188 | return
189 | }
190 |
191 | // If we got here, this is an encrypted DNSCrypt message
192 | // We should decrypt it first to get the plain DNS query
193 | m, q, err := s.decrypt(b)
194 | if err == nil {
195 | rw := &UDPResponseWriter{
196 | udpConn: l,
197 | sess: sess,
198 | encrypt: s.encrypt,
199 | req: m,
200 | query: q,
201 | logger: s.logger(),
202 | }
203 | err = s.serveDNS(rw, m)
204 | if err != nil {
205 | s.logger().Debug("failed to serve DNS query", slogutil.KeyError, err)
206 | }
207 | } else {
208 | s.logger().Debug(
209 | "failed to decrypt incoming message",
210 | "len",
211 | len(b),
212 | slogutil.KeyError,
213 | err,
214 | )
215 | }
216 | }
217 |
218 | // setUDPSocketOptions method is necessary to be able to use dns.ReadFromSessionUDP / dns.WriteToSessionUDP
219 | func setUDPSocketOptions(conn *net.UDPConn) error {
220 | if runtime.GOOS == "windows" {
221 | return nil
222 | }
223 |
224 | // We don't know if this a IPv4-only, IPv6-only or a IPv4-and-IPv6 connection.
225 | // Try enabling receiving of ECN and packet info for both IP versions.
226 | // We expect at least one of those syscalls to succeed.
227 | err6 := ipv6.NewPacketConn(conn).SetControlMessage(ipv6.FlagDst|ipv6.FlagInterface, true)
228 | err4 := ipv4.NewPacketConn(conn).SetControlMessage(ipv4.FlagDst|ipv4.FlagInterface, true)
229 | if err6 != nil && err4 != nil {
230 | return err4
231 | }
232 | return nil
233 | }
234 |
--------------------------------------------------------------------------------
/testdata/dnscrypt-cert.opendns.txt:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------
/util.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "encoding/binary"
5 | "io"
6 | "net"
7 | "strings"
8 |
9 | "github.com/ameshkov/dnscrypt/v2/xsecretbox"
10 | "github.com/miekg/dns"
11 | "golang.org/x/crypto/nacl/box"
12 | )
13 |
14 | // Prior to encryption, queries are padded using the ISO/IEC 7816-4
15 | // format. The padding starts with a byte valued 0x80 followed by a
16 | // variable number of NUL bytes.
17 | //
18 | // ## Padding for client queries over UDP
19 | //
20 | // must be at least
21 | // bytes. If the length of the client query is less than ,
22 | // the padding length must be adjusted in order to satisfy this
23 | // requirement.
24 | //
25 | // is a variable length, initially set to 256 bytes, and
26 | // must be a multiple of 64 bytes.
27 | //
28 | // ## Padding for client queries over TCP
29 | //
30 | // The length of is randomly chosen between 1 and 256
31 | // bytes (including the leading 0x80), but the total length of
32 | // must be a multiple of 64 bytes.
33 | //
34 | // For example, an originally unpadded 56-bytes DNS query can be padded as:
35 | //
36 | // <56-bytes-query> 0x80 0x00 0x00 0x00 0x00 0x00 0x00 0x00
37 | // or
38 | // <56-bytes-query> 0x80 (0x00 * 71)
39 | // or
40 | // <56-bytes-query> 0x80 (0x00 * 135)
41 | // or
42 | // <56-bytes-query> 0x80 (0x00 * 199)
43 | func pad(packet []byte) []byte {
44 | // get closest divisible by 64 to + 1 byte for 0x80
45 | minQuestionSize := len(packet) + 1 + (64 - (len(packet)+1)%64)
46 |
47 | // padded size can't be less than minUDPQuestionSize
48 | if minUDPQuestionSize > minQuestionSize {
49 | minQuestionSize = minUDPQuestionSize
50 | }
51 |
52 | packet = append(packet, 0x80)
53 | for len(packet) < minQuestionSize {
54 | packet = append(packet, 0)
55 | }
56 |
57 | return packet
58 | }
59 |
60 | // unpad - removes padding bytes
61 | func unpad(packet []byte) ([]byte, error) {
62 | for i := len(packet); ; {
63 | if i == 0 {
64 | return nil, ErrInvalidPadding
65 | }
66 | i--
67 | if packet[i] == 0x80 {
68 | if i < minDNSPacketSize {
69 | return nil, ErrInvalidPadding
70 | }
71 |
72 | return packet[:i], nil
73 | } else if packet[i] != 0x00 {
74 | return nil, ErrInvalidPadding
75 | }
76 | }
77 | }
78 |
79 | // computeSharedKey - computes a shared key
80 | func computeSharedKey(cryptoConstruction CryptoConstruction, secretKey *[keySize]byte, publicKey *[keySize]byte) ([keySize]byte, error) {
81 | if cryptoConstruction == XChacha20Poly1305 {
82 | sharedKey, err := xsecretbox.SharedKey(*secretKey, *publicKey)
83 | if err != nil {
84 | return sharedKey, err
85 | }
86 | return sharedKey, nil
87 | } else if cryptoConstruction == XSalsa20Poly1305 {
88 | sharedKey := [sharedKeySize]byte{}
89 | box.Precompute(&sharedKey, publicKey, secretKey)
90 | return sharedKey, nil
91 | }
92 | return [keySize]byte{}, ErrEsVersion
93 | }
94 |
95 | func isDigit(b byte) bool { return b >= '0' && b <= '9' }
96 |
97 | func dddToByte(s []byte) byte {
98 | return (s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0')
99 | }
100 |
101 | const (
102 | escapedByteSmall = "" +
103 | `\000\001\002\003\004\005\006\007\008\009` +
104 | `\010\011\012\013\014\015\016\017\018\019` +
105 | `\020\021\022\023\024\025\026\027\028\029` +
106 | `\030\031`
107 | escapedByteLarge = `\127\128\129` +
108 | `\130\131\132\133\134\135\136\137\138\139` +
109 | `\140\141\142\143\144\145\146\147\148\149` +
110 | `\150\151\152\153\154\155\156\157\158\159` +
111 | `\160\161\162\163\164\165\166\167\168\169` +
112 | `\170\171\172\173\174\175\176\177\178\179` +
113 | `\180\181\182\183\184\185\186\187\188\189` +
114 | `\190\191\192\193\194\195\196\197\198\199` +
115 | `\200\201\202\203\204\205\206\207\208\209` +
116 | `\210\211\212\213\214\215\216\217\218\219` +
117 | `\220\221\222\223\224\225\226\227\228\229` +
118 | `\230\231\232\233\234\235\236\237\238\239` +
119 | `\240\241\242\243\244\245\246\247\248\249` +
120 | `\250\251\252\253\254\255`
121 | )
122 |
123 | // escapeByte returns the \DDD escaping of b which must
124 | // satisfy b < ' ' || b > '~'.
125 | func escapeByte(b byte) string {
126 | if b < ' ' {
127 | return escapedByteSmall[b*4 : b*4+4]
128 | }
129 |
130 | b -= '~' + 1
131 | // The cast here is needed as b*4 may overflow byte.
132 | return escapedByteLarge[int(b)*4 : int(b)*4+4]
133 | }
134 |
135 | func packTxtString(buf []byte) string {
136 | var out strings.Builder
137 | out.Grow(3 + len(buf))
138 | for i := 0; i < len(buf); i++ {
139 | b := buf[i]
140 | switch {
141 | case b == '"' || b == '\\':
142 | out.WriteByte('\\')
143 | out.WriteByte(b)
144 | case b < ' ' || b > '~':
145 | out.WriteString(escapeByte(b))
146 | default:
147 | out.WriteByte(b)
148 | }
149 | }
150 | return out.String()
151 | }
152 |
153 | func unpackTxtString(s string) ([]byte, error) {
154 | bs := make([]byte, len(s))
155 | msg := make([]byte, 0)
156 | copy(bs, s)
157 | for i := 0; i < len(bs); i++ {
158 | if bs[i] == '\\' {
159 | i++
160 | if i == len(bs) {
161 | break
162 | }
163 | if i+2 < len(bs) && isDigit(bs[i]) && isDigit(bs[i+1]) && isDigit(bs[i+2]) {
164 | msg = append(msg, dddToByte(bs[i:]))
165 | i += 2
166 | } else if bs[i] == 't' {
167 | msg = append(msg, '\t')
168 | } else if bs[i] == 'r' {
169 | msg = append(msg, '\r')
170 | } else if bs[i] == 'n' {
171 | msg = append(msg, '\n')
172 | } else {
173 | msg = append(msg, bs[i])
174 | }
175 | } else {
176 | msg = append(msg, bs[i])
177 | }
178 | }
179 | return msg, nil
180 | }
181 |
182 | // normalize truncates the DNS response if needed depending on the protocol
183 | func normalize(proto string, req *dns.Msg, res *dns.Msg) {
184 | size := dnsSize(proto, req)
185 | // DNSCrypt encryption adds a header to each message, we should
186 | // consider this when truncating a message.
187 | // 64 should cover all cases
188 | size = size - 64
189 |
190 | // Truncate response message
191 | res.Truncate(size)
192 |
193 | // In case of UDP it is safer to simply remove all response records
194 | // dns.Msg.Truncate method will not consider that we need a response
195 | // shorter than dns.MinMsgSize
196 | if res.Truncated && proto == "udp" {
197 | res.Answer = nil
198 | }
199 | }
200 |
201 | // dnsSize returns if buffer size *advertised* in the requests OPT record.
202 | // Or when the request was over TCP, we return the maximum allowed size of 64K.
203 | func dnsSize(proto string, r *dns.Msg) int {
204 | size := uint16(0)
205 | if o := r.IsEdns0(); o != nil {
206 | size = o.UDPSize()
207 | }
208 |
209 | if proto != "udp" {
210 | return dns.MaxMsgSize
211 | }
212 |
213 | if size < dns.MinMsgSize {
214 | return dns.MinMsgSize
215 | }
216 |
217 | // normalize size
218 | return int(size)
219 | }
220 |
221 | // readPrefixed -- reads a DNS message with a 2-byte prefix containing message length
222 | func readPrefixed(conn net.Conn) ([]byte, error) {
223 | l := make([]byte, 2)
224 | _, err := conn.Read(l)
225 | if err != nil {
226 | return nil, err
227 | }
228 | packetLen := binary.BigEndian.Uint16(l)
229 | if packetLen > dns.MaxMsgSize {
230 | return nil, ErrQueryTooLarge
231 | }
232 |
233 | buf := make([]byte, packetLen)
234 | _, err = io.ReadFull(conn, buf)
235 | if err != nil {
236 | return nil, err
237 | }
238 | return buf, nil
239 | }
240 |
241 | // writePrefixed -- write a DNS message to a TCP connection
242 | // it first writes a 2-byte prefix followed by the message itself
243 | func writePrefixed(b []byte, conn net.Conn) error {
244 | l := make([]byte, 2)
245 | binary.BigEndian.PutUint16(l, uint16(len(b)))
246 | _, err := (&net.Buffers{l, b}).WriteTo(conn)
247 | return err
248 | }
249 |
250 | // isConnClosed - checks if the error signals of a closed server connecting
251 | func isConnClosed(err error) bool {
252 | if err == nil {
253 | return false
254 | }
255 | nerr, ok := err.(*net.OpError)
256 | if !ok {
257 | return false
258 | }
259 |
260 | if strings.Contains(nerr.Err.Error(), "use of closed network connection") {
261 | return true
262 | }
263 |
264 | return false
265 | }
266 |
--------------------------------------------------------------------------------
/util_test.go:
--------------------------------------------------------------------------------
1 | package dnscrypt
2 |
3 | import (
4 | "crypto/rand"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | func TestPadUnpad(t *testing.T) {
12 | longBuf := make([]byte, 272)
13 | _, err := rand.Read(longBuf)
14 | require.NoError(t, err)
15 |
16 | tests := []struct {
17 | packet []byte
18 | expPaddedLen int
19 | }{
20 | {[]byte("Example Test DNS packet"), 256},
21 | {longBuf, 320},
22 | }
23 | for i, test := range tests {
24 | padded := pad(test.packet)
25 | assert.Equal(t, test.expPaddedLen, len(padded), "test %d", i)
26 |
27 | unpadded, err := unpad(padded)
28 | assert.Nil(t, err, "test %d", i)
29 | assert.Equal(t, test.packet, unpadded, "test %d", i)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/xsecretbox/doc.go:
--------------------------------------------------------------------------------
1 | // Package xsecretbox implements encryption/decryption of a message using specified keys
2 | package xsecretbox
3 |
--------------------------------------------------------------------------------
/xsecretbox/sharedkey.go:
--------------------------------------------------------------------------------
1 | package xsecretbox
2 |
3 | import (
4 | "errors"
5 |
6 | "golang.org/x/crypto/chacha20"
7 | "golang.org/x/crypto/curve25519"
8 | )
9 |
10 | // SharedKey computes a shared secret compatible with the one used by
11 | // `crypto_box_xchacha20poly1305`.
12 | func SharedKey(secretKey [curve25519.ScalarSize]byte, publicKey [curve25519.PointSize]byte) ([KeySize]byte, error) {
13 | var sharedKey [curve25519.PointSize]byte
14 |
15 | sk, err := curve25519.X25519(secretKey[:], publicKey[:])
16 | if err != nil {
17 | return sharedKey, err
18 | }
19 |
20 | c := byte(0)
21 | for i := 0; i < KeySize; i++ {
22 | sharedKey[i] = sk[i]
23 | c |= sk[i]
24 | }
25 | if c == 0 {
26 | return sharedKey, errors.New("weak public key")
27 | }
28 | var nonce [16]byte // HChaCha20 uses only 16 bytes long nonces
29 |
30 | hRes, err := chacha20.HChaCha20(sharedKey[:], nonce[:])
31 | if err != nil {
32 | return [KeySize]byte{}, err
33 | }
34 |
35 | return ([KeySize]byte)(hRes), nil
36 | }
37 |
--------------------------------------------------------------------------------
/xsecretbox/xsecretbox.go:
--------------------------------------------------------------------------------
1 | package xsecretbox
2 |
3 | import (
4 | "crypto/subtle"
5 | "errors"
6 |
7 | "golang.org/x/crypto/chacha20"
8 | "golang.org/x/crypto/poly1305"
9 | )
10 |
11 | const (
12 | // KeySize is what the name suggests
13 | KeySize = chacha20.KeySize
14 | // NonceSize is what the name suggests
15 | NonceSize = chacha20.NonceSizeX
16 | // TagSize is what the name suggests
17 | TagSize = poly1305.TagSize
18 | // BlockSize is what the name suggests
19 | BlockSize = 64
20 | )
21 |
22 | // Seal does what the name suggests
23 | func Seal(out, nonce, message, key []byte) []byte {
24 | if len(nonce) != NonceSize {
25 | panic("unsupported nonce size")
26 | }
27 | if len(key) != KeySize {
28 | panic("unsupported key size")
29 | }
30 |
31 | var firstBlock [BlockSize]byte
32 | cipher, err := chacha20.NewUnauthenticatedCipher(key, nonce)
33 | if err != nil {
34 | panic(err)
35 | }
36 | cipher.XORKeyStream(firstBlock[:], firstBlock[:])
37 | var polyKey [KeySize]byte
38 | copy(polyKey[:], firstBlock[:KeySize])
39 |
40 | ret, out := sliceForAppend(out, TagSize+len(message))
41 | firstMessageBlock := message
42 | if len(firstMessageBlock) > (BlockSize - KeySize) {
43 | firstMessageBlock = firstMessageBlock[:(BlockSize - KeySize)]
44 | }
45 |
46 | tagOut := out
47 | out = out[poly1305.TagSize:]
48 | for i, x := range firstMessageBlock {
49 | out[i] = firstBlock[(BlockSize-KeySize)+i] ^ x
50 | }
51 | message = message[len(firstMessageBlock):]
52 | ciphertext := out
53 | out = out[len(firstMessageBlock):]
54 |
55 | cipher.SetCounter(1)
56 | cipher.XORKeyStream(out, message)
57 |
58 | var tag [TagSize]byte
59 | hash := poly1305.New(&polyKey)
60 | _, _ = hash.Write(ciphertext)
61 | hash.Sum(tag[:0])
62 | copy(tagOut, tag[:])
63 |
64 | return ret
65 | }
66 |
67 | // Open does what the name suggests
68 | func Open(out, nonce, box, key []byte) ([]byte, error) {
69 | if len(nonce) != NonceSize {
70 | panic("unsupported nonce size")
71 | }
72 | if len(key) != KeySize {
73 | panic("unsupported key size")
74 | }
75 | if len(box) < TagSize {
76 | return nil, errors.New("ciphertext is too short")
77 | }
78 |
79 | var firstBlock [BlockSize]byte
80 | cipher, err := chacha20.NewUnauthenticatedCipher(key, nonce)
81 | if err != nil {
82 | panic(err)
83 | }
84 | cipher.XORKeyStream(firstBlock[:], firstBlock[:])
85 | var polyKey [KeySize]byte
86 | copy(polyKey[:], firstBlock[:KeySize])
87 |
88 | var tag [TagSize]byte
89 | ciphertext := box[TagSize:]
90 | hash := poly1305.New(&polyKey)
91 | _, _ = hash.Write(ciphertext)
92 | hash.Sum(tag[:0])
93 | if subtle.ConstantTimeCompare(tag[:], box[:TagSize]) != 1 {
94 | return nil, errors.New("ciphertext authentication failed")
95 | }
96 |
97 | ret, out := sliceForAppend(out, len(ciphertext))
98 |
99 | firstMessageBlock := ciphertext
100 | if len(firstMessageBlock) > (BlockSize - KeySize) {
101 | firstMessageBlock = firstMessageBlock[:(BlockSize - KeySize)]
102 | }
103 | for i, x := range firstMessageBlock {
104 | out[i] = firstBlock[(BlockSize-KeySize)+i] ^ x
105 | }
106 | ciphertext = ciphertext[len(firstMessageBlock):]
107 | out = out[len(firstMessageBlock):]
108 |
109 | cipher.SetCounter(1)
110 | cipher.XORKeyStream(out, ciphertext)
111 | return ret, nil
112 | }
113 |
114 | func sliceForAppend(in []byte, n int) (head, tail []byte) {
115 | if total := len(in) + n; cap(in) >= total {
116 | head = in[:total]
117 | } else {
118 | head = make([]byte, total)
119 | copy(head, in)
120 | }
121 | tail = head[len(in):]
122 | return
123 | }
124 |
--------------------------------------------------------------------------------
/xsecretbox/xsecretbox_test.go:
--------------------------------------------------------------------------------
1 | package xsecretbox
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestSecretbox(t *testing.T) {
9 | key := [32]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}
10 | nonce := [24]byte{23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
11 | src := []byte{42, 42, 42, 42, 42, 42, 42, 42, 42, 42}
12 |
13 | dst := Seal(nil, nonce[:], src[:], key[:])
14 | dec, err := Open(nil, nonce[:], dst[:], key[:])
15 | if err != nil || !bytes.Equal(src, dec) {
16 | t.Errorf("got %x instead of %x", dec, src)
17 | }
18 |
19 | dst[0]++
20 | _, err = Open(nil, nonce[:], dst[:], key[:])
21 | if err == nil {
22 | t.Errorf("tag validation failed")
23 | }
24 |
25 | _, _ = SharedKey(key, key)
26 | }
27 |
--------------------------------------------------------------------------------