├── .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 | [![Code Coverage](https://img.shields.io/codecov/c/github/ameshkov/dnscrypt/master.svg)](https://codecov.io/github/ameshkov/dnscrypt?branch=master) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/ameshkov/dnscrypt)](https://goreportcard.com/report/ameshkov/dnscrypt) 3 | [![Go Doc](https://godoc.org/github.com/ameshkov/dnscrypt?status.svg)](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 | --------------------------------------------------------------------------------