Compare commits
235 Commits
Author | SHA1 | Date |
---|---|---|
|
d8fbd0a755 | |
|
f8a185d39e | |
|
0089167cae | |
|
fe20d5d323 | |
|
41a7730f43 | |
|
4bd038eb76 | |
|
caa3fe0583 | |
|
3b7e0b9bdd | |
|
8c643eba82 | |
|
16b12df562 | |
|
4822b271aa | |
|
b3dfea0715 | |
|
69924a02cf | |
|
ff611cdc4b | |
|
eb4745b695 | |
|
7413c83334 | |
|
5521648610 | |
|
bfcbf0fd23 | |
|
feda877277 | |
|
0d2c95b99c | |
|
c760d3c7f1 | |
|
656b7409ac | |
|
08c2616301 | |
|
dedee46bd4 | |
|
49c1b2e20f | |
|
045ac4ec6c | |
|
57e2e627a6 | |
|
2f577ca35d | |
|
d70eb7b9e1 | |
|
d48e92a0e6 | |
|
05140a3136 | |
|
af1ebf55eb | |
|
84af068d46 | |
|
33e64002b6 | |
|
51afb90ed3 | |
|
0544c8bb11 | |
|
af5144a5ca | |
|
32b1ed5f32 | |
|
294c41a1d8 | |
|
ba44371638 | |
|
f4af58267c | |
|
3a58872b63 | |
|
3b8982ccc6 | |
|
7318b01e11 | |
|
1630ffe2ca | |
|
4e8fe099f4 | |
|
df84acab71 | |
|
ab67aa6423 | |
|
af0c865ab3 | |
|
c23d8b3ce0 | |
|
996478ee91 | |
|
bd70190c4d | |
|
595ee5aa98 | |
|
ce48a4b9ef | |
|
9922549621 | |
|
d2b5d38d4f | |
|
9f8f2e3a3c | |
|
21ccaf84aa | |
|
cce7f43db4 | |
|
2a9acc8d83 | |
|
c99ea652e3 | |
|
88913150f0 | |
|
83b388a80c | |
|
40060b4a4b | |
|
ad89e5bc70 | |
|
797f1f2953 | |
|
a614451ab3 | |
|
e5407eb800 | |
|
40ce7c7df7 | |
|
b694ab3d93 | |
|
4fdbc51bbd | |
|
db96610e5e | |
|
375601dc88 | |
|
883641f4a9 | |
|
2f14d104f3 | |
|
9884b9f446 | |
|
2543d8bb2d | |
|
c08efdcc07 | |
|
4ec3e54a9e | |
|
4495f1939f | |
|
67bd57debd | |
|
7d5e1ea350 | |
|
ee8fef6743 | |
|
35023fab5c | |
|
2fd5af9f92 | |
|
e6df8867af | |
|
ba2d042a57 | |
|
a1362108be | |
|
a25c26b5ed | |
|
13238cb6ad | |
|
f9dc403cff | |
|
731b191cab | |
|
59aea23afe | |
|
23c4faca9d | |
|
428cef3187 | |
|
91eca25c30 | |
|
9732cfa6b8 | |
|
1ba9487b18 | |
|
3b41a31342 | |
|
fa528cceb7 | |
|
0e1c4e69dd | |
|
a3ad44419a | |
|
93945c2844 | |
|
db53c847ca | |
|
be51022368 | |
|
6d41f43022 | |
|
04c41addaf | |
|
276b51d84b | |
|
a433fbede4 | |
|
61a22d0ee6 | |
|
95dddd3867 | |
|
3b0ffe413f | |
|
10e0aeedbe | |
|
ef286f8f39 | |
|
5579226123 | |
|
08cf611c2b | |
|
eaab7b7e85 | |
|
0972db6834 | |
|
cec9156531 | |
|
68df4402de | |
|
7a5f1127f7 | |
|
efdec21496 | |
|
034f791cf8 | |
|
9df839b2b4 | |
|
da812eed45 | |
|
81df27db17 | |
|
86044e4e05 | |
|
34cecfe1b4 | |
|
de1def76d8 | |
|
f3da20bc00 | |
|
a7a0eafd7a | |
|
9093928550 | |
|
50b4756e47 | |
|
54ab126a04 | |
|
f17e6c7171 | |
|
978b9a827a | |
|
064ba4b789 | |
|
203ad2480b | |
|
0ffcea3295 | |
|
b7da9d95e0 | |
|
b28dcc1849 | |
|
8f63c2d20c | |
|
b7703d0fa0 | |
|
1fc9fa1db0 | |
|
d128d10d17 | |
|
5bfe94bb6e | |
|
67373879ce | |
|
2c9b7cfbaa | |
|
fd9c7eb788 | |
|
923fc6bc72 | |
|
f515aa579d | |
|
524a80c35d | |
|
438e446f5c | |
|
40ecd66164 | |
|
1d3a971542 | |
|
f0dca1ef05 | |
|
418631f446 | |
|
9dcf47a409 | |
|
7963800469 | |
|
6c0c4e6581 | |
|
c9b62b4215 | |
|
e636c10380 | |
|
ba5b1f0bae | |
|
eda228adcf | |
|
711e0fd90d | |
|
bfd8601222 | |
|
b3cafcb268 | |
|
a72e5ceb18 | |
|
bd4ba36771 | |
|
aae7df65e6 | |
|
8ebf2e419d | |
|
6d0449f981 | |
|
0788ed5f4e | |
|
730ff1f016 | |
|
78ecb5db60 | |
|
a98e771ba5 | |
|
22cda6dc4f | |
|
9b7437f11d | |
|
4d4363a5dc | |
|
4334efe802 | |
|
40eab7a196 | |
|
997f079b75 | |
|
58a17b97a1 | |
|
1e224ff5de | |
|
1cd342c79a | |
|
041b4bc010 | |
|
76b57d0384 | |
|
557870346a | |
|
046ae4ead6 | |
|
8ebfd8abbb | |
|
93f749db12 | |
|
1208fbdde0 | |
|
bb79ca102d | |
|
513f7ec990 | |
|
9a6f1f2dc9 | |
|
e393768b85 | |
|
9578caeab0 | |
|
b733ad8671 | |
|
ba5bfd0295 | |
|
ab6ac402ba | |
|
2acbc9eff3 | |
|
dcb849b337 | |
|
c674456565 | |
|
5825da9f4d | |
|
b13675009d | |
|
7f2bf8764a | |
|
d89f1e3d4b | |
|
ee62c8b086 | |
|
9cfcfb2209 | |
|
25cacca8ca | |
|
cbc52d2408 | |
|
8a56deec68 | |
|
d16ecb693e | |
|
1545072057 | |
|
fbd426fefa | |
|
a2c73fb86d | |
|
ccd41ffaf8 | |
|
77cba59d63 | |
|
9c315c51c3 | |
|
087e486609 | |
|
0930b62a13 | |
|
d49318b5a0 | |
|
37f455fa04 | |
|
2f1ea90356 | |
|
77c7d907b4 | |
|
59b8e6b3db | |
|
92185d1e17 | |
|
56c04f1fec | |
|
8aa92d4e02 | |
|
357af3038a | |
|
cfee849963 | |
|
73601d4aed | |
|
0460860e89 | |
|
d1c1f95f67 | |
|
d051b464e9 |
|
@ -0,0 +1,32 @@
|
||||||
|
name: "Code scanning - action"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, ]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 23 * * 5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
CodeQL-Build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- run: git checkout HEAD^2
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: Go
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build and Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go: [ 1.19.x, 1.20.x ]
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
19
.travis.yml
19
.travis.yml
|
@ -1,19 +0,0 @@
|
||||||
language: go
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.10.x
|
|
||||||
- 1.11.x
|
|
||||||
- 1.12.x
|
|
||||||
- tip
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
# don't use the miekg/dns when testing forks
|
|
||||||
- mkdir -p $GOPATH/src/github.com/miekg
|
|
||||||
- ln -s $TRAVIS_BUILD_DIR $GOPATH/src/github.com/miekg/ || true
|
|
||||||
|
|
||||||
script:
|
|
||||||
- go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
|
|
@ -0,0 +1 @@
|
||||||
|
* @miekg @tmthrgd
|
|
@ -1,57 +0,0 @@
|
||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
|
||||||
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:6914c49eed986dfb8dffb33516fa129c49929d4d873f41e073c83c11c372b870"
|
|
||||||
name = "golang.org/x/crypto"
|
|
||||||
packages = [
|
|
||||||
"ed25519",
|
|
||||||
"ed25519/internal/edwards25519",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "e3636079e1a4c1f337f212cc5cd2aca108f6c900"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:08e41d63f8dac84d83797368b56cf0b339e42d0224e5e56668963c28aec95685"
|
|
||||||
name = "golang.org/x/net"
|
|
||||||
packages = [
|
|
||||||
"bpf",
|
|
||||||
"context",
|
|
||||||
"internal/iana",
|
|
||||||
"internal/socket",
|
|
||||||
"ipv4",
|
|
||||||
"ipv6",
|
|
||||||
]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "4dfa2610cdf3b287375bbba5b8f2a14d3b01d8de"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:b2ea75de0ccb2db2ac79356407f8a4cd8f798fe15d41b381c00abf3ae8e55ed1"
|
|
||||||
name = "golang.org/x/sync"
|
|
||||||
packages = ["errgroup"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
digest = "1:149a432fabebb8221a80f77731b1cd63597197ded4f14af606ebe3a0959004ec"
|
|
||||||
name = "golang.org/x/sys"
|
|
||||||
packages = ["unix"]
|
|
||||||
pruneopts = ""
|
|
||||||
revision = "e4b3c5e9061176387e7cea65e4dc5853801f3fb7"
|
|
||||||
|
|
||||||
[solve-meta]
|
|
||||||
analyzer-name = "dep"
|
|
||||||
analyzer-version = 1
|
|
||||||
input-imports = [
|
|
||||||
"golang.org/x/crypto/ed25519",
|
|
||||||
"golang.org/x/net/ipv4",
|
|
||||||
"golang.org/x/net/ipv6",
|
|
||||||
"golang.org/x/sync/errgroup",
|
|
||||||
"golang.org/x/sys/unix",
|
|
||||||
]
|
|
||||||
solver-name = "gps-cdcl"
|
|
||||||
solver-version = 1
|
|
38
Gopkg.toml
38
Gopkg.toml
|
@ -1,38 +0,0 @@
|
||||||
|
|
||||||
# Gopkg.toml example
|
|
||||||
#
|
|
||||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
|
||||||
# for detailed Gopkg.toml documentation.
|
|
||||||
#
|
|
||||||
# required = ["github.com/user/thing/cmd/thing"]
|
|
||||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
|
||||||
#
|
|
||||||
# [[constraint]]
|
|
||||||
# name = "github.com/user/project"
|
|
||||||
# version = "1.0.0"
|
|
||||||
#
|
|
||||||
# [[constraint]]
|
|
||||||
# name = "github.com/user/project2"
|
|
||||||
# branch = "dev"
|
|
||||||
# source = "github.com/myfork/project2"
|
|
||||||
#
|
|
||||||
# [[override]]
|
|
||||||
# name = "github.com/x/y"
|
|
||||||
# version = "2.4.0"
|
|
||||||
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/crypto"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/net"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/sys"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "golang.org/x/sync"
|
|
49
LICENSE
49
LICENSE
|
@ -1,32 +1,29 @@
|
||||||
Extensions of the original work are copyright (c) 2011 Miek Gieben
|
BSD 3-Clause License
|
||||||
|
|
||||||
As this is fork of the official Go code the same license applies:
|
Copyright (c) 2009, The Go Authors. Extensions copyright (c) 2011, Miek Gieben.
|
||||||
|
All rights reserved.
|
||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are
|
modification, are permitted provided that the following conditions are met:
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
notice, this list of conditions and the following disclaimer.
|
list of conditions and the following disclaimer.
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
this list of conditions and the following disclaimer in the documentation
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
and/or other materials provided with the distribution.
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
contributors may be used to endorse or promote products derived from
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
this software without specific prior written permission.
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Makefile for releasing.
|
# Makefile for releasing.
|
||||||
#
|
#
|
||||||
# The release is controlled from version.go. The version found there is
|
# The release is controlled from version.go. The version found there is
|
||||||
# used to tag the git repo, we're not building any artifects so there is nothing
|
# used to tag the git repo, we're not building any artifacts so there is nothing
|
||||||
# to upload to github.
|
# to upload to github.
|
||||||
#
|
#
|
||||||
# * Up the version in version.go
|
# * Up the version in version.go
|
||||||
|
|
33
README.md
33
README.md
|
@ -26,8 +26,8 @@ avoiding breaking changes wherever reasonable. We support the last two versions
|
||||||
A not-so-up-to-date-list-that-may-be-actually-current:
|
A not-so-up-to-date-list-that-may-be-actually-current:
|
||||||
|
|
||||||
* https://github.com/coredns/coredns
|
* https://github.com/coredns/coredns
|
||||||
* https://cloudflare.com
|
|
||||||
* https://github.com/abh/geodns
|
* https://github.com/abh/geodns
|
||||||
|
* https://github.com/baidu/bfe
|
||||||
* http://www.statdns.com/
|
* http://www.statdns.com/
|
||||||
* http://www.dnsinspect.com/
|
* http://www.dnsinspect.com/
|
||||||
* https://github.com/chuangbo/jianbing-dictionary-dns
|
* https://github.com/chuangbo/jianbing-dictionary-dns
|
||||||
|
@ -41,11 +41,9 @@ A not-so-up-to-date-list-that-may-be-actually-current:
|
||||||
* https://github.com/StalkR/dns-reverse-proxy
|
* https://github.com/StalkR/dns-reverse-proxy
|
||||||
* https://github.com/tianon/rawdns
|
* https://github.com/tianon/rawdns
|
||||||
* https://mesosphere.github.io/mesos-dns/
|
* https://mesosphere.github.io/mesos-dns/
|
||||||
* https://pulse.turbobytes.com/
|
|
||||||
* https://github.com/fcambus/statzone
|
* https://github.com/fcambus/statzone
|
||||||
* https://github.com/benschw/dns-clb-go
|
* https://github.com/benschw/dns-clb-go
|
||||||
* https://github.com/corny/dnscheck for <http://public-dns.info/>
|
* https://github.com/corny/dnscheck for <http://public-dns.info/>
|
||||||
* https://namesmith.io
|
|
||||||
* https://github.com/miekg/unbound
|
* https://github.com/miekg/unbound
|
||||||
* https://github.com/miekg/exdns
|
* https://github.com/miekg/exdns
|
||||||
* https://dnslookup.org
|
* https://dnslookup.org
|
||||||
|
@ -54,21 +52,35 @@ A not-so-up-to-date-list-that-may-be-actually-current:
|
||||||
* https://github.com/mehrdadrad/mylg
|
* https://github.com/mehrdadrad/mylg
|
||||||
* https://github.com/bamarni/dockness
|
* https://github.com/bamarni/dockness
|
||||||
* https://github.com/fffaraz/microdns
|
* https://github.com/fffaraz/microdns
|
||||||
* http://kelda.io
|
|
||||||
* https://github.com/ipdcode/hades <https://jd.com>
|
* https://github.com/ipdcode/hades <https://jd.com>
|
||||||
* https://github.com/StackExchange/dnscontrol/
|
* https://github.com/StackExchange/dnscontrol/
|
||||||
* https://www.dnsperf.com/
|
* https://www.dnsperf.com/
|
||||||
* https://dnssectest.net/
|
* https://dnssectest.net/
|
||||||
* https://dns.apebits.com
|
|
||||||
* https://github.com/oif/apex
|
* https://github.com/oif/apex
|
||||||
* https://github.com/jedisct1/dnscrypt-proxy
|
* https://github.com/jedisct1/dnscrypt-proxy
|
||||||
* https://github.com/jedisct1/rpdns
|
* https://github.com/jedisct1/rpdns
|
||||||
* https://github.com/xor-gate/sshfp
|
* https://github.com/xor-gate/sshfp
|
||||||
* https://github.com/rs/dnstrace
|
* https://github.com/rs/dnstrace
|
||||||
* https://blitiri.com.ar/p/dnss ([github mirror](https://github.com/albertito/dnss))
|
* https://blitiri.com.ar/p/dnss ([github mirror](https://github.com/albertito/dnss))
|
||||||
* https://github.com/semihalev/sdns
|
|
||||||
* https://render.com
|
* https://render.com
|
||||||
* https://github.com/peterzen/goresolver
|
* https://github.com/peterzen/goresolver
|
||||||
|
* https://github.com/folbricht/routedns
|
||||||
|
* https://domainr.com/
|
||||||
|
* https://zonedb.org/
|
||||||
|
* https://router7.org/
|
||||||
|
* https://github.com/fortio/dnsping
|
||||||
|
* https://github.com/Luzilla/dnsbl_exporter
|
||||||
|
* https://github.com/bodgit/tsig
|
||||||
|
* https://github.com/v2fly/v2ray-core (test only)
|
||||||
|
* https://kuma.io/
|
||||||
|
* https://www.misaka.io/services/dns
|
||||||
|
* https://ping.sx/dig
|
||||||
|
* https://fleetdeck.io/
|
||||||
|
* https://github.com/markdingo/autoreverse
|
||||||
|
* https://github.com/slackhq/nebula
|
||||||
|
* https://github.com/dnschecktool/dow-proxy
|
||||||
|
* https://dnscheck.tools/
|
||||||
|
|
||||||
|
|
||||||
Send pull request if you want to be listed here.
|
Send pull request if you want to be listed here.
|
||||||
|
|
||||||
|
@ -93,8 +105,8 @@ DNS Authors 2012-
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
|
|
||||||
Building is done with the `go` tool. If you have setup your GOPATH correctly, the following should
|
This library uses Go modules and uses semantic versioning. Building is done with the `go` tool, so
|
||||||
work:
|
the following should work:
|
||||||
|
|
||||||
go get github.com/miekg/dns
|
go get github.com/miekg/dns
|
||||||
go build github.com/miekg/dns
|
go build github.com/miekg/dns
|
||||||
|
@ -126,6 +138,7 @@ Example programs can be found in the `github.com/miekg/exdns` repository.
|
||||||
* 2915 - NAPTR record
|
* 2915 - NAPTR record
|
||||||
* 2929 - DNS IANA Considerations
|
* 2929 - DNS IANA Considerations
|
||||||
* 3110 - RSASHA1 DNS keys
|
* 3110 - RSASHA1 DNS keys
|
||||||
|
* 3123 - APL record
|
||||||
* 3225 - DO bit (DNSSEC OK)
|
* 3225 - DO bit (DNSSEC OK)
|
||||||
* 340{1,2,3} - NAPTR record
|
* 340{1,2,3} - NAPTR record
|
||||||
* 3445 - Limiting the scope of (DNS)KEY
|
* 3445 - Limiting the scope of (DNS)KEY
|
||||||
|
@ -152,6 +165,7 @@ Example programs can be found in the `github.com/miekg/exdns` repository.
|
||||||
* 6844 - CAA record
|
* 6844 - CAA record
|
||||||
* 6891 - EDNS0 update
|
* 6891 - EDNS0 update
|
||||||
* 6895 - DNS IANA considerations
|
* 6895 - DNS IANA considerations
|
||||||
|
* 6944 - DNSSEC DNSKEY Algorithm Status
|
||||||
* 6975 - Algorithm Understanding in DNSSEC
|
* 6975 - Algorithm Understanding in DNSSEC
|
||||||
* 7043 - EUI48/EUI64 records
|
* 7043 - EUI48/EUI64 records
|
||||||
* 7314 - DNS (EDNS) EXPIRE Option
|
* 7314 - DNS (EDNS) EXPIRE Option
|
||||||
|
@ -163,6 +177,9 @@ Example programs can be found in the `github.com/miekg/exdns` repository.
|
||||||
* 7873 - Domain Name System (DNS) Cookies
|
* 7873 - Domain Name System (DNS) Cookies
|
||||||
* 8080 - EdDSA for DNSSEC
|
* 8080 - EdDSA for DNSSEC
|
||||||
* 8499 - DNS Terminology
|
* 8499 - DNS Terminology
|
||||||
|
* 8659 - DNS Certification Authority Authorization (CAA) Resource Record
|
||||||
|
* 8914 - Extended DNS Errors
|
||||||
|
* 8976 - Message Digest for DNS Zones (ZONEMD RR)
|
||||||
|
|
||||||
## Loosely Based Upon
|
## Loosely Based Upon
|
||||||
|
|
||||||
|
|
|
@ -6,22 +6,30 @@ type MsgAcceptFunc func(dh Header) MsgAcceptAction
|
||||||
|
|
||||||
// DefaultMsgAcceptFunc checks the request and will reject if:
|
// DefaultMsgAcceptFunc checks the request and will reject if:
|
||||||
//
|
//
|
||||||
// * isn't a request (don't respond in that case).
|
// * isn't a request (don't respond in that case)
|
||||||
|
//
|
||||||
// * opcode isn't OpcodeQuery or OpcodeNotify
|
// * opcode isn't OpcodeQuery or OpcodeNotify
|
||||||
|
//
|
||||||
// * Zero bit isn't zero
|
// * Zero bit isn't zero
|
||||||
// * has more than 1 question in the question section
|
//
|
||||||
|
// * does not have exactly 1 question in the question section
|
||||||
|
//
|
||||||
// * has more than 1 RR in the Answer section
|
// * has more than 1 RR in the Answer section
|
||||||
|
//
|
||||||
// * has more than 0 RRs in the Authority section
|
// * has more than 0 RRs in the Authority section
|
||||||
|
//
|
||||||
// * has more than 2 RRs in the Additional section
|
// * has more than 2 RRs in the Additional section
|
||||||
var DefaultMsgAcceptFunc MsgAcceptFunc = defaultMsgAcceptFunc
|
var DefaultMsgAcceptFunc MsgAcceptFunc = defaultMsgAcceptFunc
|
||||||
|
|
||||||
// MsgAcceptAction represents the action to be taken.
|
// MsgAcceptAction represents the action to be taken.
|
||||||
type MsgAcceptAction int
|
type MsgAcceptAction int
|
||||||
|
|
||||||
|
// Allowed returned values from a MsgAcceptFunc.
|
||||||
const (
|
const (
|
||||||
MsgAccept MsgAcceptAction = iota // Accept the message
|
MsgAccept MsgAcceptAction = iota // Accept the message
|
||||||
MsgReject // Reject the message with a RcodeFormatError
|
MsgReject // Reject the message with a RcodeFormatError
|
||||||
MsgIgnore // Ignore the error and send nothing back.
|
MsgIgnore // Ignore the error and send nothing back.
|
||||||
|
MsgRejectNotImplemented // Reject the message with a RcodeNotImplemented
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultMsgAcceptFunc(dh Header) MsgAcceptAction {
|
func defaultMsgAcceptFunc(dh Header) MsgAcceptAction {
|
||||||
|
@ -32,12 +40,9 @@ func defaultMsgAcceptFunc(dh Header) MsgAcceptAction {
|
||||||
// Don't allow dynamic updates, because then the sections can contain a whole bunch of RRs.
|
// Don't allow dynamic updates, because then the sections can contain a whole bunch of RRs.
|
||||||
opcode := int(dh.Bits>>11) & 0xF
|
opcode := int(dh.Bits>>11) & 0xF
|
||||||
if opcode != OpcodeQuery && opcode != OpcodeNotify {
|
if opcode != OpcodeQuery && opcode != OpcodeNotify {
|
||||||
return MsgReject
|
return MsgRejectNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
if isZero := dh.Bits&_Z != 0; isZero {
|
|
||||||
return MsgReject
|
|
||||||
}
|
|
||||||
if dh.Qdcount != 1 {
|
if dh.Qdcount != 1 {
|
||||||
return MsgReject
|
return MsgReject
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
|
|
||||||
func TestAcceptNotify(t *testing.T) {
|
func TestAcceptNotify(t *testing.T) {
|
||||||
HandleFunc("example.org.", handleNotify)
|
HandleFunc("example.org.", handleNotify)
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
|
248
client.go
248
client.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -17,14 +18,35 @@ const (
|
||||||
tcpIdleTimeout time.Duration = 8 * time.Second
|
tcpIdleTimeout time.Duration = 8 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func isPacketConn(c net.Conn) bool {
|
||||||
|
if _, ok := c.(net.PacketConn); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ua, ok := c.LocalAddr().(*net.UnixAddr); ok {
|
||||||
|
return ua.Net == "unixgram" || ua.Net == "unixpacket"
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// A Conn represents a connection to a DNS server.
|
// A Conn represents a connection to a DNS server.
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
net.Conn // a net.Conn holding the connection
|
net.Conn // a net.Conn holding the connection
|
||||||
UDPSize uint16 // minimum receive buffer for UDP messages
|
UDPSize uint16 // minimum receive buffer for UDP messages
|
||||||
TsigSecret map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>, zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)
|
TsigSecret map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>, zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)
|
||||||
|
TsigProvider TsigProvider // An implementation of the TsigProvider interface. If defined it replaces TsigSecret and is used for all TSIG operations.
|
||||||
tsigRequestMAC string
|
tsigRequestMAC string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (co *Conn) tsigProvider() TsigProvider {
|
||||||
|
if co.TsigProvider != nil {
|
||||||
|
return co.TsigProvider
|
||||||
|
}
|
||||||
|
// tsigSecretProvider will return ErrSecret if co.TsigSecret is nil.
|
||||||
|
return tsigSecretProvider(co.TsigSecret)
|
||||||
|
}
|
||||||
|
|
||||||
// A Client defines parameters for a DNS client.
|
// A Client defines parameters for a DNS client.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Net string // if "tcp" or "tcp-tls" (DNS over TLS) a TCP query will be initiated, otherwise an UDP one (default is "" for UDP)
|
Net string // if "tcp" or "tcp-tls" (DNS over TLS) a TCP query will be initiated, otherwise an UDP one (default is "" for UDP)
|
||||||
|
@ -33,12 +55,13 @@ type Client struct {
|
||||||
Dialer *net.Dialer // a net.Dialer used to set local address, timeouts and more
|
Dialer *net.Dialer // a net.Dialer used to set local address, timeouts and more
|
||||||
// Timeout is a cumulative timeout for dial, write and read, defaults to 0 (disabled) - overrides DialTimeout, ReadTimeout,
|
// Timeout is a cumulative timeout for dial, write and read, defaults to 0 (disabled) - overrides DialTimeout, ReadTimeout,
|
||||||
// WriteTimeout when non-zero. Can be overridden with net.Dialer.Timeout (see Client.ExchangeWithDialer and
|
// WriteTimeout when non-zero. Can be overridden with net.Dialer.Timeout (see Client.ExchangeWithDialer and
|
||||||
// Client.Dialer) or context.Context.Deadline (see the deprecated ExchangeContext)
|
// Client.Dialer) or context.Context.Deadline (see ExchangeContext)
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
DialTimeout time.Duration // net.DialTimeout, defaults to 2 seconds, or net.Dialer.Timeout if expiring earlier - overridden by Timeout when that value is non-zero
|
DialTimeout time.Duration // net.DialTimeout, defaults to 2 seconds, or net.Dialer.Timeout if expiring earlier - overridden by Timeout when that value is non-zero
|
||||||
ReadTimeout time.Duration // net.Conn.SetReadTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero
|
ReadTimeout time.Duration // net.Conn.SetReadTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero
|
||||||
WriteTimeout time.Duration // net.Conn.SetWriteTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero
|
WriteTimeout time.Duration // net.Conn.SetWriteTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero
|
||||||
TsigSecret map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>, zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)
|
TsigSecret map[string]string // secret(s) for Tsig map[<zonename>]<base64 secret>, zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2)
|
||||||
|
TsigProvider TsigProvider // An implementation of the TsigProvider interface. If defined it replaces TsigSecret and is used for all TSIG operations.
|
||||||
SingleInflight bool // if true suppress multiple outstanding queries for the same Qname, Qtype and Qclass
|
SingleInflight bool // if true suppress multiple outstanding queries for the same Qname, Qtype and Qclass
|
||||||
group singleflight
|
group singleflight
|
||||||
}
|
}
|
||||||
|
@ -79,6 +102,12 @@ func (c *Client) writeTimeout() time.Duration {
|
||||||
|
|
||||||
// Dial connects to the address on the named network.
|
// Dial connects to the address on the named network.
|
||||||
func (c *Client) Dial(address string) (conn *Conn, err error) {
|
func (c *Client) Dial(address string) (conn *Conn, err error) {
|
||||||
|
return c.DialContext(context.Background(), address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext connects to the address on the named network, with a context.Context.
|
||||||
|
// For TLS over TCP (DoT) the context isn't used yet. This will be enabled when Go 1.18 is released.
|
||||||
|
func (c *Client) DialContext(ctx context.Context, address string) (conn *Conn, err error) {
|
||||||
// create a new dialer with the appropriate timeout
|
// create a new dialer with the appropriate timeout
|
||||||
var d net.Dialer
|
var d net.Dialer
|
||||||
if c.Dialer == nil {
|
if c.Dialer == nil {
|
||||||
|
@ -98,14 +127,22 @@ func (c *Client) Dial(address string) (conn *Conn, err error) {
|
||||||
if useTLS {
|
if useTLS {
|
||||||
network = strings.TrimSuffix(network, "-tls")
|
network = strings.TrimSuffix(network, "-tls")
|
||||||
|
|
||||||
|
// TODO(miekg): Enable after Go 1.18 is released, to be able to support two prev. releases.
|
||||||
|
/*
|
||||||
|
tlsDialer := tls.Dialer{
|
||||||
|
NetDialer: &d,
|
||||||
|
Config: c.TLSConfig,
|
||||||
|
}
|
||||||
|
conn.Conn, err = tlsDialer.DialContext(ctx, network, address)
|
||||||
|
*/
|
||||||
conn.Conn, err = tls.DialWithDialer(&d, network, address, c.TLSConfig)
|
conn.Conn, err = tls.DialWithDialer(&d, network, address, c.TLSConfig)
|
||||||
} else {
|
} else {
|
||||||
conn.Conn, err = d.Dial(network, address)
|
conn.Conn, err = d.DialContext(ctx, network, address)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
conn.UDPSize = c.UDPSize
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,37 +161,55 @@ func (c *Client) Dial(address string) (conn *Conn, err error) {
|
||||||
// To specify a local address or a timeout, the caller has to set the `Client.Dialer`
|
// To specify a local address or a timeout, the caller has to set the `Client.Dialer`
|
||||||
// attribute appropriately
|
// attribute appropriately
|
||||||
func (c *Client) Exchange(m *Msg, address string) (r *Msg, rtt time.Duration, err error) {
|
func (c *Client) Exchange(m *Msg, address string) (r *Msg, rtt time.Duration, err error) {
|
||||||
if !c.SingleInflight {
|
co, err := c.Dial(address)
|
||||||
return c.exchange(m, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
t := "nop"
|
|
||||||
if t1, ok := TypeToString[m.Question[0].Qtype]; ok {
|
|
||||||
t = t1
|
|
||||||
}
|
|
||||||
cl := "nop"
|
|
||||||
if cl1, ok := ClassToString[m.Question[0].Qclass]; ok {
|
|
||||||
cl = cl1
|
|
||||||
}
|
|
||||||
r, rtt, err, shared := c.group.Do(m.Question[0].Name+t+cl, func() (*Msg, time.Duration, error) {
|
|
||||||
return c.exchange(m, address)
|
|
||||||
})
|
|
||||||
if r != nil && shared {
|
|
||||||
r = r.Copy()
|
|
||||||
}
|
|
||||||
return r, rtt, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) exchange(m *Msg, a string) (r *Msg, rtt time.Duration, err error) {
|
|
||||||
var co *Conn
|
|
||||||
|
|
||||||
co, err = c.Dial(a)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
defer co.Close()
|
defer co.Close()
|
||||||
|
return c.ExchangeWithConn(m, co)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeWithConn has the same behavior as Exchange, just with a predetermined connection
|
||||||
|
// that will be used instead of creating a new one.
|
||||||
|
// Usage pattern with a *dns.Client:
|
||||||
|
//
|
||||||
|
// c := new(dns.Client)
|
||||||
|
// // connection management logic goes here
|
||||||
|
//
|
||||||
|
// conn := c.Dial(address)
|
||||||
|
// in, rtt, err := c.ExchangeWithConn(message, conn)
|
||||||
|
//
|
||||||
|
// This allows users of the library to implement their own connection management,
|
||||||
|
// as opposed to Exchange, which will always use new connections and incur the added overhead
|
||||||
|
// that entails when using "tcp" and especially "tcp-tls" clients.
|
||||||
|
//
|
||||||
|
// When the singleflight is set for this client the context is _not_ forwarded to the (shared) exchange, to
|
||||||
|
// prevent one cancellation from canceling all outstanding requests.
|
||||||
|
func (c *Client) ExchangeWithConn(m *Msg, conn *Conn) (r *Msg, rtt time.Duration, err error) {
|
||||||
|
return c.exchangeWithConnContext(context.Background(), m, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) exchangeWithConnContext(ctx context.Context, m *Msg, conn *Conn) (r *Msg, rtt time.Duration, err error) {
|
||||||
|
if !c.SingleInflight {
|
||||||
|
return c.exchangeContext(ctx, m, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := m.Question[0]
|
||||||
|
key := fmt.Sprintf("%s:%d:%d", q.Name, q.Qtype, q.Qclass)
|
||||||
|
r, rtt, err, shared := c.group.Do(key, func() (*Msg, time.Duration, error) {
|
||||||
|
// When we're doing singleflight we don't want one context cancellation, cancel _all_ outstanding queries.
|
||||||
|
// Hence we ignore the context and use Background().
|
||||||
|
return c.exchangeContext(context.Background(), m, conn)
|
||||||
|
})
|
||||||
|
if r != nil && shared {
|
||||||
|
r = r.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, rtt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) exchangeContext(ctx context.Context, m *Msg, co *Conn) (r *Msg, rtt time.Duration, err error) {
|
||||||
opt := m.IsEdns0()
|
opt := m.IsEdns0()
|
||||||
// If EDNS0 is used use that for size.
|
// If EDNS0 is used use that for size.
|
||||||
if opt != nil && opt.UDPSize() >= MinMsgSize {
|
if opt != nil && opt.UDPSize() >= MinMsgSize {
|
||||||
|
@ -165,18 +220,41 @@ func (c *Client) exchange(m *Msg, a string) (r *Msg, rtt time.Duration, err erro
|
||||||
co.UDPSize = c.UDPSize
|
co.UDPSize = c.UDPSize
|
||||||
}
|
}
|
||||||
|
|
||||||
co.TsigSecret = c.TsigSecret
|
|
||||||
t := time.Now()
|
|
||||||
// write with the appropriate write timeout
|
// write with the appropriate write timeout
|
||||||
co.SetWriteDeadline(t.Add(c.getTimeoutForRequest(c.writeTimeout())))
|
t := time.Now()
|
||||||
|
writeDeadline := t.Add(c.getTimeoutForRequest(c.writeTimeout()))
|
||||||
|
readDeadline := t.Add(c.getTimeoutForRequest(c.readTimeout()))
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
if deadline.Before(writeDeadline) {
|
||||||
|
writeDeadline = deadline
|
||||||
|
}
|
||||||
|
if deadline.Before(readDeadline) {
|
||||||
|
readDeadline = deadline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
co.SetWriteDeadline(writeDeadline)
|
||||||
|
co.SetReadDeadline(readDeadline)
|
||||||
|
|
||||||
|
co.TsigSecret, co.TsigProvider = c.TsigSecret, c.TsigProvider
|
||||||
|
|
||||||
if err = co.WriteMsg(m); err != nil {
|
if err = co.WriteMsg(m); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
co.SetReadDeadline(time.Now().Add(c.getTimeoutForRequest(c.readTimeout())))
|
if isPacketConn(co.Conn) {
|
||||||
r, err = co.ReadMsg()
|
for {
|
||||||
if err == nil && r.Id != m.Id {
|
r, err = co.ReadMsg()
|
||||||
err = ErrId
|
// Ignore replies with mismatched IDs because they might be
|
||||||
|
// responses to earlier queries that timed out.
|
||||||
|
if err != nil || r.Id == m.Id {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r, err = co.ReadMsg()
|
||||||
|
if err == nil && r.Id != m.Id {
|
||||||
|
err = ErrId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rtt = time.Since(t)
|
rtt = time.Since(t)
|
||||||
return r, rtt, err
|
return r, rtt, err
|
||||||
|
@ -201,11 +279,8 @@ func (co *Conn) ReadMsg() (*Msg, error) {
|
||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
if t := m.IsTsig(); t != nil {
|
if t := m.IsTsig(); t != nil {
|
||||||
if _, ok := co.TsigSecret[t.Hdr.Name]; !ok {
|
|
||||||
return m, ErrSecret
|
|
||||||
}
|
|
||||||
// Need to work on the original message p, as that was used to calculate the tsig.
|
// Need to work on the original message p, as that was used to calculate the tsig.
|
||||||
err = TsigVerify(p, co.TsigSecret[t.Hdr.Name], co.tsigRequestMAC, false)
|
err = TsigVerifyWithProvider(p, co.tsigProvider(), co.tsigRequestMAC, false)
|
||||||
}
|
}
|
||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
|
@ -219,8 +294,15 @@ func (co *Conn) ReadMsgHeader(hdr *Header) ([]byte, error) {
|
||||||
n int
|
n int
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
switch co.Conn.(type) {
|
|
||||||
case *net.TCPConn, *tls.Conn:
|
if isPacketConn(co.Conn) {
|
||||||
|
if co.UDPSize > MinMsgSize {
|
||||||
|
p = make([]byte, co.UDPSize)
|
||||||
|
} else {
|
||||||
|
p = make([]byte, MinMsgSize)
|
||||||
|
}
|
||||||
|
n, err = co.Read(p)
|
||||||
|
} else {
|
||||||
var length uint16
|
var length uint16
|
||||||
if err := binary.Read(co.Conn, binary.BigEndian, &length); err != nil {
|
if err := binary.Read(co.Conn, binary.BigEndian, &length); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -228,13 +310,6 @@ func (co *Conn) ReadMsgHeader(hdr *Header) ([]byte, error) {
|
||||||
|
|
||||||
p = make([]byte, length)
|
p = make([]byte, length)
|
||||||
n, err = io.ReadFull(co.Conn, p)
|
n, err = io.ReadFull(co.Conn, p)
|
||||||
default:
|
|
||||||
if co.UDPSize > MinMsgSize {
|
|
||||||
p = make([]byte, co.UDPSize)
|
|
||||||
} else {
|
|
||||||
p = make([]byte, MinMsgSize)
|
|
||||||
}
|
|
||||||
n, err = co.Read(p)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -260,21 +335,20 @@ func (co *Conn) Read(p []byte) (n int, err error) {
|
||||||
return 0, ErrConnEmpty
|
return 0, ErrConnEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
switch co.Conn.(type) {
|
if isPacketConn(co.Conn) {
|
||||||
case *net.TCPConn, *tls.Conn:
|
// UDP connection
|
||||||
var length uint16
|
return co.Conn.Read(p)
|
||||||
if err := binary.Read(co.Conn, binary.BigEndian, &length); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if int(length) > len(p) {
|
|
||||||
return 0, io.ErrShortBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
return io.ReadFull(co.Conn, p[:length])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UDP connection
|
var length uint16
|
||||||
return co.Conn.Read(p)
|
if err := binary.Read(co.Conn, binary.BigEndian, &length); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if int(length) > len(p) {
|
||||||
|
return 0, io.ErrShortBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadFull(co.Conn, p[:length])
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteMsg sends a message through the connection co.
|
// WriteMsg sends a message through the connection co.
|
||||||
|
@ -283,13 +357,8 @@ func (co *Conn) Read(p []byte) (n int, err error) {
|
||||||
func (co *Conn) WriteMsg(m *Msg) (err error) {
|
func (co *Conn) WriteMsg(m *Msg) (err error) {
|
||||||
var out []byte
|
var out []byte
|
||||||
if t := m.IsTsig(); t != nil {
|
if t := m.IsTsig(); t != nil {
|
||||||
mac := ""
|
// Set tsigRequestMAC for the next read, although only used in zone transfers.
|
||||||
if _, ok := co.TsigSecret[t.Hdr.Name]; !ok {
|
out, co.tsigRequestMAC, err = TsigGenerateWithProvider(m, co.tsigProvider(), co.tsigRequestMAC, false)
|
||||||
return ErrSecret
|
|
||||||
}
|
|
||||||
out, mac, err = TsigGenerate(m, co.TsigSecret[t.Hdr.Name], co.tsigRequestMAC, false)
|
|
||||||
// Set for the next read, although only used in zone transfers
|
|
||||||
co.tsigRequestMAC = mac
|
|
||||||
} else {
|
} else {
|
||||||
out, err = m.Pack()
|
out, err = m.Pack()
|
||||||
}
|
}
|
||||||
|
@ -301,21 +370,19 @@ func (co *Conn) WriteMsg(m *Msg) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write implements the net.Conn Write method.
|
// Write implements the net.Conn Write method.
|
||||||
func (co *Conn) Write(p []byte) (n int, err error) {
|
func (co *Conn) Write(p []byte) (int, error) {
|
||||||
switch co.Conn.(type) {
|
if len(p) > MaxMsgSize {
|
||||||
case *net.TCPConn, *tls.Conn:
|
return 0, &Error{err: "message too large"}
|
||||||
if len(p) > MaxMsgSize {
|
|
||||||
return 0, &Error{err: "message too large"}
|
|
||||||
}
|
|
||||||
|
|
||||||
l := make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(l, uint16(len(p)))
|
|
||||||
|
|
||||||
n, err := (&net.Buffers{l, p}).WriteTo(co.Conn)
|
|
||||||
return int(n), err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return co.Conn.Write(p)
|
if isPacketConn(co.Conn) {
|
||||||
|
return co.Conn.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := make([]byte, 2+len(p))
|
||||||
|
binary.BigEndian.PutUint16(msg, uint16(len(p)))
|
||||||
|
copy(msg[2:], p)
|
||||||
|
return co.Conn.Write(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the appropriate timeout for a specific request
|
// Return the appropriate timeout for a specific request
|
||||||
|
@ -351,7 +418,7 @@ func Dial(network, address string) (conn *Conn, err error) {
|
||||||
func ExchangeContext(ctx context.Context, m *Msg, a string) (r *Msg, err error) {
|
func ExchangeContext(ctx context.Context, m *Msg, a string) (r *Msg, err error) {
|
||||||
client := Client{Net: "udp"}
|
client := Client{Net: "udp"}
|
||||||
r, _, err = client.ExchangeContext(ctx, m, a)
|
r, _, err = client.ExchangeContext(ctx, m, a)
|
||||||
// ignorint rtt to leave the original ExchangeContext API unchanged, but
|
// ignoring rtt to leave the original ExchangeContext API unchanged, but
|
||||||
// this function will go away
|
// this function will go away
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
|
@ -364,7 +431,6 @@ func ExchangeContext(ctx context.Context, m *Msg, a string) (r *Msg, err error)
|
||||||
// co.WriteMsg(m)
|
// co.WriteMsg(m)
|
||||||
// in, _ := co.ReadMsg()
|
// in, _ := co.ReadMsg()
|
||||||
// co.Close()
|
// co.Close()
|
||||||
//
|
|
||||||
func ExchangeConn(c net.Conn, m *Msg) (r *Msg, err error) {
|
func ExchangeConn(c net.Conn, m *Msg) (r *Msg, err error) {
|
||||||
println("dns: ExchangeConn: this function is deprecated")
|
println("dns: ExchangeConn: this function is deprecated")
|
||||||
co := new(Conn)
|
co := new(Conn)
|
||||||
|
@ -407,15 +473,11 @@ func DialTimeoutWithTLS(network, address string, tlsConfig *tls.Config, timeout
|
||||||
// context, if present. If there is both a context deadline and a configured
|
// context, if present. If there is both a context deadline and a configured
|
||||||
// timeout on the client, the earliest of the two takes effect.
|
// timeout on the client, the earliest of the two takes effect.
|
||||||
func (c *Client) ExchangeContext(ctx context.Context, m *Msg, a string) (r *Msg, rtt time.Duration, err error) {
|
func (c *Client) ExchangeContext(ctx context.Context, m *Msg, a string) (r *Msg, rtt time.Duration, err error) {
|
||||||
var timeout time.Duration
|
conn, err := c.DialContext(ctx, a)
|
||||||
if deadline, ok := ctx.Deadline(); !ok {
|
if err != nil {
|
||||||
timeout = 0
|
return nil, 0, err
|
||||||
} else {
|
|
||||||
timeout = time.Until(deadline)
|
|
||||||
}
|
}
|
||||||
// not passing the context to the underlying calls, as the API does not support
|
defer conn.Close()
|
||||||
// context. For timeouts you should set up Client.Dialer and call Client.Exchange.
|
|
||||||
// TODO(tmthrgd,miekg): this is a race condition.
|
return c.exchangeWithConnContext(ctx, m, conn)
|
||||||
c.Dialer = &net.Dialer{Timeout: timeout}
|
|
||||||
return c.Exchange(m, a)
|
|
||||||
}
|
}
|
||||||
|
|
222
client_test.go
222
client_test.go
|
@ -3,19 +3,116 @@ package dns
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestIsPacketConn(t *testing.T) {
|
||||||
|
// UDP
|
||||||
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Shutdown()
|
||||||
|
c, err := net.Dial("udp", addrstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
if !isPacketConn(c) {
|
||||||
|
t.Error("UDP connection should be a packet conn")
|
||||||
|
}
|
||||||
|
if !isPacketConn(struct{ *net.UDPConn }{c.(*net.UDPConn)}) {
|
||||||
|
t.Error("UDP connection (wrapped type) should be a packet conn")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCP
|
||||||
|
s, addrstr, _, err = RunLocalTCPServer(":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Shutdown()
|
||||||
|
c, err = net.Dial("tcp", addrstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
if isPacketConn(c) {
|
||||||
|
t.Error("TCP connection should not be a packet conn")
|
||||||
|
}
|
||||||
|
if isPacketConn(struct{ *net.TCPConn }{c.(*net.TCPConn)}) {
|
||||||
|
t.Error("TCP connection (wrapped type) should not be a packet conn")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unix datagram
|
||||||
|
s, addrstr, _, err = RunLocalUnixGramServer(filepath.Join(t.TempDir(), "unixgram.sock"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Shutdown()
|
||||||
|
c, err = net.Dial("unixgram", addrstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
if !isPacketConn(c) {
|
||||||
|
t.Error("Unix datagram connection should be a packet conn")
|
||||||
|
}
|
||||||
|
if !isPacketConn(struct{ *net.UnixConn }{c.(*net.UnixConn)}) {
|
||||||
|
t.Error("Unix datagram connection (wrapped type) should be a packet conn")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unix Seqpacket
|
||||||
|
shutChan, addrstr, err := RunLocalUnixSeqPacketServer(filepath.Join(t.TempDir(), "unixpacket.sock"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
shutChan <- &struct{}{}
|
||||||
|
}()
|
||||||
|
c, err = net.Dial("unixpacket", addrstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
if !isPacketConn(c) {
|
||||||
|
t.Error("Unix datagram connection should be a packet conn")
|
||||||
|
}
|
||||||
|
if !isPacketConn(struct{ *net.UnixConn }{c.(*net.UnixConn)}) {
|
||||||
|
t.Error("Unix datagram connection (wrapped type) should be a packet conn")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unix stream
|
||||||
|
s, addrstr, _, err = RunLocalUnixServer(filepath.Join(t.TempDir(), "unixstream.sock"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Shutdown()
|
||||||
|
c, err = net.Dial("unix", addrstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
if isPacketConn(c) {
|
||||||
|
t.Error("Unix stream connection should not be a packet conn")
|
||||||
|
}
|
||||||
|
if isPacketConn(struct{ *net.UnixConn }{c.(*net.UnixConn)}) {
|
||||||
|
t.Error("Unix stream connection (wrapped type) should not be a packet conn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDialUDP(t *testing.T) {
|
func TestDialUDP(t *testing.T) {
|
||||||
HandleFunc("miek.nl.", HelloServer)
|
HandleFunc("miek.nl.", HelloServer)
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -38,7 +135,7 @@ func TestClientSync(t *testing.T) {
|
||||||
HandleFunc("miek.nl.", HelloServer)
|
HandleFunc("miek.nl.", HelloServer)
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -72,7 +169,7 @@ func TestClientLocalAddress(t *testing.T) {
|
||||||
HandleFunc("miek.nl.", HelloServerEchoAddrPort)
|
HandleFunc("miek.nl.", HelloServerEchoAddrPort)
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -116,7 +213,7 @@ func TestClientTLSSyncV4(t *testing.T) {
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
}
|
}
|
||||||
|
|
||||||
s, addrstr, err := RunLocalTLSServer(":0", &config)
|
s, addrstr, _, err := RunLocalTLSServer(":0", &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -162,11 +259,40 @@ func TestClientTLSSyncV4(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isNetworkTimeout(err error) bool {
|
||||||
|
// TODO: when Go 1.14 support is dropped, do this: https://golang.org/doc/go1.15#net
|
||||||
|
var netError net.Error
|
||||||
|
return errors.As(err, &netError) && netError.Timeout()
|
||||||
|
}
|
||||||
|
|
||||||
func TestClientSyncBadID(t *testing.T) {
|
func TestClientSyncBadID(t *testing.T) {
|
||||||
HandleFunc("miek.nl.", HelloServerBadID)
|
HandleFunc("miek.nl.", HelloServerBadID)
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("miek.nl.", TypeSOA)
|
||||||
|
|
||||||
|
// Test with client.Exchange, the plain Exchange function is just a wrapper, so
|
||||||
|
// we don't need to test that separately.
|
||||||
|
c := &Client{
|
||||||
|
Timeout: 10 * time.Millisecond,
|
||||||
|
}
|
||||||
|
if _, _, err := c.Exchange(m, addrstr); err == nil || !isNetworkTimeout(err) {
|
||||||
|
t.Errorf("query did not time out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientSyncBadThenGoodID(t *testing.T) {
|
||||||
|
HandleFunc("miek.nl.", HelloServerBadThenGoodID)
|
||||||
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -176,11 +302,32 @@ func TestClientSyncBadID(t *testing.T) {
|
||||||
m.SetQuestion("miek.nl.", TypeSOA)
|
m.SetQuestion("miek.nl.", TypeSOA)
|
||||||
|
|
||||||
c := new(Client)
|
c := new(Client)
|
||||||
if _, _, err := c.Exchange(m, addrstr); err != ErrId {
|
r, _, err := c.Exchange(m, addrstr)
|
||||||
t.Errorf("did not find a bad Id")
|
if err != nil {
|
||||||
|
t.Errorf("failed to exchange: %v", err)
|
||||||
}
|
}
|
||||||
// And now with plain Exchange().
|
if r.Id != m.Id {
|
||||||
if _, err := Exchange(m, addrstr); err != ErrId {
|
t.Errorf("failed to get response with expected Id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientSyncTCPBadID(t *testing.T) {
|
||||||
|
HandleFunc("miek.nl.", HelloServerBadID)
|
||||||
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
|
s, addrstr, _, err := RunLocalTCPServer(":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("miek.nl.", TypeSOA)
|
||||||
|
|
||||||
|
c := &Client{
|
||||||
|
Net: "tcp",
|
||||||
|
}
|
||||||
|
if _, _, err := c.Exchange(m, addrstr); err != ErrId {
|
||||||
t.Errorf("did not find a bad Id")
|
t.Errorf("did not find a bad Id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,7 +336,7 @@ func TestClientEDNS0(t *testing.T) {
|
||||||
HandleFunc("miek.nl.", HelloServer)
|
HandleFunc("miek.nl.", HelloServer)
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -236,7 +383,7 @@ func TestClientEDNS0Local(t *testing.T) {
|
||||||
HandleFunc("miek.nl.", handler)
|
HandleFunc("miek.nl.", handler)
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %s", err)
|
t.Fatalf("unable to run test server: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -286,7 +433,7 @@ func TestClientConn(t *testing.T) {
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
// This uses TCP just to make it slightly different than TestClientSync
|
// This uses TCP just to make it slightly different than TestClientSync
|
||||||
s, addrstr, err := RunLocalTCPServer(":0")
|
s, addrstr, _, err := RunLocalTCPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -335,6 +482,24 @@ func TestClientConn(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClientConnWriteSinglePacket(t *testing.T) {
|
||||||
|
c := &countingConn{}
|
||||||
|
conn := Conn{
|
||||||
|
Conn: c,
|
||||||
|
}
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("miek.nl.", TypeTXT)
|
||||||
|
err := conn.WriteMsg(m)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.writes != 1 {
|
||||||
|
t.Fatalf("incorrect number of Write calls")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTruncatedMsg(t *testing.T) {
|
func TestTruncatedMsg(t *testing.T) {
|
||||||
m := new(Msg)
|
m := new(Msg)
|
||||||
m.SetQuestion("miek.nl.", TypeSRV)
|
m.SetQuestion("miek.nl.", TypeSRV)
|
||||||
|
@ -533,7 +698,7 @@ func TestConcurrentExchanges(t *testing.T) {
|
||||||
HandleFunc("miek.nl.", handler)
|
HandleFunc("miek.nl.", handler)
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %s", err)
|
t.Fatalf("unable to run test server: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -565,3 +730,34 @@ func TestConcurrentExchanges(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExchangeWithConn(t *testing.T) {
|
||||||
|
HandleFunc("miek.nl.", HelloServer)
|
||||||
|
defer HandleRemove("miek.nl.")
|
||||||
|
|
||||||
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("miek.nl.", TypeSOA)
|
||||||
|
|
||||||
|
c := new(Client)
|
||||||
|
conn, err := c.Dial(addrstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, _, err := c.ExchangeWithConn(m, conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to exchange: %v", err)
|
||||||
|
}
|
||||||
|
if r == nil {
|
||||||
|
t.Fatal("response is nil")
|
||||||
|
}
|
||||||
|
if r.Rcode != RcodeSuccess {
|
||||||
|
t.Errorf("failed to get an valid answer\n%v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
42
defaults.go
42
defaults.go
|
@ -105,7 +105,7 @@ func (dns *Msg) SetAxfr(z string) *Msg {
|
||||||
|
|
||||||
// SetTsig appends a TSIG RR to the message.
|
// SetTsig appends a TSIG RR to the message.
|
||||||
// This is only a skeleton TSIG RR that is added as the last RR in the
|
// This is only a skeleton TSIG RR that is added as the last RR in the
|
||||||
// additional section. The Tsig is calculated when the message is being send.
|
// additional section. The TSIG is calculated when the message is being send.
|
||||||
func (dns *Msg) SetTsig(z, algo string, fudge uint16, timesigned int64) *Msg {
|
func (dns *Msg) SetTsig(z, algo string, fudge uint16, timesigned int64) *Msg {
|
||||||
t := new(TSIG)
|
t := new(TSIG)
|
||||||
t.Hdr = RR_Header{z, TypeTSIG, ClassANY, 0, 0}
|
t.Hdr = RR_Header{z, TypeTSIG, ClassANY, 0, 0}
|
||||||
|
@ -146,10 +146,9 @@ func (dns *Msg) IsTsig() *TSIG {
|
||||||
// record in the additional section will do. It returns the OPT record
|
// record in the additional section will do. It returns the OPT record
|
||||||
// found or nil.
|
// found or nil.
|
||||||
func (dns *Msg) IsEdns0() *OPT {
|
func (dns *Msg) IsEdns0() *OPT {
|
||||||
// EDNS0 is at the end of the additional section, start there.
|
// RFC 6891, Section 6.1.1 allows the OPT record to appear
|
||||||
// We might want to change this to *only* look at the last two
|
// anywhere in the additional record section, but it's usually at
|
||||||
// records. So we see TSIG and/or OPT - this a slightly bigger
|
// the end so start there.
|
||||||
// change though.
|
|
||||||
for i := len(dns.Extra) - 1; i >= 0; i-- {
|
for i := len(dns.Extra) - 1; i >= 0; i-- {
|
||||||
if dns.Extra[i].Header().Rrtype == TypeOPT {
|
if dns.Extra[i].Header().Rrtype == TypeOPT {
|
||||||
return dns.Extra[i].(*OPT)
|
return dns.Extra[i].(*OPT)
|
||||||
|
@ -158,6 +157,21 @@ func (dns *Msg) IsEdns0() *OPT {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// popEdns0 is like IsEdns0, but it removes the record from the message.
|
||||||
|
func (dns *Msg) popEdns0() *OPT {
|
||||||
|
// RFC 6891, Section 6.1.1 allows the OPT record to appear
|
||||||
|
// anywhere in the additional record section, but it's usually at
|
||||||
|
// the end so start there.
|
||||||
|
for i := len(dns.Extra) - 1; i >= 0; i-- {
|
||||||
|
if dns.Extra[i].Header().Rrtype == TypeOPT {
|
||||||
|
opt := dns.Extra[i].(*OPT)
|
||||||
|
dns.Extra = append(dns.Extra[:i], dns.Extra[i+1:]...)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsDomainName checks if s is a valid domain name, it returns the number of
|
// IsDomainName checks if s is a valid domain name, it returns the number of
|
||||||
// labels and true, when a domain name is valid. Note that non fully qualified
|
// labels and true, when a domain name is valid. Note that non fully qualified
|
||||||
// domain name is considered valid, in this case the last label is counted in
|
// domain name is considered valid, in this case the last label is counted in
|
||||||
|
@ -204,6 +218,11 @@ func IsDomainName(s string) (labels int, ok bool) {
|
||||||
|
|
||||||
wasDot = false
|
wasDot = false
|
||||||
case '.':
|
case '.':
|
||||||
|
if i == 0 && len(s) > 1 {
|
||||||
|
// leading dots are not legal except for the root zone
|
||||||
|
return labels, false
|
||||||
|
}
|
||||||
|
|
||||||
if wasDot {
|
if wasDot {
|
||||||
// two dots back to back is not legal
|
// two dots back to back is not legal
|
||||||
return labels, false
|
return labels, false
|
||||||
|
@ -303,6 +322,12 @@ func Fqdn(s string) string {
|
||||||
return s + "."
|
return s + "."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanonicalName returns the domain name in canonical form. A name in canonical
|
||||||
|
// form is lowercase and fully qualified. See Section 6.2 in RFC 4034.
|
||||||
|
func CanonicalName(s string) string {
|
||||||
|
return strings.ToLower(Fqdn(s))
|
||||||
|
}
|
||||||
|
|
||||||
// Copied from the official Go code.
|
// Copied from the official Go code.
|
||||||
|
|
||||||
// ReverseAddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP
|
// ReverseAddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP
|
||||||
|
@ -329,10 +354,7 @@ func ReverseAddr(addr string) (arpa string, err error) {
|
||||||
// Add it, in reverse, to the buffer
|
// Add it, in reverse, to the buffer
|
||||||
for i := len(ip) - 1; i >= 0; i-- {
|
for i := len(ip) - 1; i >= 0; i-- {
|
||||||
v := ip[i]
|
v := ip[i]
|
||||||
buf = append(buf, hexDigit[v&0xF])
|
buf = append(buf, hexDigit[v&0xF], '.', hexDigit[v>>4], '.')
|
||||||
buf = append(buf, '.')
|
|
||||||
buf = append(buf, hexDigit[v>>4])
|
|
||||||
buf = append(buf, '.')
|
|
||||||
}
|
}
|
||||||
// Append "ip6.arpa." and return (buf already has the final .)
|
// Append "ip6.arpa." and return (buf already has the final .)
|
||||||
buf = append(buf, "ip6.arpa."...)
|
buf = append(buf, "ip6.arpa."...)
|
||||||
|
@ -350,7 +372,7 @@ func (t Type) String() string {
|
||||||
// String returns the string representation for the class c.
|
// String returns the string representation for the class c.
|
||||||
func (c Class) String() string {
|
func (c Class) String() string {
|
||||||
if s, ok := ClassToString[uint16(c)]; ok {
|
if s, ok := ClassToString[uint16(c)]; ok {
|
||||||
// Only emit mnemonics when they are unambiguous, specically ANY is in both.
|
// Only emit mnemonics when they are unambiguous, specially ANY is in both.
|
||||||
if _, ok := StringToType[s]; !ok {
|
if _, ok := StringToType[s]; !ok {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
34
dns.go
34
dns.go
|
@ -1,6 +1,9 @@
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import "strconv"
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
year68 = 1 << 31 // For RFC1982 (Serial Arithmetic) calculations in 32 bits.
|
year68 = 1 << 31 // For RFC1982 (Serial Arithmetic) calculations in 32 bits.
|
||||||
|
@ -54,7 +57,7 @@ type RR interface {
|
||||||
// parse parses an RR from zone file format.
|
// parse parses an RR from zone file format.
|
||||||
//
|
//
|
||||||
// This will only be called on a new and empty RR type with only the header populated.
|
// This will only be called on a new and empty RR type with only the header populated.
|
||||||
parse(c *zlexer, origin, file string) *ParseError
|
parse(c *zlexer, origin string) *ParseError
|
||||||
|
|
||||||
// isDuplicate returns whether the two RRs are duplicates.
|
// isDuplicate returns whether the two RRs are duplicates.
|
||||||
isDuplicate(r2 RR) bool
|
isDuplicate(r2 RR) bool
|
||||||
|
@ -105,13 +108,13 @@ func (h *RR_Header) unpack(msg []byte, off int) (int, error) {
|
||||||
panic("dns: internal error: unpack should never be called on RR_Header")
|
panic("dns: internal error: unpack should never be called on RR_Header")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RR_Header) parse(c *zlexer, origin, file string) *ParseError {
|
func (h *RR_Header) parse(c *zlexer, origin string) *ParseError {
|
||||||
panic("dns: internal error: parse should never be called on RR_Header")
|
panic("dns: internal error: parse should never be called on RR_Header")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToRFC3597 converts a known RR to the unknown RR representation from RFC 3597.
|
// ToRFC3597 converts a known RR to the unknown RR representation from RFC 3597.
|
||||||
func (rr *RFC3597) ToRFC3597(r RR) error {
|
func (rr *RFC3597) ToRFC3597(r RR) error {
|
||||||
buf := make([]byte, Len(r)*2)
|
buf := make([]byte, Len(r))
|
||||||
headerEnd, off, err := packRR(r, buf, 0, compressionMap{}, false)
|
headerEnd, off, err := packRR(r, buf, 0, compressionMap{}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -126,9 +129,30 @@ func (rr *RFC3597) ToRFC3597(r RR) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = rr.unpack(buf, headerEnd)
|
_, err = rr.unpack(buf, headerEnd)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromRFC3597 converts an unknown RR representation from RFC 3597 to the known RR type.
|
||||||
|
func (rr *RFC3597) fromRFC3597(r RR) error {
|
||||||
|
hdr := r.Header()
|
||||||
|
*hdr = rr.Hdr
|
||||||
|
|
||||||
|
// Can't overflow uint16 as the length of Rdata is validated in (*RFC3597).parse.
|
||||||
|
// We can only get here when rr was constructed with that method.
|
||||||
|
hdr.Rdlength = uint16(hex.DecodedLen(len(rr.Rdata)))
|
||||||
|
|
||||||
|
if noRdata(*hdr) {
|
||||||
|
// Dynamic update.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rr.pack requires an extra allocation and a copy so we just decode Rdata
|
||||||
|
// manually, it's simpler anyway.
|
||||||
|
msg, err := hex.DecodeString(rr.Rdata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
_, err = r.unpack(msg, 0)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
172
dnssec.go
172
dnssec.go
|
@ -3,15 +3,14 @@ package dns
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/dsa"
|
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
_ "crypto/md5"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
_ "crypto/sha1"
|
_ "crypto/sha1" // need its init function
|
||||||
_ "crypto/sha256"
|
_ "crypto/sha256" // need its init function
|
||||||
_ "crypto/sha512"
|
_ "crypto/sha512" // need its init function
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
@ -19,8 +18,6 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ed25519"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNSSEC encryption algorithm codes.
|
// DNSSEC encryption algorithm codes.
|
||||||
|
@ -68,6 +65,9 @@ var AlgorithmToString = map[uint8]string{
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlgorithmToHash is a map of algorithm crypto hash IDs to crypto.Hash's.
|
// AlgorithmToHash is a map of algorithm crypto hash IDs to crypto.Hash's.
|
||||||
|
// For newer algorithm that do their own hashing (i.e. ED25519) the returned value
|
||||||
|
// is 0, implying no (external) hashing should occur. The non-exported identityHash is then
|
||||||
|
// used.
|
||||||
var AlgorithmToHash = map[uint8]crypto.Hash{
|
var AlgorithmToHash = map[uint8]crypto.Hash{
|
||||||
RSAMD5: crypto.MD5, // Deprecated in RFC 6725
|
RSAMD5: crypto.MD5, // Deprecated in RFC 6725
|
||||||
DSA: crypto.SHA1,
|
DSA: crypto.SHA1,
|
||||||
|
@ -77,7 +77,7 @@ var AlgorithmToHash = map[uint8]crypto.Hash{
|
||||||
ECDSAP256SHA256: crypto.SHA256,
|
ECDSAP256SHA256: crypto.SHA256,
|
||||||
ECDSAP384SHA384: crypto.SHA384,
|
ECDSAP384SHA384: crypto.SHA384,
|
||||||
RSASHA512: crypto.SHA512,
|
RSASHA512: crypto.SHA512,
|
||||||
ED25519: crypto.Hash(0),
|
ED25519: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNSSEC hashing algorithm codes.
|
// DNSSEC hashing algorithm codes.
|
||||||
|
@ -140,12 +140,12 @@ func (k *DNSKEY) KeyTag() uint16 {
|
||||||
var keytag int
|
var keytag int
|
||||||
switch k.Algorithm {
|
switch k.Algorithm {
|
||||||
case RSAMD5:
|
case RSAMD5:
|
||||||
// Look at the bottom two bytes of the modules, which the last
|
// This algorithm has been deprecated, but keep this key-tag calculation.
|
||||||
// item in the pubkey. We could do this faster by looking directly
|
// Look at the bottom two bytes of the modules, which the last item in the pubkey.
|
||||||
// at the base64 values. But I'm lazy.
|
// See https://www.rfc-editor.org/errata/eid193 .
|
||||||
modulus, _ := fromBase64([]byte(k.PublicKey))
|
modulus, _ := fromBase64([]byte(k.PublicKey))
|
||||||
if len(modulus) > 1 {
|
if len(modulus) > 1 {
|
||||||
x := binary.BigEndian.Uint16(modulus[len(modulus)-2:])
|
x := binary.BigEndian.Uint16(modulus[len(modulus)-3:])
|
||||||
keytag = int(x)
|
keytag = int(x)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -200,7 +200,7 @@ func (k *DNSKEY) ToDS(h uint8) *DS {
|
||||||
wire = wire[:n]
|
wire = wire[:n]
|
||||||
|
|
||||||
owner := make([]byte, 255)
|
owner := make([]byte, 255)
|
||||||
off, err1 := PackDomainName(strings.ToLower(k.Hdr.Name), owner, 0, nil, false)
|
off, err1 := PackDomainName(CanonicalName(k.Hdr.Name), owner, 0, nil, false)
|
||||||
if err1 != nil {
|
if err1 != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -285,7 +285,7 @@ func (rr *RRSIG) Sign(k crypto.Signer, rrset []RR) error {
|
||||||
sigwire.Inception = rr.Inception
|
sigwire.Inception = rr.Inception
|
||||||
sigwire.KeyTag = rr.KeyTag
|
sigwire.KeyTag = rr.KeyTag
|
||||||
// For signing, lowercase this name
|
// For signing, lowercase this name
|
||||||
sigwire.SignerName = strings.ToLower(rr.SignerName)
|
sigwire.SignerName = CanonicalName(rr.SignerName)
|
||||||
|
|
||||||
// Create the desired binary blob
|
// Create the desired binary blob
|
||||||
signdata := make([]byte, DefaultMsgSize)
|
signdata := make([]byte, DefaultMsgSize)
|
||||||
|
@ -299,39 +299,27 @@ func (rr *RRSIG) Sign(k crypto.Signer, rrset []RR) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hash, ok := AlgorithmToHash[rr.Algorithm]
|
h, cryptohash, err := hashFromAlgorithm(rr.Algorithm)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return ErrAlg
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch rr.Algorithm {
|
switch rr.Algorithm {
|
||||||
case ED25519:
|
case RSAMD5, DSA, DSANSEC3SHA1:
|
||||||
// ed25519 signs the raw message and performs hashing internally.
|
// See RFC 6944.
|
||||||
// All other supported signature schemes operate over the pre-hashed
|
return ErrAlg
|
||||||
// message, and thus ed25519 must be handled separately here.
|
|
||||||
//
|
|
||||||
// The raw message is passed directly into sign and crypto.Hash(0) is
|
|
||||||
// used to signal to the crypto.Signer that the data has not been hashed.
|
|
||||||
signature, err := sign(k, append(signdata, wire...), crypto.Hash(0), rr.Algorithm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rr.Signature = toBase64(signature)
|
|
||||||
default:
|
default:
|
||||||
h := hash.New()
|
|
||||||
h.Write(signdata)
|
h.Write(signdata)
|
||||||
h.Write(wire)
|
h.Write(wire)
|
||||||
|
|
||||||
signature, err := sign(k, h.Sum(nil), hash, rr.Algorithm)
|
signature, err := sign(k, h.Sum(nil), cryptohash, rr.Algorithm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rr.Signature = toBase64(signature)
|
rr.Signature = toBase64(signature)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sign(k crypto.Signer, hashed []byte, hash crypto.Hash, alg uint8) ([]byte, error) {
|
func sign(k crypto.Signer, hashed []byte, hash crypto.Hash, alg uint8) ([]byte, error) {
|
||||||
|
@ -341,9 +329,8 @@ func sign(k crypto.Signer, hashed []byte, hash crypto.Hash, alg uint8) ([]byte,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch alg {
|
switch alg {
|
||||||
case RSASHA1, RSASHA1NSEC3SHA1, RSASHA256, RSASHA512:
|
case RSASHA1, RSASHA1NSEC3SHA1, RSASHA256, RSASHA512, ED25519:
|
||||||
return signature, nil
|
return signature, nil
|
||||||
|
|
||||||
case ECDSAP256SHA256, ECDSAP384SHA384:
|
case ECDSAP256SHA256, ECDSAP384SHA384:
|
||||||
ecdsaSignature := &struct {
|
ecdsaSignature := &struct {
|
||||||
R, S *big.Int
|
R, S *big.Int
|
||||||
|
@ -363,25 +350,16 @@ func sign(k crypto.Signer, hashed []byte, hash crypto.Hash, alg uint8) ([]byte,
|
||||||
signature := intToBytes(ecdsaSignature.R, intlen)
|
signature := intToBytes(ecdsaSignature.R, intlen)
|
||||||
signature = append(signature, intToBytes(ecdsaSignature.S, intlen)...)
|
signature = append(signature, intToBytes(ecdsaSignature.S, intlen)...)
|
||||||
return signature, nil
|
return signature, nil
|
||||||
|
default:
|
||||||
// There is no defined interface for what a DSA backed crypto.Signer returns
|
return nil, ErrAlg
|
||||||
case DSA, DSANSEC3SHA1:
|
|
||||||
// t := divRoundUp(divRoundUp(p.PublicKey.Y.BitLen(), 8)-64, 8)
|
|
||||||
// signature := []byte{byte(t)}
|
|
||||||
// signature = append(signature, intToBytes(r1, 20)...)
|
|
||||||
// signature = append(signature, intToBytes(s1, 20)...)
|
|
||||||
// rr.Signature = signature
|
|
||||||
|
|
||||||
case ED25519:
|
|
||||||
return signature, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrAlg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify validates an RRSet with the signature and key. This is only the
|
// Verify validates an RRSet with the signature and key. This is only the
|
||||||
// cryptographic test, the signature validity period must be checked separately.
|
// cryptographic test, the signature validity period must be checked separately.
|
||||||
// This function copies the rdata of some RRs (to lowercase domain names) for the validation to work.
|
// This function copies the rdata of some RRs (to lowercase domain names) for the validation to work.
|
||||||
|
// It also checks that the Zone Key bit (RFC 4034 2.1.1) is set on the DNSKEY
|
||||||
|
// and that the Protocol field is set to 3 (RFC 4034 2.1.2).
|
||||||
func (rr *RRSIG) Verify(k *DNSKEY, rrset []RR) error {
|
func (rr *RRSIG) Verify(k *DNSKEY, rrset []RR) error {
|
||||||
// First the easy checks
|
// First the easy checks
|
||||||
if !IsRRset(rrset) {
|
if !IsRRset(rrset) {
|
||||||
|
@ -402,6 +380,12 @@ func (rr *RRSIG) Verify(k *DNSKEY, rrset []RR) error {
|
||||||
if k.Protocol != 3 {
|
if k.Protocol != 3 {
|
||||||
return ErrKey
|
return ErrKey
|
||||||
}
|
}
|
||||||
|
// RFC 4034 2.1.1 If bit 7 has value 0, then the DNSKEY record holds some
|
||||||
|
// other type of DNS public key and MUST NOT be used to verify RRSIGs that
|
||||||
|
// cover RRsets.
|
||||||
|
if k.Flags&ZONE == 0 {
|
||||||
|
return ErrKey
|
||||||
|
}
|
||||||
|
|
||||||
// IsRRset checked that we have at least one RR and that the RRs in
|
// IsRRset checked that we have at least one RR and that the RRs in
|
||||||
// the set have consistent type, class, and name. Also check that type and
|
// the set have consistent type, class, and name. Also check that type and
|
||||||
|
@ -420,7 +404,7 @@ func (rr *RRSIG) Verify(k *DNSKEY, rrset []RR) error {
|
||||||
sigwire.Expiration = rr.Expiration
|
sigwire.Expiration = rr.Expiration
|
||||||
sigwire.Inception = rr.Inception
|
sigwire.Inception = rr.Inception
|
||||||
sigwire.KeyTag = rr.KeyTag
|
sigwire.KeyTag = rr.KeyTag
|
||||||
sigwire.SignerName = strings.ToLower(rr.SignerName)
|
sigwire.SignerName = CanonicalName(rr.SignerName)
|
||||||
// Create the desired binary blob
|
// Create the desired binary blob
|
||||||
signeddata := make([]byte, DefaultMsgSize)
|
signeddata := make([]byte, DefaultMsgSize)
|
||||||
n, err := packSigWire(sigwire, signeddata)
|
n, err := packSigWire(sigwire, signeddata)
|
||||||
|
@ -439,23 +423,22 @@ func (rr *RRSIG) Verify(k *DNSKEY, rrset []RR) error {
|
||||||
// remove the domain name and assume its ours?
|
// remove the domain name and assume its ours?
|
||||||
}
|
}
|
||||||
|
|
||||||
hash, ok := AlgorithmToHash[rr.Algorithm]
|
h, cryptohash, err := hashFromAlgorithm(rr.Algorithm)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return ErrAlg
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch rr.Algorithm {
|
switch rr.Algorithm {
|
||||||
case RSASHA1, RSASHA1NSEC3SHA1, RSASHA256, RSASHA512, RSAMD5:
|
case RSASHA1, RSASHA1NSEC3SHA1, RSASHA256, RSASHA512:
|
||||||
// TODO(mg): this can be done quicker, ie. cache the pubkey data somewhere??
|
// TODO(mg): this can be done quicker, ie. cache the pubkey data somewhere??
|
||||||
pubkey := k.publicKeyRSA() // Get the key
|
pubkey := k.publicKeyRSA() // Get the key
|
||||||
if pubkey == nil {
|
if pubkey == nil {
|
||||||
return ErrKey
|
return ErrKey
|
||||||
}
|
}
|
||||||
|
|
||||||
h := hash.New()
|
|
||||||
h.Write(signeddata)
|
h.Write(signeddata)
|
||||||
h.Write(wire)
|
h.Write(wire)
|
||||||
return rsa.VerifyPKCS1v15(pubkey, hash, h.Sum(nil), sigbuf)
|
return rsa.VerifyPKCS1v15(pubkey, cryptohash, h.Sum(nil), sigbuf)
|
||||||
|
|
||||||
case ECDSAP256SHA256, ECDSAP384SHA384:
|
case ECDSAP256SHA256, ECDSAP384SHA384:
|
||||||
pubkey := k.publicKeyECDSA()
|
pubkey := k.publicKeyECDSA()
|
||||||
|
@ -467,7 +450,6 @@ func (rr *RRSIG) Verify(k *DNSKEY, rrset []RR) error {
|
||||||
r := new(big.Int).SetBytes(sigbuf[:len(sigbuf)/2])
|
r := new(big.Int).SetBytes(sigbuf[:len(sigbuf)/2])
|
||||||
s := new(big.Int).SetBytes(sigbuf[len(sigbuf)/2:])
|
s := new(big.Int).SetBytes(sigbuf[len(sigbuf)/2:])
|
||||||
|
|
||||||
h := hash.New()
|
|
||||||
h.Write(signeddata)
|
h.Write(signeddata)
|
||||||
h.Write(wire)
|
h.Write(wire)
|
||||||
if ecdsa.Verify(pubkey, h.Sum(nil), r, s) {
|
if ecdsa.Verify(pubkey, h.Sum(nil), r, s) {
|
||||||
|
@ -509,7 +491,7 @@ func (rr *RRSIG) ValidityPeriod(t time.Time) bool {
|
||||||
return ti <= utc && utc <= te
|
return ti <= utc && utc <= te
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the signatures base64 encodedig sigdata as a byte slice.
|
// Return the signatures base64 encoding sigdata as a byte slice.
|
||||||
func (rr *RRSIG) sigBuf() []byte {
|
func (rr *RRSIG) sigBuf() []byte {
|
||||||
sigbuf, err := fromBase64([]byte(rr.Signature))
|
sigbuf, err := fromBase64([]byte(rr.Signature))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -597,30 +579,6 @@ func (k *DNSKEY) publicKeyECDSA() *ecdsa.PublicKey {
|
||||||
return pubkey
|
return pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *DNSKEY) publicKeyDSA() *dsa.PublicKey {
|
|
||||||
keybuf, err := fromBase64([]byte(k.PublicKey))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(keybuf) < 22 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
t, keybuf := int(keybuf[0]), keybuf[1:]
|
|
||||||
size := 64 + t*8
|
|
||||||
q, keybuf := keybuf[:20], keybuf[20:]
|
|
||||||
if len(keybuf) != 3*size {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
p, keybuf := keybuf[:size], keybuf[size:]
|
|
||||||
g, y := keybuf[:size], keybuf[size:]
|
|
||||||
pubkey := new(dsa.PublicKey)
|
|
||||||
pubkey.Parameters.Q = new(big.Int).SetBytes(q)
|
|
||||||
pubkey.Parameters.P = new(big.Int).SetBytes(p)
|
|
||||||
pubkey.Parameters.G = new(big.Int).SetBytes(g)
|
|
||||||
pubkey.Y = new(big.Int).SetBytes(y)
|
|
||||||
return pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *DNSKEY) publicKeyED25519() ed25519.PublicKey {
|
func (k *DNSKEY) publicKeyED25519() ed25519.PublicKey {
|
||||||
keybuf, err := fromBase64([]byte(k.PublicKey))
|
keybuf, err := fromBase64([]byte(k.PublicKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -656,7 +614,7 @@ func rawSignatureData(rrset []RR, s *RRSIG) (buf []byte, err error) {
|
||||||
h.Name = "*." + strings.Join(labels[len(labels)-int(s.Labels):], ".") + "."
|
h.Name = "*." + strings.Join(labels[len(labels)-int(s.Labels):], ".") + "."
|
||||||
}
|
}
|
||||||
// RFC 4034: 6.2. Canonical RR Form. (2) - domain name to lowercase
|
// RFC 4034: 6.2. Canonical RR Form. (2) - domain name to lowercase
|
||||||
h.Name = strings.ToLower(h.Name)
|
h.Name = CanonicalName(h.Name)
|
||||||
// 6.2. Canonical RR Form. (3) - domain rdata to lowercase.
|
// 6.2. Canonical RR Form. (3) - domain rdata to lowercase.
|
||||||
// NS, MD, MF, CNAME, SOA, MB, MG, MR, PTR,
|
// NS, MD, MF, CNAME, SOA, MB, MG, MR, PTR,
|
||||||
// HINFO, MINFO, MX, RP, AFSDB, RT, SIG, PX, NXT, NAPTR, KX,
|
// HINFO, MINFO, MX, RP, AFSDB, RT, SIG, PX, NXT, NAPTR, KX,
|
||||||
|
@ -669,49 +627,49 @@ func rawSignatureData(rrset []RR, s *RRSIG) (buf []byte, err error) {
|
||||||
// conversion.
|
// conversion.
|
||||||
switch x := r1.(type) {
|
switch x := r1.(type) {
|
||||||
case *NS:
|
case *NS:
|
||||||
x.Ns = strings.ToLower(x.Ns)
|
x.Ns = CanonicalName(x.Ns)
|
||||||
case *MD:
|
case *MD:
|
||||||
x.Md = strings.ToLower(x.Md)
|
x.Md = CanonicalName(x.Md)
|
||||||
case *MF:
|
case *MF:
|
||||||
x.Mf = strings.ToLower(x.Mf)
|
x.Mf = CanonicalName(x.Mf)
|
||||||
case *CNAME:
|
case *CNAME:
|
||||||
x.Target = strings.ToLower(x.Target)
|
x.Target = CanonicalName(x.Target)
|
||||||
case *SOA:
|
case *SOA:
|
||||||
x.Ns = strings.ToLower(x.Ns)
|
x.Ns = CanonicalName(x.Ns)
|
||||||
x.Mbox = strings.ToLower(x.Mbox)
|
x.Mbox = CanonicalName(x.Mbox)
|
||||||
case *MB:
|
case *MB:
|
||||||
x.Mb = strings.ToLower(x.Mb)
|
x.Mb = CanonicalName(x.Mb)
|
||||||
case *MG:
|
case *MG:
|
||||||
x.Mg = strings.ToLower(x.Mg)
|
x.Mg = CanonicalName(x.Mg)
|
||||||
case *MR:
|
case *MR:
|
||||||
x.Mr = strings.ToLower(x.Mr)
|
x.Mr = CanonicalName(x.Mr)
|
||||||
case *PTR:
|
case *PTR:
|
||||||
x.Ptr = strings.ToLower(x.Ptr)
|
x.Ptr = CanonicalName(x.Ptr)
|
||||||
case *MINFO:
|
case *MINFO:
|
||||||
x.Rmail = strings.ToLower(x.Rmail)
|
x.Rmail = CanonicalName(x.Rmail)
|
||||||
x.Email = strings.ToLower(x.Email)
|
x.Email = CanonicalName(x.Email)
|
||||||
case *MX:
|
case *MX:
|
||||||
x.Mx = strings.ToLower(x.Mx)
|
x.Mx = CanonicalName(x.Mx)
|
||||||
case *RP:
|
case *RP:
|
||||||
x.Mbox = strings.ToLower(x.Mbox)
|
x.Mbox = CanonicalName(x.Mbox)
|
||||||
x.Txt = strings.ToLower(x.Txt)
|
x.Txt = CanonicalName(x.Txt)
|
||||||
case *AFSDB:
|
case *AFSDB:
|
||||||
x.Hostname = strings.ToLower(x.Hostname)
|
x.Hostname = CanonicalName(x.Hostname)
|
||||||
case *RT:
|
case *RT:
|
||||||
x.Host = strings.ToLower(x.Host)
|
x.Host = CanonicalName(x.Host)
|
||||||
case *SIG:
|
case *SIG:
|
||||||
x.SignerName = strings.ToLower(x.SignerName)
|
x.SignerName = CanonicalName(x.SignerName)
|
||||||
case *PX:
|
case *PX:
|
||||||
x.Map822 = strings.ToLower(x.Map822)
|
x.Map822 = CanonicalName(x.Map822)
|
||||||
x.Mapx400 = strings.ToLower(x.Mapx400)
|
x.Mapx400 = CanonicalName(x.Mapx400)
|
||||||
case *NAPTR:
|
case *NAPTR:
|
||||||
x.Replacement = strings.ToLower(x.Replacement)
|
x.Replacement = CanonicalName(x.Replacement)
|
||||||
case *KX:
|
case *KX:
|
||||||
x.Exchanger = strings.ToLower(x.Exchanger)
|
x.Exchanger = CanonicalName(x.Exchanger)
|
||||||
case *SRV:
|
case *SRV:
|
||||||
x.Target = strings.ToLower(x.Target)
|
x.Target = CanonicalName(x.Target)
|
||||||
case *DNAME:
|
case *DNAME:
|
||||||
x.Target = strings.ToLower(x.Target)
|
x.Target = CanonicalName(x.Target)
|
||||||
}
|
}
|
||||||
// 6.2. Canonical RR Form. (5) - origTTL
|
// 6.2. Canonical RR Form. (5) - origTTL
|
||||||
wire := make([]byte, Len(r1)+1) // +1 to be safe(r)
|
wire := make([]byte, Len(r1)+1) // +1 to be safe(r)
|
||||||
|
|
|
@ -2,14 +2,12 @@ package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/dsa"
|
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"golang.org/x/crypto/ed25519"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Generate generates a DNSKEY of the given bit size.
|
// Generate generates a DNSKEY of the given bit size.
|
||||||
|
@ -20,11 +18,7 @@ import (
|
||||||
// bits should be set to the size of the algorithm.
|
// bits should be set to the size of the algorithm.
|
||||||
func (k *DNSKEY) Generate(bits int) (crypto.PrivateKey, error) {
|
func (k *DNSKEY) Generate(bits int) (crypto.PrivateKey, error) {
|
||||||
switch k.Algorithm {
|
switch k.Algorithm {
|
||||||
case DSA, DSANSEC3SHA1:
|
case RSASHA1, RSASHA256, RSASHA1NSEC3SHA1:
|
||||||
if bits != 1024 {
|
|
||||||
return nil, ErrKeySize
|
|
||||||
}
|
|
||||||
case RSAMD5, RSASHA1, RSASHA256, RSASHA1NSEC3SHA1:
|
|
||||||
if bits < 512 || bits > 4096 {
|
if bits < 512 || bits > 4096 {
|
||||||
return nil, ErrKeySize
|
return nil, ErrKeySize
|
||||||
}
|
}
|
||||||
|
@ -44,23 +38,12 @@ func (k *DNSKEY) Generate(bits int) (crypto.PrivateKey, error) {
|
||||||
if bits != 256 {
|
if bits != 256 {
|
||||||
return nil, ErrKeySize
|
return nil, ErrKeySize
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
return nil, ErrAlg
|
||||||
}
|
}
|
||||||
|
|
||||||
switch k.Algorithm {
|
switch k.Algorithm {
|
||||||
case DSA, DSANSEC3SHA1:
|
case RSASHA1, RSASHA256, RSASHA512, RSASHA1NSEC3SHA1:
|
||||||
params := new(dsa.Parameters)
|
|
||||||
if err := dsa.GenerateParameters(params, rand.Reader, dsa.L1024N160); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
priv := new(dsa.PrivateKey)
|
|
||||||
priv.PublicKey.Parameters = *params
|
|
||||||
err := dsa.GenerateKey(priv, rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
k.setPublicKeyDSA(params.Q, params.P, params.G, priv.PublicKey.Y)
|
|
||||||
return priv, nil
|
|
||||||
case RSAMD5, RSASHA1, RSASHA256, RSASHA512, RSASHA1NSEC3SHA1:
|
|
||||||
priv, err := rsa.GenerateKey(rand.Reader, bits)
|
priv, err := rsa.GenerateKey(rand.Reader, bits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -120,16 +103,6 @@ func (k *DNSKEY) setPublicKeyECDSA(_X, _Y *big.Int) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the public key for DSA
|
|
||||||
func (k *DNSKEY) setPublicKeyDSA(_Q, _P, _G, _Y *big.Int) bool {
|
|
||||||
if _Q == nil || _P == nil || _G == nil || _Y == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
buf := dsaToBuf(_Q, _P, _G, _Y)
|
|
||||||
k.PublicKey = toBase64(buf)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the public key for Ed25519
|
// Set the public key for Ed25519
|
||||||
func (k *DNSKEY) setPublicKeyED25519(_K ed25519.PublicKey) bool {
|
func (k *DNSKEY) setPublicKeyED25519(_K ed25519.PublicKey) bool {
|
||||||
if _K == nil {
|
if _K == nil {
|
||||||
|
@ -164,15 +137,3 @@ func curveToBuf(_X, _Y *big.Int, intlen int) []byte {
|
||||||
buf = append(buf, intToBytes(_Y, intlen)...)
|
buf = append(buf, intToBytes(_Y, intlen)...)
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the public key for X and Y for Curve. The two
|
|
||||||
// values are just concatenated.
|
|
||||||
func dsaToBuf(_Q, _P, _G, _Y *big.Int) []byte {
|
|
||||||
t := divRoundUp(divRoundUp(_G.BitLen(), 8)-64, 8)
|
|
||||||
buf := []byte{byte(t)}
|
|
||||||
buf = append(buf, intToBytes(_Q, 20)...)
|
|
||||||
buf = append(buf, intToBytes(_P, 64+t*8)...)
|
|
||||||
buf = append(buf, intToBytes(_G, 64+t*8)...)
|
|
||||||
buf = append(buf, intToBytes(_Y, 64+t*8)...)
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,15 +3,13 @@ package dns
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/dsa"
|
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/crypto/ed25519"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewPrivateKey returns a PrivateKey by parsing the string s.
|
// NewPrivateKey returns a PrivateKey by parsing the string s.
|
||||||
|
@ -44,26 +42,7 @@ func (k *DNSKEY) ReadPrivateKey(q io.Reader, file string) (crypto.PrivateKey, er
|
||||||
return nil, ErrPrivKey
|
return nil, ErrPrivKey
|
||||||
}
|
}
|
||||||
switch uint8(algo) {
|
switch uint8(algo) {
|
||||||
case DSA:
|
case RSASHA1, RSASHA1NSEC3SHA1, RSASHA256, RSASHA512:
|
||||||
priv, err := readPrivateKeyDSA(m)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pub := k.publicKeyDSA()
|
|
||||||
if pub == nil {
|
|
||||||
return nil, ErrKey
|
|
||||||
}
|
|
||||||
priv.PublicKey = *pub
|
|
||||||
return priv, nil
|
|
||||||
case RSAMD5:
|
|
||||||
fallthrough
|
|
||||||
case RSASHA1:
|
|
||||||
fallthrough
|
|
||||||
case RSASHA1NSEC3SHA1:
|
|
||||||
fallthrough
|
|
||||||
case RSASHA256:
|
|
||||||
fallthrough
|
|
||||||
case RSASHA512:
|
|
||||||
priv, err := readPrivateKeyRSA(m)
|
priv, err := readPrivateKeyRSA(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -74,11 +53,7 @@ func (k *DNSKEY) ReadPrivateKey(q io.Reader, file string) (crypto.PrivateKey, er
|
||||||
}
|
}
|
||||||
priv.PublicKey = *pub
|
priv.PublicKey = *pub
|
||||||
return priv, nil
|
return priv, nil
|
||||||
case ECCGOST:
|
case ECDSAP256SHA256, ECDSAP384SHA384:
|
||||||
return nil, ErrPrivKey
|
|
||||||
case ECDSAP256SHA256:
|
|
||||||
fallthrough
|
|
||||||
case ECDSAP384SHA384:
|
|
||||||
priv, err := readPrivateKeyECDSA(m)
|
priv, err := readPrivateKeyECDSA(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -92,7 +67,7 @@ func (k *DNSKEY) ReadPrivateKey(q io.Reader, file string) (crypto.PrivateKey, er
|
||||||
case ED25519:
|
case ED25519:
|
||||||
return readPrivateKeyED25519(m)
|
return readPrivateKeyED25519(m)
|
||||||
default:
|
default:
|
||||||
return nil, ErrPrivKey
|
return nil, ErrAlg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,24 +104,6 @@ func readPrivateKeyRSA(m map[string]string) (*rsa.PrivateKey, error) {
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readPrivateKeyDSA(m map[string]string) (*dsa.PrivateKey, error) {
|
|
||||||
p := new(dsa.PrivateKey)
|
|
||||||
p.X = new(big.Int)
|
|
||||||
for k, v := range m {
|
|
||||||
switch k {
|
|
||||||
case "private_value(x)":
|
|
||||||
v1, err := fromBase64([]byte(v))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.X.SetBytes(v1)
|
|
||||||
case "created", "publish", "activate":
|
|
||||||
/* not used in Go (yet) */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readPrivateKeyECDSA(m map[string]string) (*ecdsa.PrivateKey, error) {
|
func readPrivateKeyECDSA(m map[string]string) (*ecdsa.PrivateKey, error) {
|
||||||
p := new(ecdsa.PrivateKey)
|
p := new(ecdsa.PrivateKey)
|
||||||
p.D = new(big.Int)
|
p.D = new(big.Int)
|
||||||
|
|
|
@ -2,13 +2,11 @@ package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/dsa"
|
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"golang.org/x/crypto/ed25519"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const format = "Private-key-format: v1.3\n"
|
const format = "Private-key-format: v1.3\n"
|
||||||
|
@ -17,8 +15,8 @@ var bigIntOne = big.NewInt(1)
|
||||||
|
|
||||||
// PrivateKeyString converts a PrivateKey to a string. This string has the same
|
// PrivateKeyString converts a PrivateKey to a string. This string has the same
|
||||||
// format as the private-key-file of BIND9 (Private-key-format: v1.3).
|
// format as the private-key-file of BIND9 (Private-key-format: v1.3).
|
||||||
// It needs some info from the key (the algorithm), so its a method of the DNSKEY
|
// It needs some info from the key (the algorithm), so its a method of the DNSKEY.
|
||||||
// It supports rsa.PrivateKey, ecdsa.PrivateKey and dsa.PrivateKey
|
// It supports *rsa.PrivateKey, *ecdsa.PrivateKey and ed25519.PrivateKey.
|
||||||
func (r *DNSKEY) PrivateKeyString(p crypto.PrivateKey) string {
|
func (r *DNSKEY) PrivateKeyString(p crypto.PrivateKey) string {
|
||||||
algorithm := strconv.Itoa(int(r.Algorithm))
|
algorithm := strconv.Itoa(int(r.Algorithm))
|
||||||
algorithm += " (" + AlgorithmToString[r.Algorithm] + ")"
|
algorithm += " (" + AlgorithmToString[r.Algorithm] + ")"
|
||||||
|
@ -67,21 +65,6 @@ func (r *DNSKEY) PrivateKeyString(p crypto.PrivateKey) string {
|
||||||
"Algorithm: " + algorithm + "\n" +
|
"Algorithm: " + algorithm + "\n" +
|
||||||
"PrivateKey: " + private + "\n"
|
"PrivateKey: " + private + "\n"
|
||||||
|
|
||||||
case *dsa.PrivateKey:
|
|
||||||
T := divRoundUp(divRoundUp(p.PublicKey.Parameters.G.BitLen(), 8)-64, 8)
|
|
||||||
prime := toBase64(intToBytes(p.PublicKey.Parameters.P, 64+T*8))
|
|
||||||
subprime := toBase64(intToBytes(p.PublicKey.Parameters.Q, 20))
|
|
||||||
base := toBase64(intToBytes(p.PublicKey.Parameters.G, 64+T*8))
|
|
||||||
priv := toBase64(intToBytes(p.X, 20))
|
|
||||||
pub := toBase64(intToBytes(p.PublicKey.Y, 64+T*8))
|
|
||||||
return format +
|
|
||||||
"Algorithm: " + algorithm + "\n" +
|
|
||||||
"Prime(p): " + prime + "\n" +
|
|
||||||
"Subprime(q): " + subprime + "\n" +
|
|
||||||
"Base(g): " + base + "\n" +
|
|
||||||
"Private_value(x): " + priv + "\n" +
|
|
||||||
"Public_value(y): " + pub + "\n"
|
|
||||||
|
|
||||||
case ed25519.PrivateKey:
|
case ed25519.PrivateKey:
|
||||||
private := toBase64(p.Seed())
|
private := toBase64(p.Seed())
|
||||||
return format +
|
return format +
|
||||||
|
|
|
@ -3,13 +3,12 @@ package dns
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ed25519"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getSoa() *SOA {
|
func getSoa() *SOA {
|
||||||
|
@ -168,7 +167,7 @@ func Test65534(t *testing.T) {
|
||||||
key.Flags = 256
|
key.Flags = 256
|
||||||
key.Protocol = 3
|
key.Protocol = 3
|
||||||
key.Algorithm = RSASHA256
|
key.Algorithm = RSASHA256
|
||||||
privkey, _ := key.Generate(1024)
|
privkey, _ := key.Generate(512)
|
||||||
|
|
||||||
sig := new(RRSIG)
|
sig := new(RRSIG)
|
||||||
sig.Hdr = RR_Header{"miek.nl.", TypeRRSIG, ClassINET, 14400, 0}
|
sig.Hdr = RR_Header{"miek.nl.", TypeRRSIG, ClassINET, 14400, 0}
|
||||||
|
@ -251,7 +250,7 @@ func TestKeyRSA(t *testing.T) {
|
||||||
key.Flags = 256
|
key.Flags = 256
|
||||||
key.Protocol = 3
|
key.Protocol = 3
|
||||||
key.Algorithm = RSASHA256
|
key.Algorithm = RSASHA256
|
||||||
priv, _ := key.Generate(2048)
|
priv, _ := key.Generate(512)
|
||||||
|
|
||||||
soa := new(SOA)
|
soa := new(SOA)
|
||||||
soa.Hdr = RR_Header{"miek.nl.", TypeSOA, ClassINET, 14400, 0}
|
soa.Hdr = RR_Header{"miek.nl.", TypeSOA, ClassINET, 14400, 0}
|
||||||
|
@ -325,7 +324,7 @@ Activate: 20110302104537`
|
||||||
}
|
}
|
||||||
switch priv := p.(type) {
|
switch priv := p.(type) {
|
||||||
case *rsa.PrivateKey:
|
case *rsa.PrivateKey:
|
||||||
if 65537 != priv.PublicKey.E {
|
if priv.PublicKey.E != 65537 {
|
||||||
t.Error("exponenent should be 65537")
|
t.Error("exponenent should be 65537")
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -856,3 +855,16 @@ func TestParseKeyReadError(t *testing.T) {
|
||||||
t.Errorf("expected a nil map, but got %v", m)
|
t.Errorf("expected a nil map, but got %v", m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRSAMD5KeyTag(t *testing.T) {
|
||||||
|
rr1, _ := NewRR("test. IN DNSKEY 257 3 1 AwEAAcntNdoMnY8pvyPcpDTAaiqHyAhf53XUBANq166won/fjBFvmuzhTuP5r4el/pV0tzEBL73zpoU48BqF66uiL+qRijXCySJiaBUvLNll5rpwuduAOoVpmwOmkC4fV6izHOAx/Uy8c+pYP0YR8+1P7GuTFxgnMmt9sUGtoe+la0X/ ;{id = 27461 (ksk), size = 1024b}")
|
||||||
|
rr2, _ := NewRR("test. IN DNSKEY 257 3 1 AwEAAf0bKO/m45ylk5BlSLmQHQRBLx1m/ZUXvyPFB387bJXxnTk6so3ub97L1RQ+8bOoiRh3Qm5EaYihjco7J8b/W5WbS3tVsE79nY584RfTKT2zcZ9AoFP2XLChXxPIf/6l0H9n6sH0aBjsG8vabEIp8e06INM3CXVPiMRPPeGNa0Ub ;{id = 27461 (ksk), size = 1024b}")
|
||||||
|
|
||||||
|
exp := uint16(27461)
|
||||||
|
if x := rr1.(*DNSKEY).KeyTag(); x != exp {
|
||||||
|
t.Errorf("expected %d, got %d, as keytag for rr1", exp, x)
|
||||||
|
}
|
||||||
|
if x := rr2.(*DNSKEY).KeyTag(); x != exp { // yes, same key tag
|
||||||
|
t.Errorf("expected %d, got %d, as keytag for rr2", exp, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -30,10 +30,10 @@ func AddOrigin(s, origin string) string {
|
||||||
if dns.IsFqdn(s) {
|
if dns.IsFqdn(s) {
|
||||||
return s // s is already a FQDN, no need to mess with it.
|
return s // s is already a FQDN, no need to mess with it.
|
||||||
}
|
}
|
||||||
if len(origin) == 0 {
|
if origin == "" {
|
||||||
return s // Nothing to append.
|
return s // Nothing to append.
|
||||||
}
|
}
|
||||||
if s == "@" || len(s) == 0 {
|
if s == "@" || s == "" {
|
||||||
return origin // Expand apex.
|
return origin // Expand apex.
|
||||||
}
|
}
|
||||||
if origin == "." {
|
if origin == "." {
|
||||||
|
@ -50,7 +50,7 @@ func TrimDomainName(s, origin string) string {
|
||||||
// If the return value ends in a ".", the domain was not the suffix.
|
// If the return value ends in a ".", the domain was not the suffix.
|
||||||
// origin can end in "." or not. Either way the results should be the same.
|
// origin can end in "." or not. Either way the results should be the same.
|
||||||
|
|
||||||
if len(s) == 0 {
|
if s == "" {
|
||||||
return "@"
|
return "@"
|
||||||
}
|
}
|
||||||
// Someone is using TrimDomainName(s, ".") to remove a dot if it exists.
|
// Someone is using TrimDomainName(s, ".") to remove a dot if it exists.
|
||||||
|
|
|
@ -63,7 +63,7 @@ func TestTrimDomainName(t *testing.T) {
|
||||||
// Paranoid tests.
|
// Paranoid tests.
|
||||||
// These test shouldn't be needed but I was weary of off-by-one errors.
|
// These test shouldn't be needed but I was weary of off-by-one errors.
|
||||||
// In theory, these can't happen because there are no single-letter TLDs,
|
// In theory, these can't happen because there are no single-letter TLDs,
|
||||||
// but it is good to exercize the code this way.
|
// but it is good to exercise the code this way.
|
||||||
tests := []struct{ experiment, expected string }{
|
tests := []struct{ experiment, expected string }{
|
||||||
{"", "@"},
|
{"", "@"},
|
||||||
{".", "."},
|
{".", "."},
|
||||||
|
|
133
doc.go
133
doc.go
|
@ -13,28 +13,28 @@ names in a message will result in a packing failure.
|
||||||
Resource records are native types. They are not stored in wire format. Basic
|
Resource records are native types. They are not stored in wire format. Basic
|
||||||
usage pattern for creating a new resource record:
|
usage pattern for creating a new resource record:
|
||||||
|
|
||||||
r := new(dns.MX)
|
r := new(dns.MX)
|
||||||
r.Hdr = dns.RR_Header{Name: "miek.nl.", Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 3600}
|
r.Hdr = dns.RR_Header{Name: "miek.nl.", Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 3600}
|
||||||
r.Preference = 10
|
r.Preference = 10
|
||||||
r.Mx = "mx.miek.nl."
|
r.Mx = "mx.miek.nl."
|
||||||
|
|
||||||
Or directly from a string:
|
Or directly from a string:
|
||||||
|
|
||||||
mx, err := dns.NewRR("miek.nl. 3600 IN MX 10 mx.miek.nl.")
|
mx, err := dns.NewRR("miek.nl. 3600 IN MX 10 mx.miek.nl.")
|
||||||
|
|
||||||
Or when the default origin (.) and TTL (3600) and class (IN) suit you:
|
Or when the default origin (.) and TTL (3600) and class (IN) suit you:
|
||||||
|
|
||||||
mx, err := dns.NewRR("miek.nl MX 10 mx.miek.nl")
|
mx, err := dns.NewRR("miek.nl MX 10 mx.miek.nl")
|
||||||
|
|
||||||
Or even:
|
Or even:
|
||||||
|
|
||||||
mx, err := dns.NewRR("$ORIGIN nl.\nmiek 1H IN MX 10 mx.miek")
|
mx, err := dns.NewRR("$ORIGIN nl.\nmiek 1H IN MX 10 mx.miek")
|
||||||
|
|
||||||
In the DNS messages are exchanged, these messages contain resource records
|
In the DNS messages are exchanged, these messages contain resource records
|
||||||
(sets). Use pattern for creating a message:
|
(sets). Use pattern for creating a message:
|
||||||
|
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetQuestion("miek.nl.", dns.TypeMX)
|
m.SetQuestion("miek.nl.", dns.TypeMX)
|
||||||
|
|
||||||
Or when not certain if the domain name is fully qualified:
|
Or when not certain if the domain name is fully qualified:
|
||||||
|
|
||||||
|
@ -45,17 +45,17 @@ records for the miek.nl. zone.
|
||||||
|
|
||||||
The following is slightly more verbose, but more flexible:
|
The following is slightly more verbose, but more flexible:
|
||||||
|
|
||||||
m1 := new(dns.Msg)
|
m1 := new(dns.Msg)
|
||||||
m1.Id = dns.Id()
|
m1.Id = dns.Id()
|
||||||
m1.RecursionDesired = true
|
m1.RecursionDesired = true
|
||||||
m1.Question = make([]dns.Question, 1)
|
m1.Question = make([]dns.Question, 1)
|
||||||
m1.Question[0] = dns.Question{"miek.nl.", dns.TypeMX, dns.ClassINET}
|
m1.Question[0] = dns.Question{"miek.nl.", dns.TypeMX, dns.ClassINET}
|
||||||
|
|
||||||
After creating a message it can be sent. Basic use pattern for synchronous
|
After creating a message it can be sent. Basic use pattern for synchronous
|
||||||
querying the DNS at a server configured on 127.0.0.1 and port 53:
|
querying the DNS at a server configured on 127.0.0.1 and port 53:
|
||||||
|
|
||||||
c := new(dns.Client)
|
c := new(dns.Client)
|
||||||
in, rtt, err := c.Exchange(m1, "127.0.0.1:53")
|
in, rtt, err := c.Exchange(m1, "127.0.0.1:53")
|
||||||
|
|
||||||
Suppressing multiple outstanding queries (with the same question, type and
|
Suppressing multiple outstanding queries (with the same question, type and
|
||||||
class) is as easy as setting:
|
class) is as easy as setting:
|
||||||
|
@ -72,7 +72,7 @@ and port to use for the connection:
|
||||||
Port: 12345,
|
Port: 12345,
|
||||||
Zone: "",
|
Zone: "",
|
||||||
}
|
}
|
||||||
c.Dialer := &net.Dialer{
|
c.Dialer = &net.Dialer{
|
||||||
Timeout: 200 * time.Millisecond,
|
Timeout: 200 * time.Millisecond,
|
||||||
LocalAddr: &laddr,
|
LocalAddr: &laddr,
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ with:
|
||||||
|
|
||||||
in, err := dns.Exchange(m1, "127.0.0.1:53")
|
in, err := dns.Exchange(m1, "127.0.0.1:53")
|
||||||
|
|
||||||
When this functions returns you will get dns message. A dns message consists
|
When this functions returns you will get DNS message. A DNS message consists
|
||||||
out of four sections.
|
out of four sections.
|
||||||
The question section: in.Question, the answer section: in.Answer,
|
The question section: in.Question, the answer section: in.Answer,
|
||||||
the authority section: in.Ns and the additional section: in.Extra.
|
the authority section: in.Ns and the additional section: in.Extra.
|
||||||
|
@ -96,7 +96,7 @@ the Answer section:
|
||||||
// do something with t.Txt
|
// do something with t.Txt
|
||||||
}
|
}
|
||||||
|
|
||||||
Domain Name and TXT Character String Representations
|
# Domain Name and TXT Character String Representations
|
||||||
|
|
||||||
Both domain names and TXT character strings are converted to presentation form
|
Both domain names and TXT character strings are converted to presentation form
|
||||||
both when unpacked and when converted to strings.
|
both when unpacked and when converted to strings.
|
||||||
|
@ -108,7 +108,7 @@ be escaped. Bytes below 32 and above 127 will be converted to \DDD form.
|
||||||
For domain names, in addition to the above rules brackets, periods, spaces,
|
For domain names, in addition to the above rules brackets, periods, spaces,
|
||||||
semicolons and the at symbol are escaped.
|
semicolons and the at symbol are escaped.
|
||||||
|
|
||||||
DNSSEC
|
# DNSSEC
|
||||||
|
|
||||||
DNSSEC (DNS Security Extension) adds a layer of security to the DNS. It uses
|
DNSSEC (DNS Security Extension) adds a layer of security to the DNS. It uses
|
||||||
public key cryptography to sign resource records. The public keys are stored in
|
public key cryptography to sign resource records. The public keys are stored in
|
||||||
|
@ -117,12 +117,12 @@ DNSKEY records and the signatures in RRSIG records.
|
||||||
Requesting DNSSEC information for a zone is done by adding the DO (DNSSEC OK)
|
Requesting DNSSEC information for a zone is done by adding the DO (DNSSEC OK)
|
||||||
bit to a request.
|
bit to a request.
|
||||||
|
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetEdns0(4096, true)
|
m.SetEdns0(4096, true)
|
||||||
|
|
||||||
Signature generation, signature verification and key generation are all supported.
|
Signature generation, signature verification and key generation are all supported.
|
||||||
|
|
||||||
DYNAMIC UPDATES
|
# DYNAMIC UPDATES
|
||||||
|
|
||||||
Dynamic updates reuses the DNS message format, but renames three of the
|
Dynamic updates reuses the DNS message format, but renames three of the
|
||||||
sections. Question is Zone, Answer is Prerequisite, Authority is Update, only
|
sections. Question is Zone, Answer is Prerequisite, Authority is Update, only
|
||||||
|
@ -133,33 +133,33 @@ certain resource records or names in a zone to specify if resource records
|
||||||
should be added or removed. The table from RFC 2136 supplemented with the Go
|
should be added or removed. The table from RFC 2136 supplemented with the Go
|
||||||
DNS function shows which functions exist to specify the prerequisites.
|
DNS function shows which functions exist to specify the prerequisites.
|
||||||
|
|
||||||
3.2.4 - Table Of Metavalues Used In Prerequisite Section
|
3.2.4 - Table Of Metavalues Used In Prerequisite Section
|
||||||
|
|
||||||
CLASS TYPE RDATA Meaning Function
|
CLASS TYPE RDATA Meaning Function
|
||||||
--------------------------------------------------------------
|
--------------------------------------------------------------
|
||||||
ANY ANY empty Name is in use dns.NameUsed
|
ANY ANY empty Name is in use dns.NameUsed
|
||||||
ANY rrset empty RRset exists (value indep) dns.RRsetUsed
|
ANY rrset empty RRset exists (value indep) dns.RRsetUsed
|
||||||
NONE ANY empty Name is not in use dns.NameNotUsed
|
NONE ANY empty Name is not in use dns.NameNotUsed
|
||||||
NONE rrset empty RRset does not exist dns.RRsetNotUsed
|
NONE rrset empty RRset does not exist dns.RRsetNotUsed
|
||||||
zone rrset rr RRset exists (value dep) dns.Used
|
zone rrset rr RRset exists (value dep) dns.Used
|
||||||
|
|
||||||
The prerequisite section can also be left empty. If you have decided on the
|
The prerequisite section can also be left empty. If you have decided on the
|
||||||
prerequisites you can tell what RRs should be added or deleted. The next table
|
prerequisites you can tell what RRs should be added or deleted. The next table
|
||||||
shows the options you have and what functions to call.
|
shows the options you have and what functions to call.
|
||||||
|
|
||||||
3.4.2.6 - Table Of Metavalues Used In Update Section
|
3.4.2.6 - Table Of Metavalues Used In Update Section
|
||||||
|
|
||||||
CLASS TYPE RDATA Meaning Function
|
CLASS TYPE RDATA Meaning Function
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
ANY ANY empty Delete all RRsets from name dns.RemoveName
|
ANY ANY empty Delete all RRsets from name dns.RemoveName
|
||||||
ANY rrset empty Delete an RRset dns.RemoveRRset
|
ANY rrset empty Delete an RRset dns.RemoveRRset
|
||||||
NONE rrset rr Delete an RR from RRset dns.Remove
|
NONE rrset rr Delete an RR from RRset dns.Remove
|
||||||
zone rrset rr Add to an RRset dns.Insert
|
zone rrset rr Add to an RRset dns.Insert
|
||||||
|
|
||||||
TRANSACTION SIGNATURE
|
# TRANSACTION SIGNATURE
|
||||||
|
|
||||||
An TSIG or transaction signature adds a HMAC TSIG record to each message sent.
|
An TSIG or transaction signature adds a HMAC TSIG record to each message sent.
|
||||||
The supported algorithms include: HmacMD5, HmacSHA1, HmacSHA256 and HmacSHA512.
|
The supported algorithms include: HmacSHA1, HmacSHA256 and HmacSHA512.
|
||||||
|
|
||||||
Basic use pattern when querying with a TSIG name "axfr." (note that these key names
|
Basic use pattern when querying with a TSIG name "axfr." (note that these key names
|
||||||
must be fully qualified - as they are domain names) and the base64 secret
|
must be fully qualified - as they are domain names) and the base64 secret
|
||||||
|
@ -174,7 +174,7 @@ changes to the RRset after calling SetTsig() the signature will be incorrect.
|
||||||
c.TsigSecret = map[string]string{"axfr.": "so6ZGir4GPAqINNh9U5c3A=="}
|
c.TsigSecret = map[string]string{"axfr.": "so6ZGir4GPAqINNh9U5c3A=="}
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetQuestion("miek.nl.", dns.TypeMX)
|
m.SetQuestion("miek.nl.", dns.TypeMX)
|
||||||
m.SetTsig("axfr.", dns.HmacMD5, 300, time.Now().Unix())
|
m.SetTsig("axfr.", dns.HmacSHA256, 300, time.Now().Unix())
|
||||||
...
|
...
|
||||||
// When sending the TSIG RR is calculated and filled in before sending
|
// When sending the TSIG RR is calculated and filled in before sending
|
||||||
|
|
||||||
|
@ -187,13 +187,37 @@ request an AXFR for miek.nl. with TSIG key named "axfr." and secret
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
t.TsigSecret = map[string]string{"axfr.": "so6ZGir4GPAqINNh9U5c3A=="}
|
t.TsigSecret = map[string]string{"axfr.": "so6ZGir4GPAqINNh9U5c3A=="}
|
||||||
m.SetAxfr("miek.nl.")
|
m.SetAxfr("miek.nl.")
|
||||||
m.SetTsig("axfr.", dns.HmacMD5, 300, time.Now().Unix())
|
m.SetTsig("axfr.", dns.HmacSHA256, 300, time.Now().Unix())
|
||||||
c, err := t.In(m, "176.58.119.54:53")
|
c, err := t.In(m, "176.58.119.54:53")
|
||||||
for r := range c { ... }
|
for r := range c { ... }
|
||||||
|
|
||||||
You can now read the records from the transfer as they come in. Each envelope
|
You can now read the records from the transfer as they come in. Each envelope
|
||||||
is checked with TSIG. If something is not correct an error is returned.
|
is checked with TSIG. If something is not correct an error is returned.
|
||||||
|
|
||||||
|
A custom TSIG implementation can be used. This requires additional code to
|
||||||
|
perform any session establishment and signature generation/verification. The
|
||||||
|
client must be configured with an implementation of the TsigProvider interface:
|
||||||
|
|
||||||
|
type Provider struct{}
|
||||||
|
|
||||||
|
func (*Provider) Generate(msg []byte, tsig *dns.TSIG) ([]byte, error) {
|
||||||
|
// Use tsig.Hdr.Name and tsig.Algorithm in your code to
|
||||||
|
// generate the MAC using msg as the payload.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Provider) Verify(msg []byte, tsig *dns.TSIG) error {
|
||||||
|
// Use tsig.Hdr.Name and tsig.Algorithm in your code to verify
|
||||||
|
// that msg matches the value in tsig.MAC.
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(dns.Client)
|
||||||
|
c.TsigProvider = new(Provider)
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion("miek.nl.", dns.TypeMX)
|
||||||
|
m.SetTsig(keyname, dns.HmacSHA256, 300, time.Now().Unix())
|
||||||
|
...
|
||||||
|
// TSIG RR is calculated by calling your Generate method
|
||||||
|
|
||||||
Basic use pattern validating and replying to a message that has TSIG set.
|
Basic use pattern validating and replying to a message that has TSIG set.
|
||||||
|
|
||||||
server := &dns.Server{Addr: ":53", Net: "udp"}
|
server := &dns.Server{Addr: ":53", Net: "udp"}
|
||||||
|
@ -207,27 +231,27 @@ Basic use pattern validating and replying to a message that has TSIG set.
|
||||||
if r.IsTsig() != nil {
|
if r.IsTsig() != nil {
|
||||||
if w.TsigStatus() == nil {
|
if w.TsigStatus() == nil {
|
||||||
// *Msg r has an TSIG record and it was validated
|
// *Msg r has an TSIG record and it was validated
|
||||||
m.SetTsig("axfr.", dns.HmacMD5, 300, time.Now().Unix())
|
m.SetTsig("axfr.", dns.HmacSHA256, 300, time.Now().Unix())
|
||||||
} else {
|
} else {
|
||||||
// *Msg r has an TSIG records and it was not valided
|
// *Msg r has an TSIG records and it was not validated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.WriteMsg(m)
|
w.WriteMsg(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
PRIVATE RRS
|
# PRIVATE RRS
|
||||||
|
|
||||||
RFC 6895 sets aside a range of type codes for private use. This range is 65,280
|
RFC 6895 sets aside a range of type codes for private use. This range is 65,280
|
||||||
- 65,534 (0xFF00 - 0xFFFE). When experimenting with new Resource Records these
|
- 65,534 (0xFF00 - 0xFFFE). When experimenting with new Resource Records these
|
||||||
can be used, before requesting an official type code from IANA.
|
can be used, before requesting an official type code from IANA.
|
||||||
|
|
||||||
See https://miek.nl/2014/September/21/idn-and-private-rr-in-go-dns/ for more
|
See https://miek.nl/2014/september/21/idn-and-private-rr-in-go-dns/ for more
|
||||||
information.
|
information.
|
||||||
|
|
||||||
EDNS0
|
# EDNS0
|
||||||
|
|
||||||
EDNS0 is an extension mechanism for the DNS defined in RFC 2671 and updated by
|
EDNS0 is an extension mechanism for the DNS defined in RFC 2671 and updated by
|
||||||
RFC 6891. It defines an new RR type, the OPT RR, which is then completely
|
RFC 6891. It defines a new RR type, the OPT RR, which is then completely
|
||||||
abused.
|
abused.
|
||||||
|
|
||||||
Basic use pattern for creating an (empty) OPT RR:
|
Basic use pattern for creating an (empty) OPT RR:
|
||||||
|
@ -238,9 +262,8 @@ Basic use pattern for creating an (empty) OPT RR:
|
||||||
|
|
||||||
The rdata of an OPT RR consists out of a slice of EDNS0 (RFC 6891) interfaces.
|
The rdata of an OPT RR consists out of a slice of EDNS0 (RFC 6891) interfaces.
|
||||||
Currently only a few have been standardized: EDNS0_NSID (RFC 5001) and
|
Currently only a few have been standardized: EDNS0_NSID (RFC 5001) and
|
||||||
EDNS0_SUBNET (draft-vandergaast-edns-client-subnet-02). Note that these options
|
EDNS0_SUBNET (RFC 7871). Note that these options may be combined in an OPT RR.
|
||||||
may be combined in an OPT RR. Basic use pattern for a server to check if (and
|
Basic use pattern for a server to check if (and which) options are set:
|
||||||
which) options are set:
|
|
||||||
|
|
||||||
// o is a dns.OPT
|
// o is a dns.OPT
|
||||||
for _, s := range o.Option {
|
for _, s := range o.Option {
|
||||||
|
@ -256,12 +279,12 @@ SIG(0)
|
||||||
|
|
||||||
From RFC 2931:
|
From RFC 2931:
|
||||||
|
|
||||||
SIG(0) provides protection for DNS transactions and requests ....
|
SIG(0) provides protection for DNS transactions and requests ....
|
||||||
... protection for glue records, DNS requests, protection for message headers
|
... protection for glue records, DNS requests, protection for message headers
|
||||||
on requests and responses, and protection of the overall integrity of a response.
|
on requests and responses, and protection of the overall integrity of a response.
|
||||||
|
|
||||||
It works like TSIG, except that SIG(0) uses public key cryptography, instead of
|
It works like TSIG, except that SIG(0) uses public key cryptography, instead of
|
||||||
the shared secret approach in TSIG. Supported algorithms: DSA, ECDSAP256SHA256,
|
the shared secret approach in TSIG. Supported algorithms: ECDSAP256SHA256,
|
||||||
ECDSAP384SHA384, RSASHA1, RSASHA256 and RSASHA512.
|
ECDSAP384SHA384, RSASHA1, RSASHA256 and RSASHA512.
|
||||||
|
|
||||||
Signing subsequent messages in multi-message sessions is not implemented.
|
Signing subsequent messages in multi-message sessions is not implemented.
|
||||||
|
|
|
@ -3,9 +3,8 @@ package dns
|
||||||
//go:generate go run duplicate_generate.go
|
//go:generate go run duplicate_generate.go
|
||||||
|
|
||||||
// IsDuplicate checks of r1 and r2 are duplicates of each other, excluding the TTL.
|
// IsDuplicate checks of r1 and r2 are duplicates of each other, excluding the TTL.
|
||||||
// So this means the header data is equal *and* the RDATA is the same. Return true
|
// So this means the header data is equal *and* the RDATA is the same. Returns true
|
||||||
// is so, otherwise false.
|
// if so, otherwise false. It's a protocol violation to have identical RRs in a message.
|
||||||
// It's is a protocol violation to have identical RRs in a message.
|
|
||||||
func IsDuplicate(r1, r2 RR) bool {
|
func IsDuplicate(r1, r2 RR) bool {
|
||||||
// Check whether the record header is identical.
|
// Check whether the record header is identical.
|
||||||
if !r1.Header().isDuplicate(r2.Header()) {
|
if !r1.Header().isDuplicate(r2.Header()) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
//+build ignore
|
//go:build ignore
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
// types_generate.go is meant to run with go generate. It will use
|
// types_generate.go is meant to run with go generate. It will use
|
||||||
// go/{importer,types} to track down all the RR struct types. Then for each type
|
// go/{importer,types} to track down all the RR struct types. Then for each type
|
||||||
|
@ -11,10 +12,11 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
"go/importer"
|
|
||||||
"go/types"
|
"go/types"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
)
|
)
|
||||||
|
|
||||||
var packageHdr = `
|
var packageHdr = `
|
||||||
|
@ -29,6 +31,9 @@ func getTypeStruct(t types.Type, scope *types.Scope) (*types.Struct, bool) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
if st.NumFields() == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
if st.Field(0).Type() == scope.Lookup("RR_Header").Type() {
|
if st.Field(0).Type() == scope.Lookup("RR_Header").Type() {
|
||||||
return st, false
|
return st, false
|
||||||
}
|
}
|
||||||
|
@ -39,9 +44,19 @@ func getTypeStruct(t types.Type, scope *types.Scope) (*types.Struct, bool) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadModule retrieves package description for a given module.
|
||||||
|
func loadModule(name string) (*types.Package, error) {
|
||||||
|
conf := packages.Config{Mode: packages.NeedTypes | packages.NeedTypesInfo}
|
||||||
|
pkgs, err := packages.Load(&conf, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pkgs[0].Types, nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Import and type-check the package
|
// Import and type-check the package
|
||||||
pkg, err := importer.Default().Import("github.com/miekg/dns")
|
pkg, err := loadModule("github.com/miekg/dns")
|
||||||
fatalIfErr(err)
|
fatalIfErr(err)
|
||||||
scope := pkg.Scope()
|
scope := pkg.Scope()
|
||||||
|
|
||||||
|
@ -72,10 +87,7 @@ func main() {
|
||||||
for _, name := range namedTypes {
|
for _, name := range namedTypes {
|
||||||
|
|
||||||
o := scope.Lookup(name)
|
o := scope.Lookup(name)
|
||||||
st, isEmbedded := getTypeStruct(o.Type(), scope)
|
st, _ := getTypeStruct(o.Type(), scope)
|
||||||
if isEmbedded {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Fprintf(b, "func (r1 *%s) isDuplicate(_r2 RR) bool {\n", name)
|
fmt.Fprintf(b, "func (r1 *%s) isDuplicate(_r2 RR) bool {\n", name)
|
||||||
fmt.Fprintf(b, "r2, ok := _r2.(*%s)\n", name)
|
fmt.Fprintf(b, "r2, ok := _r2.(*%s)\n", name)
|
||||||
fmt.Fprint(b, "if !ok { return false }\n")
|
fmt.Fprint(b, "if !ok { return false }\n")
|
||||||
|
@ -100,6 +112,24 @@ func main() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if st.Tag(i) == `dns:"apl"` {
|
||||||
|
o3(`for i := 0; i < len(r1.%s); i++ {
|
||||||
|
if !r1.%s[i].equals(&r2.%s[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if st.Tag(i) == `dns:"pairs"` {
|
||||||
|
o2(`if !areSVCBPairArraysEqual(r1.%s, r2.%s) {
|
||||||
|
return false
|
||||||
|
}`)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
o3(`for i := 0; i < len(r1.%s); i++ {
|
o3(`for i := 0; i < len(r1.%s); i++ {
|
||||||
if r1.%s[i] != r2.%s[i] {
|
if r1.%s[i] != r2.%s[i] {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -34,6 +34,39 @@ func TestDuplicateTXT(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDuplicateSVCB(t *testing.T) {
|
||||||
|
a1, _ := NewRR(`example.com. 3600 IN SVCB 1 . ipv6hint=1::3:3:3:3 key65300=\254\032\030\000\ \043,\;`)
|
||||||
|
a2, _ := NewRR(`example.com. 3600 IN SVCB 1 . ipv6hint=1:0::3:3:3:3 key65300="\254\ \030\000 +\,;"`)
|
||||||
|
|
||||||
|
if !IsDuplicate(a1, a2) {
|
||||||
|
t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
a2, _ = NewRR(`example.com. 3600 IN SVCB 1 . ipv6hint=1::3:3:3:3 key65300="\255\ \030\000 +\,;"`)
|
||||||
|
|
||||||
|
if IsDuplicate(a1, a2) {
|
||||||
|
t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
a1, _ = NewRR(`example.com. 3600 IN SVCB 1 . ipv6hint=1::3:3:3:3`)
|
||||||
|
|
||||||
|
if IsDuplicate(a1, a2) {
|
||||||
|
t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
a2, _ = NewRR(`example.com. 3600 IN SVCB 1 . ipv4hint=1.1.1.1`)
|
||||||
|
|
||||||
|
if IsDuplicate(a1, a2) {
|
||||||
|
t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
a1, _ = NewRR(`example.com. 3600 IN SVCB 1 . ipv4hint=1.1.1.1,1.0.2.1`)
|
||||||
|
|
||||||
|
if IsDuplicate(a1, a2) {
|
||||||
|
t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDuplicateOwner(t *testing.T) {
|
func TestDuplicateOwner(t *testing.T) {
|
||||||
a1, _ := NewRR("www.example.org. IN A 127.0.0.1")
|
a1, _ := NewRR("www.example.org. IN A 127.0.0.1")
|
||||||
a2, _ := NewRR("www.example.org. IN A 127.0.0.1")
|
a2, _ := NewRR("www.example.org. IN A 127.0.0.1")
|
||||||
|
|
293
edns.go
293
edns.go
|
@ -14,6 +14,7 @@ const (
|
||||||
EDNS0LLQ = 0x1 // long lived queries: http://tools.ietf.org/html/draft-sekar-dns-llq-01
|
EDNS0LLQ = 0x1 // long lived queries: http://tools.ietf.org/html/draft-sekar-dns-llq-01
|
||||||
EDNS0UL = 0x2 // update lease draft: http://files.dns-sd.org/draft-sekar-dns-ul.txt
|
EDNS0UL = 0x2 // update lease draft: http://files.dns-sd.org/draft-sekar-dns-ul.txt
|
||||||
EDNS0NSID = 0x3 // nsid (See RFC 5001)
|
EDNS0NSID = 0x3 // nsid (See RFC 5001)
|
||||||
|
EDNS0ESU = 0x4 // ENUM Source-URI draft: https://datatracker.ietf.org/doc/html/draft-kaplan-enum-source-uri-00
|
||||||
EDNS0DAU = 0x5 // DNSSEC Algorithm Understood
|
EDNS0DAU = 0x5 // DNSSEC Algorithm Understood
|
||||||
EDNS0DHU = 0x6 // DS Hash Understood
|
EDNS0DHU = 0x6 // DS Hash Understood
|
||||||
EDNS0N3U = 0x7 // NSEC3 Hash Understood
|
EDNS0N3U = 0x7 // NSEC3 Hash Understood
|
||||||
|
@ -22,11 +23,49 @@ const (
|
||||||
EDNS0COOKIE = 0xa // EDNS0 Cookie
|
EDNS0COOKIE = 0xa // EDNS0 Cookie
|
||||||
EDNS0TCPKEEPALIVE = 0xb // EDNS0 tcp keep alive (See RFC 7828)
|
EDNS0TCPKEEPALIVE = 0xb // EDNS0 tcp keep alive (See RFC 7828)
|
||||||
EDNS0PADDING = 0xc // EDNS0 padding (See RFC 7830)
|
EDNS0PADDING = 0xc // EDNS0 padding (See RFC 7830)
|
||||||
|
EDNS0EDE = 0xf // EDNS0 extended DNS errors (See RFC 8914)
|
||||||
EDNS0LOCALSTART = 0xFDE9 // Beginning of range reserved for local/experimental use (See RFC 6891)
|
EDNS0LOCALSTART = 0xFDE9 // Beginning of range reserved for local/experimental use (See RFC 6891)
|
||||||
EDNS0LOCALEND = 0xFFFE // End of range reserved for local/experimental use (See RFC 6891)
|
EDNS0LOCALEND = 0xFFFE // End of range reserved for local/experimental use (See RFC 6891)
|
||||||
_DO = 1 << 15 // DNSSEC OK
|
_DO = 1 << 15 // DNSSEC OK
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// makeDataOpt is used to unpack the EDNS0 option(s) from a message.
|
||||||
|
func makeDataOpt(code uint16) EDNS0 {
|
||||||
|
// All the EDNS0.* constants above need to be in this switch.
|
||||||
|
switch code {
|
||||||
|
case EDNS0LLQ:
|
||||||
|
return new(EDNS0_LLQ)
|
||||||
|
case EDNS0UL:
|
||||||
|
return new(EDNS0_UL)
|
||||||
|
case EDNS0NSID:
|
||||||
|
return new(EDNS0_NSID)
|
||||||
|
case EDNS0DAU:
|
||||||
|
return new(EDNS0_DAU)
|
||||||
|
case EDNS0DHU:
|
||||||
|
return new(EDNS0_DHU)
|
||||||
|
case EDNS0N3U:
|
||||||
|
return new(EDNS0_N3U)
|
||||||
|
case EDNS0SUBNET:
|
||||||
|
return new(EDNS0_SUBNET)
|
||||||
|
case EDNS0EXPIRE:
|
||||||
|
return new(EDNS0_EXPIRE)
|
||||||
|
case EDNS0COOKIE:
|
||||||
|
return new(EDNS0_COOKIE)
|
||||||
|
case EDNS0TCPKEEPALIVE:
|
||||||
|
return new(EDNS0_TCP_KEEPALIVE)
|
||||||
|
case EDNS0PADDING:
|
||||||
|
return new(EDNS0_PADDING)
|
||||||
|
case EDNS0EDE:
|
||||||
|
return new(EDNS0_EDE)
|
||||||
|
case EDNS0ESU:
|
||||||
|
return &EDNS0_ESU{Code: EDNS0ESU}
|
||||||
|
default:
|
||||||
|
e := new(EDNS0_LOCAL)
|
||||||
|
e.Code = code
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// OPT is the EDNS0 RR appended to messages to convey extra (meta) information.
|
// OPT is the EDNS0 RR appended to messages to convey extra (meta) information.
|
||||||
// See RFC 6891.
|
// See RFC 6891.
|
||||||
type OPT struct {
|
type OPT struct {
|
||||||
|
@ -39,7 +78,10 @@ func (rr *OPT) String() string {
|
||||||
if rr.Do() {
|
if rr.Do() {
|
||||||
s += "flags: do; "
|
s += "flags: do; "
|
||||||
} else {
|
} else {
|
||||||
s += "flags: ; "
|
s += "flags:; "
|
||||||
|
}
|
||||||
|
if rr.Hdr.Ttl&0x7FFF != 0 {
|
||||||
|
s += fmt.Sprintf("MBZ: 0x%04x, ", rr.Hdr.Ttl&0x7FFF)
|
||||||
}
|
}
|
||||||
s += "udp: " + strconv.Itoa(int(rr.UDPSize()))
|
s += "udp: " + strconv.Itoa(int(rr.UDPSize()))
|
||||||
|
|
||||||
|
@ -59,6 +101,10 @@ func (rr *OPT) String() string {
|
||||||
s += "\n; SUBNET: " + o.String()
|
s += "\n; SUBNET: " + o.String()
|
||||||
case *EDNS0_COOKIE:
|
case *EDNS0_COOKIE:
|
||||||
s += "\n; COOKIE: " + o.String()
|
s += "\n; COOKIE: " + o.String()
|
||||||
|
case *EDNS0_EXPIRE:
|
||||||
|
s += "\n; EXPIRE: " + o.String()
|
||||||
|
case *EDNS0_TCP_KEEPALIVE:
|
||||||
|
s += "\n; KEEPALIVE: " + o.String()
|
||||||
case *EDNS0_UL:
|
case *EDNS0_UL:
|
||||||
s += "\n; UPDATE LEASE: " + o.String()
|
s += "\n; UPDATE LEASE: " + o.String()
|
||||||
case *EDNS0_LLQ:
|
case *EDNS0_LLQ:
|
||||||
|
@ -73,6 +119,10 @@ func (rr *OPT) String() string {
|
||||||
s += "\n; LOCAL OPT: " + o.String()
|
s += "\n; LOCAL OPT: " + o.String()
|
||||||
case *EDNS0_PADDING:
|
case *EDNS0_PADDING:
|
||||||
s += "\n; PADDING: " + o.String()
|
s += "\n; PADDING: " + o.String()
|
||||||
|
case *EDNS0_EDE:
|
||||||
|
s += "\n; EDE: " + o.String()
|
||||||
|
case *EDNS0_ESU:
|
||||||
|
s += "\n; ESU: " + o.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
|
@ -88,11 +138,11 @@ func (rr *OPT) len(off int, compression map[string]struct{}) int {
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rr *OPT) parse(c *zlexer, origin, file string) *ParseError {
|
func (*OPT) parse(c *zlexer, origin string) *ParseError {
|
||||||
panic("dns: internal error: parse should never be called on OPT")
|
return &ParseError{err: "OPT records do not have a presentation format"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r1 *OPT) isDuplicate(r2 RR) bool { return false }
|
func (rr *OPT) isDuplicate(r2 RR) bool { return false }
|
||||||
|
|
||||||
// return the old value -> delete SetVersion?
|
// return the old value -> delete SetVersion?
|
||||||
|
|
||||||
|
@ -148,6 +198,16 @@ func (rr *OPT) SetDo(do ...bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Z returns the Z part of the OPT RR as a uint16 with only the 15 least significant bits used.
|
||||||
|
func (rr *OPT) Z() uint16 {
|
||||||
|
return uint16(rr.Hdr.Ttl & 0x7FFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetZ sets the Z part of the OPT RR, note only the 15 least significant bits of z are used.
|
||||||
|
func (rr *OPT) SetZ(z uint16) {
|
||||||
|
rr.Hdr.Ttl = rr.Hdr.Ttl&^0x7FFF | uint32(z&0x7FFF)
|
||||||
|
}
|
||||||
|
|
||||||
// EDNS0 defines an EDNS0 Option. An OPT RR can have multiple options appended to it.
|
// EDNS0 defines an EDNS0 Option. An OPT RR can have multiple options appended to it.
|
||||||
type EDNS0 interface {
|
type EDNS0 interface {
|
||||||
// Option returns the option code for the option.
|
// Option returns the option code for the option.
|
||||||
|
@ -203,7 +263,7 @@ func (e *EDNS0_NSID) copy() EDNS0 { return &EDNS0_NSID{e.Code, e.Nsid}
|
||||||
// o.Hdr.Name = "."
|
// o.Hdr.Name = "."
|
||||||
// o.Hdr.Rrtype = dns.TypeOPT
|
// o.Hdr.Rrtype = dns.TypeOPT
|
||||||
// e := new(dns.EDNS0_SUBNET)
|
// e := new(dns.EDNS0_SUBNET)
|
||||||
// e.Code = dns.EDNS0SUBNET
|
// e.Code = dns.EDNS0SUBNET // by default this is filled in through unpacking OPT packets (unpackDataOpt)
|
||||||
// e.Family = 1 // 1 for IPv4 source address, 2 for IPv6
|
// e.Family = 1 // 1 for IPv4 source address, 2 for IPv6
|
||||||
// e.SourceNetmask = 32 // 32 for IPV4, 128 for IPv6
|
// e.SourceNetmask = 32 // 32 for IPV4, 128 for IPv6
|
||||||
// e.SourceScope = 0
|
// e.SourceScope = 0
|
||||||
|
@ -360,7 +420,7 @@ func (e *EDNS0_COOKIE) copy() EDNS0 { return &EDNS0_COOKIE{e.Code, e.C
|
||||||
// The EDNS0_UL (Update Lease) (draft RFC) option is used to tell the server to set
|
// The EDNS0_UL (Update Lease) (draft RFC) option is used to tell the server to set
|
||||||
// an expiration on an update RR. This is helpful for clients that cannot clean
|
// an expiration on an update RR. This is helpful for clients that cannot clean
|
||||||
// up after themselves. This is a draft RFC and more information can be found at
|
// up after themselves. This is a draft RFC and more information can be found at
|
||||||
// http://files.dns-sd.org/draft-sekar-dns-ul.txt
|
// https://tools.ietf.org/html/draft-sekar-dns-ul-02
|
||||||
//
|
//
|
||||||
// o := new(dns.OPT)
|
// o := new(dns.OPT)
|
||||||
// o.Hdr.Name = "."
|
// o.Hdr.Name = "."
|
||||||
|
@ -370,24 +430,36 @@ func (e *EDNS0_COOKIE) copy() EDNS0 { return &EDNS0_COOKIE{e.Code, e.C
|
||||||
// e.Lease = 120 // in seconds
|
// e.Lease = 120 // in seconds
|
||||||
// o.Option = append(o.Option, e)
|
// o.Option = append(o.Option, e)
|
||||||
type EDNS0_UL struct {
|
type EDNS0_UL struct {
|
||||||
Code uint16 // Always EDNS0UL
|
Code uint16 // Always EDNS0UL
|
||||||
Lease uint32
|
Lease uint32
|
||||||
|
KeyLease uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option implements the EDNS0 interface.
|
// Option implements the EDNS0 interface.
|
||||||
func (e *EDNS0_UL) Option() uint16 { return EDNS0UL }
|
func (e *EDNS0_UL) Option() uint16 { return EDNS0UL }
|
||||||
func (e *EDNS0_UL) String() string { return strconv.FormatUint(uint64(e.Lease), 10) }
|
func (e *EDNS0_UL) String() string { return fmt.Sprintf("%d %d", e.Lease, e.KeyLease) }
|
||||||
func (e *EDNS0_UL) copy() EDNS0 { return &EDNS0_UL{e.Code, e.Lease} }
|
func (e *EDNS0_UL) copy() EDNS0 { return &EDNS0_UL{e.Code, e.Lease, e.KeyLease} }
|
||||||
|
|
||||||
// Copied: http://golang.org/src/pkg/net/dnsmsg.go
|
// Copied: http://golang.org/src/pkg/net/dnsmsg.go
|
||||||
func (e *EDNS0_UL) pack() ([]byte, error) {
|
func (e *EDNS0_UL) pack() ([]byte, error) {
|
||||||
b := make([]byte, 4)
|
var b []byte
|
||||||
|
if e.KeyLease == 0 {
|
||||||
|
b = make([]byte, 4)
|
||||||
|
} else {
|
||||||
|
b = make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint32(b[4:], e.KeyLease)
|
||||||
|
}
|
||||||
binary.BigEndian.PutUint32(b, e.Lease)
|
binary.BigEndian.PutUint32(b, e.Lease)
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EDNS0_UL) unpack(b []byte) error {
|
func (e *EDNS0_UL) unpack(b []byte) error {
|
||||||
if len(b) < 4 {
|
switch len(b) {
|
||||||
|
case 4:
|
||||||
|
e.KeyLease = 0
|
||||||
|
case 8:
|
||||||
|
e.KeyLease = binary.BigEndian.Uint32(b[4:])
|
||||||
|
default:
|
||||||
return ErrBuf
|
return ErrBuf
|
||||||
}
|
}
|
||||||
e.Lease = binary.BigEndian.Uint32(b)
|
e.Lease = binary.BigEndian.Uint32(b)
|
||||||
|
@ -440,7 +512,7 @@ func (e *EDNS0_LLQ) copy() EDNS0 {
|
||||||
return &EDNS0_LLQ{e.Code, e.Version, e.Opcode, e.Error, e.Id, e.LeaseLife}
|
return &EDNS0_LLQ{e.Code, e.Version, e.Opcode, e.Error, e.Id, e.LeaseLife}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EDNS0_DUA implements the EDNS0 "DNSSEC Algorithm Understood" option. See RFC 6975.
|
// EDNS0_DAU implements the EDNS0 "DNSSEC Algorithm Understood" option. See RFC 6975.
|
||||||
type EDNS0_DAU struct {
|
type EDNS0_DAU struct {
|
||||||
Code uint16 // Always EDNS0DAU
|
Code uint16 // Always EDNS0DAU
|
||||||
AlgCode []uint8
|
AlgCode []uint8
|
||||||
|
@ -513,31 +585,47 @@ func (e *EDNS0_N3U) String() string {
|
||||||
}
|
}
|
||||||
func (e *EDNS0_N3U) copy() EDNS0 { return &EDNS0_N3U{e.Code, e.AlgCode} }
|
func (e *EDNS0_N3U) copy() EDNS0 { return &EDNS0_N3U{e.Code, e.AlgCode} }
|
||||||
|
|
||||||
// EDNS0_EXPIRE implementes the EDNS0 option as described in RFC 7314.
|
// EDNS0_EXPIRE implements the EDNS0 option as described in RFC 7314.
|
||||||
type EDNS0_EXPIRE struct {
|
type EDNS0_EXPIRE struct {
|
||||||
Code uint16 // Always EDNS0EXPIRE
|
Code uint16 // Always EDNS0EXPIRE
|
||||||
Expire uint32
|
Expire uint32
|
||||||
|
Empty bool // Empty is used to signal an empty Expire option in a backwards compatible way, it's not used on the wire.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option implements the EDNS0 interface.
|
// Option implements the EDNS0 interface.
|
||||||
func (e *EDNS0_EXPIRE) Option() uint16 { return EDNS0EXPIRE }
|
func (e *EDNS0_EXPIRE) Option() uint16 { return EDNS0EXPIRE }
|
||||||
func (e *EDNS0_EXPIRE) String() string { return strconv.FormatUint(uint64(e.Expire), 10) }
|
func (e *EDNS0_EXPIRE) copy() EDNS0 { return &EDNS0_EXPIRE{e.Code, e.Expire, e.Empty} }
|
||||||
func (e *EDNS0_EXPIRE) copy() EDNS0 { return &EDNS0_EXPIRE{e.Code, e.Expire} }
|
|
||||||
|
|
||||||
func (e *EDNS0_EXPIRE) pack() ([]byte, error) {
|
func (e *EDNS0_EXPIRE) pack() ([]byte, error) {
|
||||||
|
if e.Empty {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
b := make([]byte, 4)
|
b := make([]byte, 4)
|
||||||
binary.BigEndian.PutUint32(b, e.Expire)
|
binary.BigEndian.PutUint32(b, e.Expire)
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EDNS0_EXPIRE) unpack(b []byte) error {
|
func (e *EDNS0_EXPIRE) unpack(b []byte) error {
|
||||||
|
if len(b) == 0 {
|
||||||
|
// zero-length EXPIRE query, see RFC 7314 Section 2
|
||||||
|
e.Empty = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if len(b) < 4 {
|
if len(b) < 4 {
|
||||||
return ErrBuf
|
return ErrBuf
|
||||||
}
|
}
|
||||||
e.Expire = binary.BigEndian.Uint32(b)
|
e.Expire = binary.BigEndian.Uint32(b)
|
||||||
|
e.Empty = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *EDNS0_EXPIRE) String() (s string) {
|
||||||
|
if e.Empty {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strconv.FormatUint(uint64(e.Expire), 10)
|
||||||
|
}
|
||||||
|
|
||||||
// The EDNS0_LOCAL option is used for local/experimental purposes. The option
|
// The EDNS0_LOCAL option is used for local/experimental purposes. The option
|
||||||
// code is recommended to be within the range [EDNS0LOCALSTART, EDNS0LOCALEND]
|
// code is recommended to be within the range [EDNS0LOCALSTART, EDNS0LOCALEND]
|
||||||
// (RFC6891), although any unassigned code can actually be used. The content of
|
// (RFC6891), although any unassigned code can actually be used. The content of
|
||||||
|
@ -588,57 +676,52 @@ func (e *EDNS0_LOCAL) unpack(b []byte) error {
|
||||||
// EDNS0_TCP_KEEPALIVE is an EDNS0 option that instructs the server to keep
|
// EDNS0_TCP_KEEPALIVE is an EDNS0 option that instructs the server to keep
|
||||||
// the TCP connection alive. See RFC 7828.
|
// the TCP connection alive. See RFC 7828.
|
||||||
type EDNS0_TCP_KEEPALIVE struct {
|
type EDNS0_TCP_KEEPALIVE struct {
|
||||||
Code uint16 // Always EDNSTCPKEEPALIVE
|
Code uint16 // Always EDNSTCPKEEPALIVE
|
||||||
Length uint16 // the value 0 if the TIMEOUT is omitted, the value 2 if it is present;
|
|
||||||
Timeout uint16 // an idle timeout value for the TCP connection, specified in units of 100 milliseconds, encoded in network byte order.
|
// Timeout is an idle timeout value for the TCP connection, specified in
|
||||||
|
// units of 100 milliseconds, encoded in network byte order. If set to 0,
|
||||||
|
// pack will return a nil slice.
|
||||||
|
Timeout uint16
|
||||||
|
|
||||||
|
// Length is the option's length.
|
||||||
|
// Deprecated: this field is deprecated and is always equal to 0.
|
||||||
|
Length uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option implements the EDNS0 interface.
|
// Option implements the EDNS0 interface.
|
||||||
func (e *EDNS0_TCP_KEEPALIVE) Option() uint16 { return EDNS0TCPKEEPALIVE }
|
func (e *EDNS0_TCP_KEEPALIVE) Option() uint16 { return EDNS0TCPKEEPALIVE }
|
||||||
|
|
||||||
func (e *EDNS0_TCP_KEEPALIVE) pack() ([]byte, error) {
|
func (e *EDNS0_TCP_KEEPALIVE) pack() ([]byte, error) {
|
||||||
if e.Timeout != 0 && e.Length != 2 {
|
if e.Timeout > 0 {
|
||||||
return nil, errors.New("dns: timeout specified but length is not 2")
|
b := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(b, e.Timeout)
|
||||||
|
return b, nil
|
||||||
}
|
}
|
||||||
if e.Timeout == 0 && e.Length != 0 {
|
return nil, nil
|
||||||
return nil, errors.New("dns: timeout not specified but length is not 0")
|
|
||||||
}
|
|
||||||
b := make([]byte, 4+e.Length)
|
|
||||||
binary.BigEndian.PutUint16(b[0:], e.Code)
|
|
||||||
binary.BigEndian.PutUint16(b[2:], e.Length)
|
|
||||||
if e.Length == 2 {
|
|
||||||
binary.BigEndian.PutUint16(b[4:], e.Timeout)
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EDNS0_TCP_KEEPALIVE) unpack(b []byte) error {
|
func (e *EDNS0_TCP_KEEPALIVE) unpack(b []byte) error {
|
||||||
if len(b) < 4 {
|
switch len(b) {
|
||||||
return ErrBuf
|
case 0:
|
||||||
}
|
case 2:
|
||||||
e.Length = binary.BigEndian.Uint16(b[2:4])
|
e.Timeout = binary.BigEndian.Uint16(b)
|
||||||
if e.Length != 0 && e.Length != 2 {
|
default:
|
||||||
return errors.New("dns: length mismatch, want 0/2 but got " + strconv.FormatUint(uint64(e.Length), 10))
|
return fmt.Errorf("dns: length mismatch, want 0/2 but got %d", len(b))
|
||||||
}
|
|
||||||
if e.Length == 2 {
|
|
||||||
if len(b) < 6 {
|
|
||||||
return ErrBuf
|
|
||||||
}
|
|
||||||
e.Timeout = binary.BigEndian.Uint16(b[4:6])
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EDNS0_TCP_KEEPALIVE) String() (s string) {
|
func (e *EDNS0_TCP_KEEPALIVE) String() string {
|
||||||
s = "use tcp keep-alive"
|
s := "use tcp keep-alive"
|
||||||
if e.Length == 0 {
|
if e.Timeout == 0 {
|
||||||
s += ", timeout omitted"
|
s += ", timeout omitted"
|
||||||
} else {
|
} else {
|
||||||
s += fmt.Sprintf(", timeout %dms", e.Timeout*100)
|
s += fmt.Sprintf(", timeout %dms", e.Timeout*100)
|
||||||
}
|
}
|
||||||
return
|
return s
|
||||||
}
|
}
|
||||||
func (e *EDNS0_TCP_KEEPALIVE) copy() EDNS0 { return &EDNS0_TCP_KEEPALIVE{e.Code, e.Length, e.Timeout} }
|
|
||||||
|
func (e *EDNS0_TCP_KEEPALIVE) copy() EDNS0 { return &EDNS0_TCP_KEEPALIVE{e.Code, e.Timeout, e.Length} }
|
||||||
|
|
||||||
// EDNS0_PADDING option is used to add padding to a request/response. The default
|
// EDNS0_PADDING option is used to add padding to a request/response. The default
|
||||||
// value of padding SHOULD be 0x0 but other values MAY be used, for instance if
|
// value of padding SHOULD be 0x0 but other values MAY be used, for instance if
|
||||||
|
@ -657,3 +740,117 @@ func (e *EDNS0_PADDING) copy() EDNS0 {
|
||||||
copy(b, e.Padding)
|
copy(b, e.Padding)
|
||||||
return &EDNS0_PADDING{b}
|
return &EDNS0_PADDING{b}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extended DNS Error Codes (RFC 8914).
|
||||||
|
const (
|
||||||
|
ExtendedErrorCodeOther uint16 = iota
|
||||||
|
ExtendedErrorCodeUnsupportedDNSKEYAlgorithm
|
||||||
|
ExtendedErrorCodeUnsupportedDSDigestType
|
||||||
|
ExtendedErrorCodeStaleAnswer
|
||||||
|
ExtendedErrorCodeForgedAnswer
|
||||||
|
ExtendedErrorCodeDNSSECIndeterminate
|
||||||
|
ExtendedErrorCodeDNSBogus
|
||||||
|
ExtendedErrorCodeSignatureExpired
|
||||||
|
ExtendedErrorCodeSignatureNotYetValid
|
||||||
|
ExtendedErrorCodeDNSKEYMissing
|
||||||
|
ExtendedErrorCodeRRSIGsMissing
|
||||||
|
ExtendedErrorCodeNoZoneKeyBitSet
|
||||||
|
ExtendedErrorCodeNSECMissing
|
||||||
|
ExtendedErrorCodeCachedError
|
||||||
|
ExtendedErrorCodeNotReady
|
||||||
|
ExtendedErrorCodeBlocked
|
||||||
|
ExtendedErrorCodeCensored
|
||||||
|
ExtendedErrorCodeFiltered
|
||||||
|
ExtendedErrorCodeProhibited
|
||||||
|
ExtendedErrorCodeStaleNXDOMAINAnswer
|
||||||
|
ExtendedErrorCodeNotAuthoritative
|
||||||
|
ExtendedErrorCodeNotSupported
|
||||||
|
ExtendedErrorCodeNoReachableAuthority
|
||||||
|
ExtendedErrorCodeNetworkError
|
||||||
|
ExtendedErrorCodeInvalidData
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtendedErrorCodeToString maps extended error info codes to a human readable
|
||||||
|
// description.
|
||||||
|
var ExtendedErrorCodeToString = map[uint16]string{
|
||||||
|
ExtendedErrorCodeOther: "Other",
|
||||||
|
ExtendedErrorCodeUnsupportedDNSKEYAlgorithm: "Unsupported DNSKEY Algorithm",
|
||||||
|
ExtendedErrorCodeUnsupportedDSDigestType: "Unsupported DS Digest Type",
|
||||||
|
ExtendedErrorCodeStaleAnswer: "Stale Answer",
|
||||||
|
ExtendedErrorCodeForgedAnswer: "Forged Answer",
|
||||||
|
ExtendedErrorCodeDNSSECIndeterminate: "DNSSEC Indeterminate",
|
||||||
|
ExtendedErrorCodeDNSBogus: "DNSSEC Bogus",
|
||||||
|
ExtendedErrorCodeSignatureExpired: "Signature Expired",
|
||||||
|
ExtendedErrorCodeSignatureNotYetValid: "Signature Not Yet Valid",
|
||||||
|
ExtendedErrorCodeDNSKEYMissing: "DNSKEY Missing",
|
||||||
|
ExtendedErrorCodeRRSIGsMissing: "RRSIGs Missing",
|
||||||
|
ExtendedErrorCodeNoZoneKeyBitSet: "No Zone Key Bit Set",
|
||||||
|
ExtendedErrorCodeNSECMissing: "NSEC Missing",
|
||||||
|
ExtendedErrorCodeCachedError: "Cached Error",
|
||||||
|
ExtendedErrorCodeNotReady: "Not Ready",
|
||||||
|
ExtendedErrorCodeBlocked: "Blocked",
|
||||||
|
ExtendedErrorCodeCensored: "Censored",
|
||||||
|
ExtendedErrorCodeFiltered: "Filtered",
|
||||||
|
ExtendedErrorCodeProhibited: "Prohibited",
|
||||||
|
ExtendedErrorCodeStaleNXDOMAINAnswer: "Stale NXDOMAIN Answer",
|
||||||
|
ExtendedErrorCodeNotAuthoritative: "Not Authoritative",
|
||||||
|
ExtendedErrorCodeNotSupported: "Not Supported",
|
||||||
|
ExtendedErrorCodeNoReachableAuthority: "No Reachable Authority",
|
||||||
|
ExtendedErrorCodeNetworkError: "Network Error",
|
||||||
|
ExtendedErrorCodeInvalidData: "Invalid Data",
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringToExtendedErrorCode is a map from human readable descriptions to
|
||||||
|
// extended error info codes.
|
||||||
|
var StringToExtendedErrorCode = reverseInt16(ExtendedErrorCodeToString)
|
||||||
|
|
||||||
|
// EDNS0_EDE option is used to return additional information about the cause of
|
||||||
|
// DNS errors.
|
||||||
|
type EDNS0_EDE struct {
|
||||||
|
InfoCode uint16
|
||||||
|
ExtraText string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option implements the EDNS0 interface.
|
||||||
|
func (e *EDNS0_EDE) Option() uint16 { return EDNS0EDE }
|
||||||
|
func (e *EDNS0_EDE) copy() EDNS0 { return &EDNS0_EDE{e.InfoCode, e.ExtraText} }
|
||||||
|
|
||||||
|
func (e *EDNS0_EDE) String() string {
|
||||||
|
info := strconv.FormatUint(uint64(e.InfoCode), 10)
|
||||||
|
if s, ok := ExtendedErrorCodeToString[e.InfoCode]; ok {
|
||||||
|
info += fmt.Sprintf(" (%s)", s)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: (%s)", info, e.ExtraText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EDNS0_EDE) pack() ([]byte, error) {
|
||||||
|
b := make([]byte, 2+len(e.ExtraText))
|
||||||
|
binary.BigEndian.PutUint16(b[0:], e.InfoCode)
|
||||||
|
copy(b[2:], []byte(e.ExtraText))
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EDNS0_EDE) unpack(b []byte) error {
|
||||||
|
if len(b) < 2 {
|
||||||
|
return ErrBuf
|
||||||
|
}
|
||||||
|
e.InfoCode = binary.BigEndian.Uint16(b[0:])
|
||||||
|
e.ExtraText = string(b[2:])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The EDNS0_ESU option for ENUM Source-URI Extension
|
||||||
|
type EDNS0_ESU struct {
|
||||||
|
Code uint16
|
||||||
|
Uri string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option implements the EDNS0 interface.
|
||||||
|
func (e *EDNS0_ESU) Option() uint16 { return EDNS0ESU }
|
||||||
|
func (e *EDNS0_ESU) String() string { return e.Uri }
|
||||||
|
func (e *EDNS0_ESU) copy() EDNS0 { return &EDNS0_ESU{e.Code, e.Uri} }
|
||||||
|
func (e *EDNS0_ESU) pack() ([]byte, error) { return []byte(e.Uri), nil }
|
||||||
|
func (e *EDNS0_ESU) unpack(b []byte) error {
|
||||||
|
e.Uri = string(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
168
edns_test.go
168
edns_test.go
|
@ -1,6 +1,7 @@
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -109,3 +110,170 @@ func TestEDNS0_SUBNETUnpack(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEDNS0_UL(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
l uint32
|
||||||
|
kl uint32
|
||||||
|
}{
|
||||||
|
{0x01234567, 0},
|
||||||
|
{0x76543210, 0xFEDCBA98},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
expect := EDNS0_UL{EDNS0UL, c.l, c.kl}
|
||||||
|
b, err := expect.pack()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to pack: %v", err)
|
||||||
|
}
|
||||||
|
actual := EDNS0_UL{EDNS0UL, ^uint32(0), ^uint32(0)}
|
||||||
|
if err := actual.unpack(b); err != nil {
|
||||||
|
t.Fatalf("failed to unpack: %v", err)
|
||||||
|
}
|
||||||
|
if expect != actual {
|
||||||
|
t.Errorf("unpacked option is different; expected %v, got %v", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZ(t *testing.T) {
|
||||||
|
e := &OPT{}
|
||||||
|
e.Hdr.Name = "."
|
||||||
|
e.Hdr.Rrtype = TypeOPT
|
||||||
|
e.SetVersion(8)
|
||||||
|
e.SetDo()
|
||||||
|
if e.Z() != 0 {
|
||||||
|
t.Errorf("expected Z of 0, got %d", e.Z())
|
||||||
|
}
|
||||||
|
e.SetZ(5)
|
||||||
|
if e.Z() != 5 {
|
||||||
|
t.Errorf("expected Z of 5, got %d", e.Z())
|
||||||
|
}
|
||||||
|
e.SetZ(0xFFFF)
|
||||||
|
if e.Z() != 0x7FFF {
|
||||||
|
t.Errorf("expected Z of 0x7FFFF, got %d", e.Z())
|
||||||
|
}
|
||||||
|
if e.Version() != 8 {
|
||||||
|
t.Errorf("expected version to still be 8, got %d", e.Version())
|
||||||
|
}
|
||||||
|
if !e.Do() {
|
||||||
|
t.Error("expected DO to be set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEDNS0_ESU(t *testing.T) {
|
||||||
|
p := []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x29, 0x04,
|
||||||
|
0xC4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||||
|
0x04, 0x00, 0x24, 0x73, 0x69, 0x70, 0x3A, 0x2B,
|
||||||
|
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
||||||
|
0x39, 0x40, 0x74, 0x65, 0x73, 0x74, 0x2E, 0x63,
|
||||||
|
0x6F, 0x6D, 0x3B, 0x75, 0x73, 0x65, 0x72, 0x3D,
|
||||||
|
0x63, 0x67, 0x72, 0x61, 0x74, 0x65, 0x73,
|
||||||
|
}
|
||||||
|
|
||||||
|
m := new(Msg)
|
||||||
|
if err := m.Unpack(p); err != nil {
|
||||||
|
t.Fatalf("failed to unpack: %v", err)
|
||||||
|
}
|
||||||
|
opt := m.IsEdns0()
|
||||||
|
if opt == nil {
|
||||||
|
t.Fatalf("expected edns0 option")
|
||||||
|
}
|
||||||
|
if len(opt.Option) != 1 {
|
||||||
|
t.Fatalf("expected only one option: %v", opt.Option)
|
||||||
|
}
|
||||||
|
edns0 := opt.Option[0]
|
||||||
|
esu, ok := edns0.(*EDNS0_ESU)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected option of type EDNS0_ESU, got %t", edns0)
|
||||||
|
}
|
||||||
|
expect := "sip:+123456789@test.com;user=cgrates"
|
||||||
|
if esu.Uri != expect {
|
||||||
|
t.Errorf("unpacked option is different; expected %v, got %v", expect, esu.Uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEDNS0_TCP_KEEPALIVE_unpack(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
b []byte
|
||||||
|
expected uint16
|
||||||
|
expectedErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
b: []byte{},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "timeout 1",
|
||||||
|
b: []byte{0, 1},
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
b: []byte{0, 1, 3},
|
||||||
|
expectedErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
e := &EDNS0_TCP_KEEPALIVE{}
|
||||||
|
err := e.unpack(tc.b)
|
||||||
|
if err != nil && !tc.expectedErr {
|
||||||
|
t.Error("failed to unpack, expected no error")
|
||||||
|
}
|
||||||
|
if err == nil && tc.expectedErr {
|
||||||
|
t.Error("unpacked, but expected an error")
|
||||||
|
}
|
||||||
|
if e.Timeout != tc.expected {
|
||||||
|
t.Errorf("invalid timeout, actual: %d, expected: %d", e.Timeout, tc.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEDNS0_TCP_KEEPALIVE_pack(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
edns *EDNS0_TCP_KEEPALIVE
|
||||||
|
expected []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
edns: &EDNS0_TCP_KEEPALIVE{
|
||||||
|
Code: EDNS0TCPKEEPALIVE,
|
||||||
|
Timeout: 0,
|
||||||
|
},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "timeout 1",
|
||||||
|
edns: &EDNS0_TCP_KEEPALIVE{
|
||||||
|
Code: EDNS0TCPKEEPALIVE,
|
||||||
|
Timeout: 1,
|
||||||
|
},
|
||||||
|
expected: []byte{0, 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
b, err := tc.edns.pack()
|
||||||
|
if err != nil {
|
||||||
|
t.Error("expected no error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expected == nil && b != nil {
|
||||||
|
t.Errorf("invalid result, expected nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := bytes.Compare(b, tc.expected)
|
||||||
|
if res != 0 {
|
||||||
|
t.Errorf("invalid result, expected: %v, actual: %v", tc.expected, b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ func ExampleMX() {
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetQuestion("miek.nl.", dns.TypeMX)
|
m.SetQuestion("miek.nl.", dns.TypeMX)
|
||||||
m.RecursionDesired = true
|
m.RecursionDesired = true
|
||||||
r, _, err := c.Exchange(m, config.Servers[0]+":"+config.Port)
|
r, _, err := c.Exchange(m, net.JoinHostPort(config.Servers[0], config.Port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ func ExampleDS() {
|
||||||
zone := "miek.nl"
|
zone := "miek.nl"
|
||||||
m.SetQuestion(dns.Fqdn(zone), dns.TypeDNSKEY)
|
m.SetQuestion(dns.Fqdn(zone), dns.TypeDNSKEY)
|
||||||
m.SetEdns0(4096, true)
|
m.SetEdns0(4096, true)
|
||||||
r, _, err := c.Exchange(m, config.Servers[0]+":"+config.Port)
|
r, _, err := c.Exchange(m, net.JoinHostPort(config.Servers[0], config.Port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,7 @@ type APAIR struct {
|
||||||
func NewAPAIR() dns.PrivateRdata { return new(APAIR) }
|
func NewAPAIR() dns.PrivateRdata { return new(APAIR) }
|
||||||
|
|
||||||
func (rd *APAIR) String() string { return rd.addr[0].String() + " " + rd.addr[1].String() }
|
func (rd *APAIR) String() string { return rd.addr[0].String() + " " + rd.addr[1].String() }
|
||||||
|
|
||||||
func (rd *APAIR) Parse(txt []string) error {
|
func (rd *APAIR) Parse(txt []string) error {
|
||||||
if len(txt) != 2 {
|
if len(txt) != 2 {
|
||||||
return errors.New("two addresses required for APAIR")
|
return errors.New("two addresses required for APAIR")
|
||||||
|
@ -121,21 +122,23 @@ func (rd *APAIR) Len() int {
|
||||||
func ExamplePrivateHandle() {
|
func ExamplePrivateHandle() {
|
||||||
dns.PrivateHandle("APAIR", TypeAPAIR, NewAPAIR)
|
dns.PrivateHandle("APAIR", TypeAPAIR, NewAPAIR)
|
||||||
defer dns.PrivateHandleRemove(TypeAPAIR)
|
defer dns.PrivateHandleRemove(TypeAPAIR)
|
||||||
|
var oldId = dns.Id
|
||||||
|
dns.Id = func() uint16 { return 3 }
|
||||||
|
defer func() { dns.Id = oldId }()
|
||||||
|
|
||||||
rr, err := dns.NewRR("miek.nl. APAIR (1.2.3.4 1.2.3.5)")
|
rr, err := dns.NewRR("miek.nl. APAIR (1.2.3.4 1.2.3.5)")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("could not parse APAIR record: ", err)
|
log.Fatal("could not parse APAIR record: ", err)
|
||||||
}
|
}
|
||||||
fmt.Println(rr)
|
fmt.Println(rr) // see first line of Output below
|
||||||
// Output: miek.nl. 3600 IN APAIR 1.2.3.4 1.2.3.5
|
|
||||||
|
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.Id = 12345
|
|
||||||
m.SetQuestion("miek.nl.", TypeAPAIR)
|
m.SetQuestion("miek.nl.", TypeAPAIR)
|
||||||
m.Answer = append(m.Answer, rr)
|
m.Answer = append(m.Answer, rr)
|
||||||
|
|
||||||
fmt.Println(m)
|
fmt.Println(m)
|
||||||
// ;; opcode: QUERY, status: NOERROR, id: 12345
|
// Output: miek.nl. 3600 IN APAIR 1.2.3.4 1.2.3.5
|
||||||
|
// ;; opcode: QUERY, status: NOERROR, id: 3
|
||||||
// ;; flags: rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
|
// ;; flags: rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
|
||||||
//
|
//
|
||||||
// ;; QUESTION SECTION:
|
// ;; QUESTION SECTION:
|
||||||
|
|
12
fuzz.go
12
fuzz.go
|
@ -1,7 +1,10 @@
|
||||||
|
//go:build fuzz
|
||||||
// +build fuzz
|
// +build fuzz
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
func Fuzz(data []byte) int {
|
func Fuzz(data []byte) int {
|
||||||
msg := new(Msg)
|
msg := new(Msg)
|
||||||
|
|
||||||
|
@ -16,7 +19,14 @@ func Fuzz(data []byte) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzNewRR(data []byte) int {
|
func FuzzNewRR(data []byte) int {
|
||||||
if _, err := NewRR(string(data)); err != nil {
|
str := string(data)
|
||||||
|
// Do not fuzz lines that include the $INCLUDE keyword and hint the fuzzer
|
||||||
|
// at avoiding them.
|
||||||
|
// See GH#1025 for context.
|
||||||
|
if strings.Contains(strings.ToUpper(str), "$INCLUDE") {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if _, err := NewRR(str); err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -0,0 +1,269 @@
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestPackDataOpt tests generated using fuzz.go and with a message pack
|
||||||
|
// containing the following bytes:
|
||||||
|
// "0000\x00\x00000000\x00\x00/00000" +
|
||||||
|
// "0\x00\v\x00#\b00000000\x00\x00)000" +
|
||||||
|
// "000\x00\x1c00\x00\x0000\x00\x01000\x00\x00\x00\b" +
|
||||||
|
// "\x00\v\x00\x02\x0000000000"
|
||||||
|
// That bytes sequence created the overflow error.
|
||||||
|
func TestPackDataOpt(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
option []EDNS0
|
||||||
|
msg []byte
|
||||||
|
off int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want int
|
||||||
|
wantErr bool
|
||||||
|
wantErrMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "overflow",
|
||||||
|
args: args{
|
||||||
|
option: []EDNS0{
|
||||||
|
&EDNS0_LOCAL{Code: 0x3030, Data: []uint8{}},
|
||||||
|
&EDNS0_LOCAL{Code: 0x3030, Data: []uint8{0x30}},
|
||||||
|
&EDNS0_LOCAL{Code: 0x3030, Data: []uint8{}},
|
||||||
|
&EDNS0_SUBNET{
|
||||||
|
Code: 0x0, Family: 0x2,
|
||||||
|
SourceNetmask: 0x0, SourceScope: 0x30,
|
||||||
|
Address: net.IP{0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}},
|
||||||
|
},
|
||||||
|
msg: []byte{
|
||||||
|
0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x2,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2f, 0x30,
|
||||||
|
0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x0b, 0x00,
|
||||||
|
0x23, 0x08, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
|
||||||
|
0x30, 0x30, 0x00, 0x00, 0x29, 0x30, 0x30, 0x30,
|
||||||
|
0x30, 0x30, 0x30, 0x00, 0x00, 0x30, 0x30, 0x00,
|
||||||
|
0x00, 0x30, 0x30, 0x00, 0x01, 0x30, 0x00, 0x00,
|
||||||
|
0x00,
|
||||||
|
},
|
||||||
|
off: 54,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrMsg: "dns: overflow packing opt",
|
||||||
|
want: 57,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := packDataOpt(tt.args.option, tt.args.msg, tt.args.off)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("packDataOpt() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil && tt.wantErrMsg != err.Error() {
|
||||||
|
t.Errorf("packDataOpt() error msg = %v, wantErrMsg %v", err.Error(), tt.wantErrMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("packDataOpt() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCrashNSEC tests generated using fuzz.go and with a message pack
|
||||||
|
// containing the following bytes:
|
||||||
|
// "0000\x00\x00000000\x00\x00/00000" +
|
||||||
|
// "0\x00\v\x00#\b00000\x00\x00\x00\x00\x00\x1a000" +
|
||||||
|
// "000\x00\x00\x00\x00\x1a000000\x00\x00\x00\x00\x1a0" +
|
||||||
|
// "00000\x00\v00\a0000000\x00"
|
||||||
|
// That byte sequence, when Unpack() and subsequent Pack() created a
|
||||||
|
// panic: runtime error: slice bounds out of range
|
||||||
|
// which was attributed to the fact that NSEC RR length computation was different (and smaller)
|
||||||
|
// then when within packDataNsec.
|
||||||
|
func TestCrashNSEC(t *testing.T) {
|
||||||
|
compression := make(map[string]struct{})
|
||||||
|
nsec := &NSEC{
|
||||||
|
Hdr: RR_Header{
|
||||||
|
Name: ".",
|
||||||
|
Rrtype: 0x2f,
|
||||||
|
Class: 0x3030,
|
||||||
|
Ttl: 0x30303030,
|
||||||
|
Rdlength: 0xb,
|
||||||
|
},
|
||||||
|
NextDomain: ".",
|
||||||
|
TypeBitMap: []uint16{
|
||||||
|
0x2302, 0x2303, 0x230a, 0x230b,
|
||||||
|
0x2312, 0x2313, 0x231a, 0x231b,
|
||||||
|
0x2322, 0x2323,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedLength := 19
|
||||||
|
l := nsec.len(0, compression)
|
||||||
|
if l != expectedLength {
|
||||||
|
t.Fatalf("expected length of %d, got %d", expectedLength, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCrashNSEC3 tests generated using fuzz.go and with a message pack
|
||||||
|
// containing the following bytes:
|
||||||
|
// "0000\x00\x00000000\x00\x00200000" +
|
||||||
|
// "0\x00\v0000\x00\x00#\x0300\x00\x00\x00\x1a000" +
|
||||||
|
// "000\x00\v00\x0200\x00\x03000\x00"
|
||||||
|
// That byte sequence, when Unpack() and subsequent Pack() created a
|
||||||
|
// panic: runtime error: slice bounds out of range
|
||||||
|
// which was attributed to the fact that NSEC3 RR length computation was
|
||||||
|
// different (and smaller) then within NSEC3.pack (which relies on
|
||||||
|
// packDataNsec).
|
||||||
|
func TestCrashNSEC3(t *testing.T) {
|
||||||
|
compression := make(map[string]struct{})
|
||||||
|
nsec3 := &NSEC3{
|
||||||
|
Hdr: RR_Header{
|
||||||
|
Name: ".",
|
||||||
|
Rrtype: 0x32,
|
||||||
|
Class: 0x3030,
|
||||||
|
Ttl: 0x30303030,
|
||||||
|
Rdlength: 0xb,
|
||||||
|
},
|
||||||
|
Hash: 0x30,
|
||||||
|
Flags: 0x30,
|
||||||
|
Iterations: 0x3030,
|
||||||
|
SaltLength: 0x0,
|
||||||
|
Salt: "",
|
||||||
|
HashLength: 0x0,
|
||||||
|
NextDomain: ".",
|
||||||
|
TypeBitMap: []uint16{
|
||||||
|
0x2302, 0x2303, 0x230a, 0x230b,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedLength := 24
|
||||||
|
l := nsec3.len(0, compression)
|
||||||
|
if l != expectedLength {
|
||||||
|
t.Fatalf("expected length of %d, got %d", expectedLength, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewRRCommentLengthCrasherString test inputs to NewRR that generated crashes.
|
||||||
|
func TestNewRRCommentLengthCrasherString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
|
||||||
|
{
|
||||||
|
"HINFO1", " HINFO ;;;;;;;;;;;;;" +
|
||||||
|
";;;;;;;;\x00\x19;;;;;;;;;;" +
|
||||||
|
";\u007f;;;;;;;;;;;;;;;;;;" +
|
||||||
|
";;}mP_Qq_3sJ_1_84X_5" +
|
||||||
|
"45iW_3K4p8J8_v9_LT3_" +
|
||||||
|
"6_0l_3D4VT3xq6N_3K__" +
|
||||||
|
"_U_xX2m;;;;;;(;;;;;;" +
|
||||||
|
";;;;;;;;;;;;;;;\x1d;;;;" +
|
||||||
|
";;;;;;-0x804dBDe8ba " +
|
||||||
|
"\t \t\tr HINFO \" \t\t\tve" +
|
||||||
|
"k1xH11e__P6_dk1_51bo" +
|
||||||
|
"g8gJK1V_O_v84_Bw4_1_" +
|
||||||
|
"72jQ3_0J3V_S5iYn4h5X" +
|
||||||
|
"R_2n___51J nN_ \t\tm " +
|
||||||
|
"aa_XO4_5\t \t\t \t\tg6b" +
|
||||||
|
"p_KI_1_YWc_K8c2b___A" +
|
||||||
|
"e_Y1m__4Y_R_avy6t08x" +
|
||||||
|
"b5Cp9_7uS_yLa\t\t\t d " +
|
||||||
|
"EKe1Q83vS___ a \t\t " +
|
||||||
|
"\tmP_Qq_3sJ_1_84X_545" +
|
||||||
|
"iW_3K4p8J8_v9_LT3_6_" +
|
||||||
|
"0l_3D4VT3xq6N_3K___U" +
|
||||||
|
"_xX2\"\" \t \t_fL Ogl5" +
|
||||||
|
"_09i_9__3O7C__QMAG2U" +
|
||||||
|
"35IO8RRU6aJ9_6_57_6_" +
|
||||||
|
"b05BMoX5I__4833_____" +
|
||||||
|
"yfD_2_OPs__sqzM_pqQi" +
|
||||||
|
"_\t\t \tN__GuY4_Trath_0" +
|
||||||
|
"yy___cAK_a__0J0q5 L_" +
|
||||||
|
"p63Fzdva_Lb_29V7_R__" +
|
||||||
|
"Go_H2_8m_4__FJM5B_Y5" +
|
||||||
|
"Slw_ghp_55l_X2_Pnt6Y" +
|
||||||
|
"_Wd_hM7jRZ_\t\t \tm \t" +
|
||||||
|
" \t\ta md rK \x00 7_\"sr " +
|
||||||
|
"- sg o -0x804dBDe8b" +
|
||||||
|
"a \t \t\tN_W6J3PBS_W__C" +
|
||||||
|
"yJu__k6F_jY0INI_LC27" +
|
||||||
|
"7x14b_1b___Y8f_K_3y_" +
|
||||||
|
"0055yaP_LKu_72g_T_32" +
|
||||||
|
"iBk1Zm_o 9i1P44_S0_" +
|
||||||
|
"_4AXUpo2__H55tL_g78_" +
|
||||||
|
"8V_8l0yg6bp_KI_1_YWc" +
|
||||||
|
"_K8c2b \t \tmaa_XO4_5" +
|
||||||
|
"rg6bp_KI_1_YWc_K8c2b" +
|
||||||
|
" _C20w i_4 \t\t u_k d" +
|
||||||
|
" rKsg09099 \"\"2335779" +
|
||||||
|
"05047986112651e025 \t" +
|
||||||
|
" \t\tN_W6J3PBS_W__CyJu" +
|
||||||
|
"__k6F_jY0INI_LC277x1" +
|
||||||
|
"4b_1b___Y8f_K_3y_005" +
|
||||||
|
"5yaP_LKu_72g_T_32iBk" +
|
||||||
|
"1Zm_o 9i1P44_S0__4A" +
|
||||||
|
"XUpo2__H55tL_g78_8V_" +
|
||||||
|
"8l0y_9K9_C__6af__wj_" +
|
||||||
|
"UbSYy_ge29S_s_Qe259q" +
|
||||||
|
"_kGod \t\t\t\t :0xb1AF1F" +
|
||||||
|
"b71D2ACeaB3FEce2ssg " +
|
||||||
|
"o dr-0x804dBDe8ba \t " +
|
||||||
|
"\t\t$ Y5 _BzOc6S_Lk0K" +
|
||||||
|
"y43j1TzV__9367tbX56_" +
|
||||||
|
"6B3__q6_v8_4_0_t_2q_" +
|
||||||
|
"nJ2gV3j9_tkOrx_H__a}" +
|
||||||
|
"mT 0g6bp_KI_1_YWc_K8" +
|
||||||
|
"c2b\t_ a\t \t54KM8f9_63" +
|
||||||
|
"zJ2Q_c1_C_Zf4ICF4m0q" +
|
||||||
|
"_RVm_3Zh4vr7yI_H2 a" +
|
||||||
|
" m 0yq__TiqA_FQBv_SS" +
|
||||||
|
"_Hm_8T8__M8F2_53TTo_" +
|
||||||
|
"k_o2__u_W6Vr__524q9l" +
|
||||||
|
"9CQsC_kOU___g_94 \"" +
|
||||||
|
" ~a_j_16_6iUSu_96V1W" +
|
||||||
|
"5r01j____gn157__8_LO" +
|
||||||
|
"0y_08Jr6OR__WF8__JK_" +
|
||||||
|
"N_wx_k_CGB_SjJ9R74i_" +
|
||||||
|
"7_1t_6 m NULLNULLNUL" +
|
||||||
|
"L \t \t\t\t drK\t\x00 7_\"\" 5" +
|
||||||
|
"_5_y732S43__D_8U9FX2" +
|
||||||
|
"27_k\t\tg6bp_KI_1_YWc_" +
|
||||||
|
"K8c2b_J_wx8yw1CMw27j" +
|
||||||
|
"___f_a8uw_ Er9gB_L2 " +
|
||||||
|
"\t\t \t\t\tm aa_XO4_5 Y_" +
|
||||||
|
" I_T7762_zlMi_n8_FjH" +
|
||||||
|
"vy62p__M4S_8__r092af" +
|
||||||
|
"P_T_vhp6__SA_jVF13c5" +
|
||||||
|
"2__8J48K__S4YcjoY91X" +
|
||||||
|
"_iNf06 am aa_XO4_5\t" +
|
||||||
|
" d _ am_SYY4G__2h4QL" +
|
||||||
|
"iUIDd \t\t \tXXp__KFjR" +
|
||||||
|
"V__JU3o\"\" d \t_Iks_ " +
|
||||||
|
"aa_XO4_5<g6bp_KI_1_Y" +
|
||||||
|
"Wc_K8c2b _BzOc6S_Lk0" +
|
||||||
|
"Ky43j1TzV__9367tbX56" +
|
||||||
|
"_6B3__q6_v8_4_0_t_2q" +
|
||||||
|
"_nJ2gV3j9_tkOrx_H__ " +
|
||||||
|
"a\t_Iks_ \\ ma 0_58_r1" +
|
||||||
|
"y8jib_FaV_C_e \t \td\"\"" +
|
||||||
|
" ^Dy_0 \t\t \t ;;;;;;;" +
|
||||||
|
";;;;;;;;;;;",
|
||||||
|
`dns: bad HINFO Fields: "comment length insufficient for parsing" at line: 1:1951`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, err := NewRR(tc.in)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expecting error for crasher line %s", tc.in)
|
||||||
|
}
|
||||||
|
if tc.err != err.Error() {
|
||||||
|
t.Errorf("Expecting error %s, got %s", tc.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
33
generate.go
33
generate.go
|
@ -20,13 +20,13 @@ import (
|
||||||
// of $ after that are interpreted.
|
// of $ after that are interpreted.
|
||||||
func (zp *ZoneParser) generate(l lex) (RR, bool) {
|
func (zp *ZoneParser) generate(l lex) (RR, bool) {
|
||||||
token := l.token
|
token := l.token
|
||||||
step := 1
|
step := int64(1)
|
||||||
if i := strings.IndexByte(token, '/'); i >= 0 {
|
if i := strings.IndexByte(token, '/'); i >= 0 {
|
||||||
if i+1 == len(token) {
|
if i+1 == len(token) {
|
||||||
return zp.setParseError("bad step in $GENERATE range", l)
|
return zp.setParseError("bad step in $GENERATE range", l)
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := strconv.Atoi(token[i+1:])
|
s, err := strconv.ParseInt(token[i+1:], 10, 64)
|
||||||
if err != nil || s <= 0 {
|
if err != nil || s <= 0 {
|
||||||
return zp.setParseError("bad step in $GENERATE range", l)
|
return zp.setParseError("bad step in $GENERATE range", l)
|
||||||
}
|
}
|
||||||
|
@ -40,20 +40,24 @@ func (zp *ZoneParser) generate(l lex) (RR, bool) {
|
||||||
return zp.setParseError("bad start-stop in $GENERATE range", l)
|
return zp.setParseError("bad start-stop in $GENERATE range", l)
|
||||||
}
|
}
|
||||||
|
|
||||||
start, err := strconv.Atoi(sx[0])
|
start, err := strconv.ParseInt(sx[0], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return zp.setParseError("bad start in $GENERATE range", l)
|
return zp.setParseError("bad start in $GENERATE range", l)
|
||||||
}
|
}
|
||||||
|
|
||||||
end, err := strconv.Atoi(sx[1])
|
end, err := strconv.ParseInt(sx[1], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return zp.setParseError("bad stop in $GENERATE range", l)
|
return zp.setParseError("bad stop in $GENERATE range", l)
|
||||||
}
|
}
|
||||||
if end < 0 || start < 0 || end < start {
|
if end < 0 || start < 0 || end < start || (end-start)/step > 65535 {
|
||||||
return zp.setParseError("bad range in $GENERATE range", l)
|
return zp.setParseError("bad range in $GENERATE range", l)
|
||||||
}
|
}
|
||||||
|
|
||||||
zp.c.Next() // _BLANK
|
// _BLANK
|
||||||
|
l, ok := zp.c.Next()
|
||||||
|
if !ok || l.value != zBlank {
|
||||||
|
return zp.setParseError("garbage after $GENERATE range", l)
|
||||||
|
}
|
||||||
|
|
||||||
// Create a complete new string, which we then parse again.
|
// Create a complete new string, which we then parse again.
|
||||||
var s string
|
var s string
|
||||||
|
@ -81,6 +85,7 @@ func (zp *ZoneParser) generate(l lex) (RR, bool) {
|
||||||
}
|
}
|
||||||
zp.sub = NewZoneParser(r, zp.origin, zp.file)
|
zp.sub = NewZoneParser(r, zp.origin, zp.file)
|
||||||
zp.sub.includeDepth, zp.sub.includeAllowed = zp.includeDepth, zp.includeAllowed
|
zp.sub.includeDepth, zp.sub.includeAllowed = zp.includeDepth, zp.includeAllowed
|
||||||
|
zp.sub.generateDisallowed = true
|
||||||
zp.sub.SetDefaultTTL(defaultTtl)
|
zp.sub.SetDefaultTTL(defaultTtl)
|
||||||
return zp.subNext()
|
return zp.subNext()
|
||||||
}
|
}
|
||||||
|
@ -89,10 +94,10 @@ type generateReader struct {
|
||||||
s string
|
s string
|
||||||
si int
|
si int
|
||||||
|
|
||||||
cur int
|
cur int64
|
||||||
start int
|
start int64
|
||||||
end int
|
end int64
|
||||||
step int
|
step int64
|
||||||
|
|
||||||
mod bytes.Buffer
|
mod bytes.Buffer
|
||||||
|
|
||||||
|
@ -168,7 +173,7 @@ func (r *generateReader) ReadByte() (byte, error) {
|
||||||
return '$', nil
|
return '$', nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset int
|
var offset int64
|
||||||
|
|
||||||
// Search for { and }
|
// Search for { and }
|
||||||
if r.s[si+1] == '{' {
|
if r.s[si+1] == '{' {
|
||||||
|
@ -203,7 +208,7 @@ func (r *generateReader) ReadByte() (byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert a $GENERATE modifier 0,0,d to something Printf can deal with.
|
// Convert a $GENERATE modifier 0,0,d to something Printf can deal with.
|
||||||
func modToPrintf(s string) (string, int, string) {
|
func modToPrintf(s string) (string, int64, string) {
|
||||||
// Modifier is { offset [ ,width [ ,base ] ] } - provide default
|
// Modifier is { offset [ ,width [ ,base ] ] } - provide default
|
||||||
// values for optional width and type, if necessary.
|
// values for optional width and type, if necessary.
|
||||||
var offStr, widthStr, base string
|
var offStr, widthStr, base string
|
||||||
|
@ -224,12 +229,12 @@ func modToPrintf(s string) (string, int, string) {
|
||||||
return "", 0, "bad base in $GENERATE"
|
return "", 0, "bad base in $GENERATE"
|
||||||
}
|
}
|
||||||
|
|
||||||
offset, err := strconv.Atoi(offStr)
|
offset, err := strconv.ParseInt(offStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, "bad offset in $GENERATE"
|
return "", 0, "bad offset in $GENERATE"
|
||||||
}
|
}
|
||||||
|
|
||||||
width, err := strconv.Atoi(widthStr)
|
width, err := strconv.ParseInt(widthStr, 10, 64)
|
||||||
if err != nil || width < 0 || width > 255 {
|
if err != nil || width < 0 || width > 255 {
|
||||||
return "", 0, "bad width in $GENERATE"
|
return "", 0, "bad width in $GENERATE"
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,19 +61,24 @@ $GENERATE 0-1/0 dhcp-${0,4,d} A 10.0.0.$
|
||||||
`, true},
|
`, true},
|
||||||
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||||
$GENERATE 0-1 $$INCLUDE ` + tmpdir + string(filepath.Separator) + `${0,4,d}.conf
|
$GENERATE 0-1 $$INCLUDE ` + tmpdir + string(filepath.Separator) + `${0,4,d}.conf
|
||||||
|
`, false},
|
||||||
|
{`@ IN SOA ns.test. hostmaster.test. ( 1 8h 2h 7d 1d )
|
||||||
|
$GENERATE 0-1 dhcp-${0,4,d} A 10.0.0.$
|
||||||
|
$GENERATE 0-2 dhcp-${0,4,d} A 10.1.0.$
|
||||||
`, false},
|
`, false},
|
||||||
}
|
}
|
||||||
Outer:
|
|
||||||
for i := range tests {
|
for i := range tests {
|
||||||
for tok := range ParseZone(strings.NewReader(tests[i].zone), "test.", "test") {
|
z := NewZoneParser(strings.NewReader(tests[i].zone), "test.", "test")
|
||||||
if tok.Error != nil {
|
z.SetIncludeAllowed(true)
|
||||||
if !tests[i].fail {
|
|
||||||
t.Errorf("expected \n\n%s\nto be parsed, but got %v", tests[i].zone, tok.Error)
|
for _, ok := z.Next(); ok; _, ok = z.Next() {
|
||||||
}
|
|
||||||
continue Outer
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if tests[i].fail {
|
|
||||||
|
err := z.Err()
|
||||||
|
if err != nil && !tests[i].fail {
|
||||||
|
t.Errorf("expected \n\n%s\nto be parsed, but got %v", tests[i].zone, err)
|
||||||
|
} else if err == nil && tests[i].fail {
|
||||||
t.Errorf("expected \n\n%s\nto fail, but got no error", tests[i].zone)
|
t.Errorf("expected \n\n%s\nto fail, but got no error", tests[i].zone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,7 +162,7 @@ func TestGenerateModToPrintf(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
mod string
|
mod string
|
||||||
wantFmt string
|
wantFmt string
|
||||||
wantOffset int
|
wantOffset int64
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"0,0,d", "%d", 0, false},
|
{"0,0,d", "%d", 0, false},
|
||||||
|
@ -204,3 +209,27 @@ $GENERATE 32-158 dhcp-${-32,4,d} A 10.0.0.$
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCrasherString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
in string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{"$GENERATE 0-300103\"$$GENERATE 2-2", "bad range in $GENERATE"},
|
||||||
|
{"$GENERATE 0-5414137360", "bad range in $GENERATE"},
|
||||||
|
{"$GENERATE 11522-3668518066406258", "bad range in $GENERATE"},
|
||||||
|
{"$GENERATE 0-200\"(;00000000000000\n$$GENERATE 0-0", "dns: garbage after $GENERATE range: \"\\\"\" at line: 1:16"},
|
||||||
|
{"$GENERATE 6-2048 $$GENERATE 6-036160 $$$$ORIGIN \\$", `dns: nested $GENERATE directive not allowed: "6-036160" at line: 1:19`},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.in, func(t *testing.T) {
|
||||||
|
_, err := NewRR(tc.in)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expecting error for crasher line %s", tc.in)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tc.err) {
|
||||||
|
t.Errorf("Expecting error %s, got %s", tc.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
module github.com/miekg/dns
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
||||||
|
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2
|
||||||
|
)
|
|
@ -0,0 +1,33 @@
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||||
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
|
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8=
|
||||||
|
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
|
||||||
|
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
@ -0,0 +1,31 @@
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// identityHash will not hash, it only buffers the data written into it and returns it as-is.
|
||||||
|
type identityHash struct {
|
||||||
|
b *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the hash.Hash interface.
|
||||||
|
|
||||||
|
func (i identityHash) Write(b []byte) (int, error) { return i.b.Write(b) }
|
||||||
|
func (i identityHash) Size() int { return i.b.Len() }
|
||||||
|
func (i identityHash) BlockSize() int { return 1024 }
|
||||||
|
func (i identityHash) Reset() { i.b.Reset() }
|
||||||
|
func (i identityHash) Sum(b []byte) []byte { return append(b, i.b.Bytes()...) }
|
||||||
|
|
||||||
|
func hashFromAlgorithm(alg uint8) (hash.Hash, crypto.Hash, error) {
|
||||||
|
hashnumber, ok := AlgorithmToHash[alg]
|
||||||
|
if !ok {
|
||||||
|
return nil, 0, ErrAlg
|
||||||
|
}
|
||||||
|
if hashnumber == 0 {
|
||||||
|
return identityHash{b: &bytes.Buffer{}}, hashnumber, nil
|
||||||
|
}
|
||||||
|
return hashnumber.New(), hashnumber, nil
|
||||||
|
}
|
|
@ -7,24 +7,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTCPRtt(t *testing.T) {
|
|
||||||
m := new(Msg)
|
|
||||||
m.RecursionDesired = true
|
|
||||||
m.SetQuestion("example.org.", TypeA)
|
|
||||||
|
|
||||||
c := &Client{}
|
|
||||||
for _, proto := range []string{"udp", "tcp"} {
|
|
||||||
c.Net = proto
|
|
||||||
_, rtt, err := c.Exchange(m, "8.8.4.4:53")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if rtt == 0 {
|
|
||||||
t.Fatalf("expecting non zero rtt %s, got zero", c.Net)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNSEC3MissingSalt(t *testing.T) {
|
func TestNSEC3MissingSalt(t *testing.T) {
|
||||||
rr := testRR("ji6neoaepv8b5o6k4ev33abha8ht9fgc.example. NSEC3 1 1 12 aabbccdd K8UDEMVP1J2F7EG6JEBPS17VP3N8I58H")
|
rr := testRR("ji6neoaepv8b5o6k4ev33abha8ht9fgc.example. NSEC3 1 1 12 aabbccdd K8UDEMVP1J2F7EG6JEBPS17VP3N8I58H")
|
||||||
m := new(Msg)
|
m := new(Msg)
|
||||||
|
|
66
labels.go
66
labels.go
|
@ -10,7 +10,7 @@ package dns
|
||||||
// escaped dots (\.) for instance.
|
// escaped dots (\.) for instance.
|
||||||
// s must be a syntactically valid domain name, see IsDomainName.
|
// s must be a syntactically valid domain name, see IsDomainName.
|
||||||
func SplitDomainName(s string) (labels []string) {
|
func SplitDomainName(s string) (labels []string) {
|
||||||
if len(s) == 0 {
|
if s == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fqdnEnd := 0 // offset of the final '.' or the length of the name
|
fqdnEnd := 0 // offset of the final '.' or the length of the name
|
||||||
|
@ -83,7 +83,7 @@ func CompareDomainName(s1, s2 string) (n int) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountLabel counts the the number of labels in the string s.
|
// CountLabel counts the number of labels in the string s.
|
||||||
// s must be a syntactically valid domain name.
|
// s must be a syntactically valid domain name.
|
||||||
func CountLabel(s string) (labels int) {
|
func CountLabel(s string) (labels int) {
|
||||||
if s == "." {
|
if s == "." {
|
||||||
|
@ -122,24 +122,27 @@ func Split(s string) []int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NextLabel returns the index of the start of the next label in the
|
// NextLabel returns the index of the start of the next label in the
|
||||||
// string s starting at offset.
|
// string s starting at offset. A negative offset will cause a panic.
|
||||||
// The bool end is true when the end of the string has been reached.
|
// The bool end is true when the end of the string has been reached.
|
||||||
// Also see PrevLabel.
|
// Also see PrevLabel.
|
||||||
func NextLabel(s string, offset int) (i int, end bool) {
|
func NextLabel(s string, offset int) (i int, end bool) {
|
||||||
quote := false
|
if s == "" {
|
||||||
|
return 0, true
|
||||||
|
}
|
||||||
for i = offset; i < len(s)-1; i++ {
|
for i = offset; i < len(s)-1; i++ {
|
||||||
switch s[i] {
|
if s[i] != '.' {
|
||||||
case '\\':
|
continue
|
||||||
quote = !quote
|
|
||||||
default:
|
|
||||||
quote = false
|
|
||||||
case '.':
|
|
||||||
if quote {
|
|
||||||
quote = !quote
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return i + 1, false
|
|
||||||
}
|
}
|
||||||
|
j := i - 1
|
||||||
|
for j >= 0 && s[j] == '\\' {
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j-i)%2 == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return i + 1, false
|
||||||
}
|
}
|
||||||
return i + 1, true
|
return i + 1, true
|
||||||
}
|
}
|
||||||
|
@ -149,17 +152,38 @@ func NextLabel(s string, offset int) (i int, end bool) {
|
||||||
// The bool start is true when the start of the string has been overshot.
|
// The bool start is true when the start of the string has been overshot.
|
||||||
// Also see NextLabel.
|
// Also see NextLabel.
|
||||||
func PrevLabel(s string, n int) (i int, start bool) {
|
func PrevLabel(s string, n int) (i int, start bool) {
|
||||||
|
if s == "" {
|
||||||
|
return 0, true
|
||||||
|
}
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return len(s), false
|
return len(s), false
|
||||||
}
|
}
|
||||||
lab := Split(s)
|
|
||||||
if lab == nil {
|
l := len(s) - 1
|
||||||
return 0, true
|
if s[l] == '.' {
|
||||||
|
l--
|
||||||
}
|
}
|
||||||
if n > len(lab) {
|
|
||||||
return 0, true
|
for ; l >= 0 && n > 0; l-- {
|
||||||
|
if s[l] != '.' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
j := l - 1
|
||||||
|
for j >= 0 && s[j] == '\\' {
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j-l)%2 == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
n--
|
||||||
|
if n == 0 {
|
||||||
|
return l + 1, false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return lab[len(lab)-n], false
|
|
||||||
|
return 0, n > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// equal compares a and b while ignoring case. It returns true when equal otherwise false.
|
// equal compares a and b while ignoring case. It returns true when equal otherwise false.
|
||||||
|
|
124
labels_test.go
124
labels_test.go
|
@ -40,16 +40,17 @@ func TestCompareDomainName(t *testing.T) {
|
||||||
|
|
||||||
func TestSplit(t *testing.T) {
|
func TestSplit(t *testing.T) {
|
||||||
splitter := map[string]int{
|
splitter := map[string]int{
|
||||||
"www.miek.nl.": 3,
|
"www.miek.nl.": 3,
|
||||||
"www.miek.nl": 3,
|
"www.miek.nl": 3,
|
||||||
"www..miek.nl": 4,
|
"www..miek.nl": 4,
|
||||||
`www\.miek.nl.`: 2,
|
`www\.miek.nl.`: 2,
|
||||||
`www\\.miek.nl.`: 3,
|
`www\\.miek.nl.`: 3,
|
||||||
".": 0,
|
`www\\\.miek.nl.`: 2,
|
||||||
"nl.": 1,
|
".": 0,
|
||||||
"nl": 1,
|
"nl.": 1,
|
||||||
"com.": 1,
|
"nl": 1,
|
||||||
".com.": 2,
|
"com.": 1,
|
||||||
|
".com.": 2,
|
||||||
}
|
}
|
||||||
for s, i := range splitter {
|
for s, i := range splitter {
|
||||||
if x := len(Split(s)); x != i {
|
if x := len(Split(s)); x != i {
|
||||||
|
@ -79,12 +80,32 @@ func TestSplit2(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNextLabel(t *testing.T) {
|
||||||
|
type next struct {
|
||||||
|
string
|
||||||
|
int
|
||||||
|
}
|
||||||
|
nexts := map[next]int{
|
||||||
|
{"", 1}: 0,
|
||||||
|
{"www.miek.nl.", 0}: 4,
|
||||||
|
{"www.miek.nl.", 4}: 9,
|
||||||
|
{"www.miek.nl.", 9}: 12,
|
||||||
|
}
|
||||||
|
for s, i := range nexts {
|
||||||
|
x, ok := NextLabel(s.string, s.int)
|
||||||
|
if i != x {
|
||||||
|
t.Errorf("label should be %d, got %d, %t: next %d, %s", i, x, ok, s.int, s.string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPrevLabel(t *testing.T) {
|
func TestPrevLabel(t *testing.T) {
|
||||||
type prev struct {
|
type prev struct {
|
||||||
string
|
string
|
||||||
int
|
int
|
||||||
}
|
}
|
||||||
prever := map[prev]int{
|
prever := map[prev]int{
|
||||||
|
{"", 1}: 0,
|
||||||
{"www.miek.nl.", 0}: 12,
|
{"www.miek.nl.", 0}: 12,
|
||||||
{"www.miek.nl.", 1}: 9,
|
{"www.miek.nl.", 1}: 9,
|
||||||
{"www.miek.nl.", 2}: 4,
|
{"www.miek.nl.", 2}: 4,
|
||||||
|
@ -102,7 +123,7 @@ func TestPrevLabel(t *testing.T) {
|
||||||
for s, i := range prever {
|
for s, i := range prever {
|
||||||
x, ok := PrevLabel(s.string, s.int)
|
x, ok := PrevLabel(s.string, s.int)
|
||||||
if i != x {
|
if i != x {
|
||||||
t.Errorf("label should be %d, got %d, %t: preving %d, %s", i, x, ok, s.int, s.string)
|
t.Errorf("label should be %d, got %d, %t: previous %d, %s", i, x, ok, s.int, s.string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,7 +176,10 @@ func TestIsDomainName(t *testing.T) {
|
||||||
lab int
|
lab int
|
||||||
}
|
}
|
||||||
names := map[string]*ret{
|
names := map[string]*ret{
|
||||||
"..": {false, 1},
|
".": {true, 1},
|
||||||
|
"..": {false, 0},
|
||||||
|
"double-dot..test": {false, 1},
|
||||||
|
".leading-dot.test": {false, 0},
|
||||||
"@.": {true, 1},
|
"@.": {true, 1},
|
||||||
"www.example.com": {true, 3},
|
"www.example.com": {true, 3},
|
||||||
"www.e%ample.com": {true, 3},
|
"www.e%ample.com": {true, 3},
|
||||||
|
@ -210,6 +234,22 @@ func TestIsFqdnEscaped(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCanonicalName(t *testing.T) {
|
||||||
|
for s, expect := range map[string]string{
|
||||||
|
"": ".",
|
||||||
|
".": ".",
|
||||||
|
"tld": "tld.",
|
||||||
|
"tld.": "tld.",
|
||||||
|
"example.test": "example.test.",
|
||||||
|
"Lower.CASE.test.": "lower.case.test.",
|
||||||
|
"*.Test": "*.test.",
|
||||||
|
} {
|
||||||
|
if got := CanonicalName(s); got != expect {
|
||||||
|
t.Errorf("CanonicalName(%q) = %q, expected %q", s, got, expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkSplitLabels(b *testing.B) {
|
func BenchmarkSplitLabels(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
Split("www.example.com.")
|
Split("www.example.com.")
|
||||||
|
@ -237,3 +277,63 @@ func BenchmarkIsSubDomain(b *testing.B) {
|
||||||
IsSubDomain("miek.nl.", "aa.example.com.")
|
IsSubDomain("miek.nl.", "aa.example.com.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkNextLabelSimple(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
NextLabel("www.example.com", 0)
|
||||||
|
NextLabel("www.example.com", 5)
|
||||||
|
NextLabel("www.example.com", 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPrevLabelSimple(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
PrevLabel("www.example.com", 0)
|
||||||
|
PrevLabel("www.example.com", 5)
|
||||||
|
PrevLabel("www.example.com", 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNextLabelComplex(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
NextLabel(`www\.example.com`, 0)
|
||||||
|
NextLabel(`www\\.example.com`, 0)
|
||||||
|
NextLabel(`www\\\.example.com`, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPrevLabelComplex(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
PrevLabel(`www\.example.com`, 10)
|
||||||
|
PrevLabel(`www\\.example.com`, 10)
|
||||||
|
PrevLabel(`www\\\.example.com`, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkNextLabelMixed(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
NextLabel("www.example.com", 0)
|
||||||
|
NextLabel(`www\.example.com`, 0)
|
||||||
|
NextLabel("www.example.com", 5)
|
||||||
|
NextLabel(`www\\.example.com`, 0)
|
||||||
|
NextLabel("www.example.com", 12)
|
||||||
|
NextLabel(`www\\\.example.com`, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPrevLabelMixed(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
PrevLabel("www.example.com", 0)
|
||||||
|
PrevLabel(`www\.example.com`, 10)
|
||||||
|
PrevLabel("www.example.com", 5)
|
||||||
|
PrevLabel(`www\\.example.com`, 10)
|
||||||
|
PrevLabel("www.example.com", 12)
|
||||||
|
PrevLabel(`www\\\.example.com`, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -396,10 +396,19 @@ func TestMsgCompressLengthLargeRecordsWithPaddingPermutation(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMsgCompressLengthLargeRecordsAllValues(t *testing.T) {
|
func TestMsgCompressLengthLargeRecordsAllValues(t *testing.T) {
|
||||||
|
// we want to cross the 14 (16384) bit boundary here, so we build it up to just below and go slightly over.
|
||||||
msg := new(Msg)
|
msg := new(Msg)
|
||||||
msg.Compress = true
|
msg.Compress = true
|
||||||
msg.SetQuestion("redis.service.consul.", TypeSRV)
|
msg.SetQuestion("redis.service.consul.", TypeSRV)
|
||||||
for i := 0; i < 900; i++ {
|
for i := 0; i < 170; i++ {
|
||||||
|
target := fmt.Sprintf("host-redis-%d-%d.test.acme.com.node.dc1.consul.", i/256, i%256)
|
||||||
|
msg.Answer = append(msg.Answer, &SRV{Hdr: RR_Header{Name: "redis.service.consul.", Class: 1, Rrtype: TypeSRV, Ttl: 0x3c}, Port: 0x4c57, Target: target})
|
||||||
|
msg.Extra = append(msg.Extra, &CNAME{Hdr: RR_Header{Name: target, Class: ClassINET, Rrtype: TypeCNAME, Ttl: 0x3c}, Target: fmt.Sprintf("fx.168.%d.%d.", i/256, i%256)})
|
||||||
|
}
|
||||||
|
// msg.Len() == 15458
|
||||||
|
// msg.Len() == 16470 at 180
|
||||||
|
|
||||||
|
for i := 170; i < 181; i++ {
|
||||||
target := fmt.Sprintf("host-redis-%d-%d.test.acme.com.node.dc1.consul.", i/256, i%256)
|
target := fmt.Sprintf("host-redis-%d-%d.test.acme.com.node.dc1.consul.", i/256, i%256)
|
||||||
msg.Answer = append(msg.Answer, &SRV{Hdr: RR_Header{Name: "redis.service.consul.", Class: 1, Rrtype: TypeSRV, Ttl: 0x3c}, Port: 0x4c57, Target: target})
|
msg.Answer = append(msg.Answer, &SRV{Hdr: RR_Header{Name: "redis.service.consul.", Class: 1, Rrtype: TypeSRV, Ttl: 0x3c}, Port: 0x4c57, Target: target})
|
||||||
msg.Extra = append(msg.Extra, &CNAME{Hdr: RR_Header{Name: target, Class: ClassINET, Rrtype: TypeCNAME, Ttl: 0x3c}, Target: fmt.Sprintf("fx.168.%d.%d.", i/256, i%256)})
|
msg.Extra = append(msg.Extra, &CNAME{Hdr: RR_Header{Name: target, Class: ClassINET, Rrtype: TypeCNAME, Ttl: 0x3c}, Target: fmt.Sprintf("fx.168.%d.%d.", i/256, i%256)})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !go1.11 || (!aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd)
|
||||||
// +build !go1.11 !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd
|
// +build !go1.11 !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd
|
||||||
|
|
||||||
package dns
|
package dns
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build go1.11 && (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd)
|
||||||
// +build go1.11
|
// +build go1.11
|
||||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd
|
// +build aix darwin dragonfly freebsd linux netbsd openbsd
|
||||||
|
|
99
msg.go
99
msg.go
|
@ -11,14 +11,12 @@ package dns
|
||||||
//go:generate go run msg_generate.go
|
//go:generate go run msg_generate.go
|
||||||
|
|
||||||
import (
|
import (
|
||||||
crand "crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"math/rand"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -73,53 +71,23 @@ var (
|
||||||
ErrTime error = &Error{err: "bad time"} // ErrTime indicates a timing error in TSIG authentication.
|
ErrTime error = &Error{err: "bad time"} // ErrTime indicates a timing error in TSIG authentication.
|
||||||
)
|
)
|
||||||
|
|
||||||
// Id by default, returns a 16 bits random number to be used as a
|
// Id by default returns a 16-bit random number to be used as a message id. The
|
||||||
// message id. The random provided should be good enough. This being a
|
// number is drawn from a cryptographically secure random number generator.
|
||||||
// variable the function can be reassigned to a custom function.
|
// This being a variable the function can be reassigned to a custom function.
|
||||||
// For instance, to make it return a static value:
|
// For instance, to make it return a static value for testing:
|
||||||
//
|
//
|
||||||
// dns.Id = func() uint16 { return 3 }
|
// dns.Id = func() uint16 { return 3 }
|
||||||
var Id = id
|
var Id = id
|
||||||
|
|
||||||
var (
|
|
||||||
idLock sync.Mutex
|
|
||||||
idRand *rand.Rand
|
|
||||||
)
|
|
||||||
|
|
||||||
// id returns a 16 bits random number to be used as a
|
// id returns a 16 bits random number to be used as a
|
||||||
// message id. The random provided should be good enough.
|
// message id. The random provided should be good enough.
|
||||||
func id() uint16 {
|
func id() uint16 {
|
||||||
idLock.Lock()
|
var output uint16
|
||||||
|
err := binary.Read(rand.Reader, binary.BigEndian, &output)
|
||||||
if idRand == nil {
|
if err != nil {
|
||||||
// This (partially) works around
|
panic("dns: reading random id failed: " + err.Error())
|
||||||
// https://github.com/golang/go/issues/11833 by only
|
|
||||||
// seeding idRand upon the first call to id.
|
|
||||||
|
|
||||||
var seed int64
|
|
||||||
var buf [8]byte
|
|
||||||
|
|
||||||
if _, err := crand.Read(buf[:]); err == nil {
|
|
||||||
seed = int64(binary.LittleEndian.Uint64(buf[:]))
|
|
||||||
} else {
|
|
||||||
seed = rand.Int63()
|
|
||||||
}
|
|
||||||
|
|
||||||
idRand = rand.New(rand.NewSource(seed))
|
|
||||||
}
|
}
|
||||||
|
return output
|
||||||
// The call to idRand.Uint32 must be within the
|
|
||||||
// mutex lock because *rand.Rand is not safe for
|
|
||||||
// concurrent use.
|
|
||||||
//
|
|
||||||
// There is no added performance overhead to calling
|
|
||||||
// idRand.Uint32 inside a mutex lock over just
|
|
||||||
// calling rand.Uint32 as the global math/rand rng
|
|
||||||
// is internally protected by a sync.Mutex.
|
|
||||||
id := uint16(idRand.Uint32())
|
|
||||||
|
|
||||||
idLock.Unlock()
|
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MsgHdr is a a manually-unpacked version of (id, bits).
|
// MsgHdr is a a manually-unpacked version of (id, bits).
|
||||||
|
@ -297,6 +265,11 @@ loop:
|
||||||
|
|
||||||
wasDot = false
|
wasDot = false
|
||||||
case '.':
|
case '.':
|
||||||
|
if i == 0 && len(s) > 1 {
|
||||||
|
// leading dots are not legal except for the root zone
|
||||||
|
return len(msg), ErrRdata
|
||||||
|
}
|
||||||
|
|
||||||
if wasDot {
|
if wasDot {
|
||||||
// two dots back to back is not legal
|
// two dots back to back is not legal
|
||||||
return len(msg), ErrRdata
|
return len(msg), ErrRdata
|
||||||
|
@ -430,17 +403,12 @@ Loop:
|
||||||
return "", lenmsg, ErrLongDomain
|
return "", lenmsg, ErrLongDomain
|
||||||
}
|
}
|
||||||
for _, b := range msg[off : off+c] {
|
for _, b := range msg[off : off+c] {
|
||||||
switch b {
|
if isDomainNameLabelSpecial(b) {
|
||||||
case '.', '(', ')', ';', ' ', '@':
|
|
||||||
fallthrough
|
|
||||||
case '"', '\\':
|
|
||||||
s = append(s, '\\', b)
|
s = append(s, '\\', b)
|
||||||
default:
|
} else if b < ' ' || b > '~' {
|
||||||
if b < ' ' || b > '~' { // unprintable, use \DDD
|
s = append(s, escapeByte(b)...)
|
||||||
s = append(s, escapeByte(b)...)
|
} else {
|
||||||
} else {
|
s = append(s, b)
|
||||||
s = append(s, b)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s = append(s, '.')
|
s = append(s, '.')
|
||||||
|
@ -661,11 +629,18 @@ func UnpackRRWithHeader(h RR_Header, msg []byte, off int) (rr RR, off1 int, err
|
||||||
rr = &RFC3597{Hdr: h}
|
rr = &RFC3597{Hdr: h}
|
||||||
}
|
}
|
||||||
|
|
||||||
if noRdata(h) {
|
if off < 0 || off > len(msg) {
|
||||||
return rr, off, nil
|
return &h, off, &Error{err: "bad off"}
|
||||||
}
|
}
|
||||||
|
|
||||||
end := off + int(h.Rdlength)
|
end := off + int(h.Rdlength)
|
||||||
|
if end < off || end > len(msg) {
|
||||||
|
return &h, end, &Error{err: "bad rdlength"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if noRdata(h) {
|
||||||
|
return rr, off, nil
|
||||||
|
}
|
||||||
|
|
||||||
off, err = rr.unpack(msg, off)
|
off, err = rr.unpack(msg, off)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -693,7 +668,6 @@ func unpackRRslice(l int, msg []byte, off int) (dst1 []RR, off1 int, err error)
|
||||||
}
|
}
|
||||||
// If offset does not increase anymore, l is a lie
|
// If offset does not increase anymore, l is a lie
|
||||||
if off1 == off {
|
if off1 == off {
|
||||||
l = i
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
dst = append(dst, r)
|
dst = append(dst, r)
|
||||||
|
@ -706,9 +680,9 @@ func unpackRRslice(l int, msg []byte, off int) (dst1 []RR, off1 int, err error)
|
||||||
|
|
||||||
// Convert a MsgHdr to a string, with dig-like headers:
|
// Convert a MsgHdr to a string, with dig-like headers:
|
||||||
//
|
//
|
||||||
//;; opcode: QUERY, status: NOERROR, id: 48404
|
// ;; opcode: QUERY, status: NOERROR, id: 48404
|
||||||
//
|
//
|
||||||
//;; flags: qr aa rd ra;
|
// ;; flags: qr aa rd ra;
|
||||||
func (h *MsgHdr) String() string {
|
func (h *MsgHdr) String() string {
|
||||||
if h == nil {
|
if h == nil {
|
||||||
return "<nil> MsgHdr"
|
return "<nil> MsgHdr"
|
||||||
|
@ -773,7 +747,7 @@ func (dns *Msg) packBufferWithCompressionMap(buf []byte, compression compression
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set extended rcode unconditionally if we have an opt, this will allow
|
// Set extended rcode unconditionally if we have an opt, this will allow
|
||||||
// reseting the extended rcode bits if they need to.
|
// resetting the extended rcode bits if they need to.
|
||||||
if opt := dns.IsEdns0(); opt != nil {
|
if opt := dns.IsEdns0(); opt != nil {
|
||||||
opt.SetExtendedRcode(uint16(dns.Rcode))
|
opt.SetExtendedRcode(uint16(dns.Rcode))
|
||||||
} else if dns.Rcode > 0xF {
|
} else if dns.Rcode > 0xF {
|
||||||
|
@ -932,6 +906,11 @@ func (dns *Msg) String() string {
|
||||||
s += "ANSWER: " + strconv.Itoa(len(dns.Answer)) + ", "
|
s += "ANSWER: " + strconv.Itoa(len(dns.Answer)) + ", "
|
||||||
s += "AUTHORITY: " + strconv.Itoa(len(dns.Ns)) + ", "
|
s += "AUTHORITY: " + strconv.Itoa(len(dns.Ns)) + ", "
|
||||||
s += "ADDITIONAL: " + strconv.Itoa(len(dns.Extra)) + "\n"
|
s += "ADDITIONAL: " + strconv.Itoa(len(dns.Extra)) + "\n"
|
||||||
|
opt := dns.IsEdns0()
|
||||||
|
if opt != nil {
|
||||||
|
// OPT PSEUDOSECTION
|
||||||
|
s += opt.String() + "\n"
|
||||||
|
}
|
||||||
if len(dns.Question) > 0 {
|
if len(dns.Question) > 0 {
|
||||||
s += "\n;; QUESTION SECTION:\n"
|
s += "\n;; QUESTION SECTION:\n"
|
||||||
for _, r := range dns.Question {
|
for _, r := range dns.Question {
|
||||||
|
@ -954,10 +933,10 @@ func (dns *Msg) String() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(dns.Extra) > 0 {
|
if len(dns.Extra) > 0 && (opt == nil || len(dns.Extra) > 1) {
|
||||||
s += "\n;; ADDITIONAL SECTION:\n"
|
s += "\n;; ADDITIONAL SECTION:\n"
|
||||||
for _, r := range dns.Extra {
|
for _, r := range dns.Extra {
|
||||||
if r != nil {
|
if r != nil && r.Header().Rrtype != TypeOPT {
|
||||||
s += r.String() + "\n"
|
s += r.String() + "\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
//+build ignore
|
//go:build ignore
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
// msg_generate.go is meant to run with go generate. It will use
|
// msg_generate.go is meant to run with go generate. It will use
|
||||||
// go/{importer,types} to track down all the RR struct types. Then for each type
|
// go/{importer,types} to track down all the RR struct types. Then for each type
|
||||||
|
@ -10,11 +11,12 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
"go/importer"
|
|
||||||
"go/types"
|
"go/types"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
)
|
)
|
||||||
|
|
||||||
var packageHdr = `
|
var packageHdr = `
|
||||||
|
@ -34,6 +36,9 @@ func getTypeStruct(t types.Type, scope *types.Scope) (*types.Struct, bool) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
if st.NumFields() == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
if st.Field(0).Type() == scope.Lookup("RR_Header").Type() {
|
if st.Field(0).Type() == scope.Lookup("RR_Header").Type() {
|
||||||
return st, false
|
return st, false
|
||||||
}
|
}
|
||||||
|
@ -44,9 +49,19 @@ func getTypeStruct(t types.Type, scope *types.Scope) (*types.Struct, bool) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadModule retrieves package description for a given module.
|
||||||
|
func loadModule(name string) (*types.Package, error) {
|
||||||
|
conf := packages.Config{Mode: packages.NeedTypes | packages.NeedTypesInfo}
|
||||||
|
pkgs, err := packages.Load(&conf, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pkgs[0].Types, nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Import and type-check the package
|
// Import and type-check the package
|
||||||
pkg, err := importer.Default().Import("github.com/miekg/dns")
|
pkg, err := loadModule("github.com/miekg/dns")
|
||||||
fatalIfErr(err)
|
fatalIfErr(err)
|
||||||
scope := pkg.Scope()
|
scope := pkg.Scope()
|
||||||
|
|
||||||
|
@ -99,8 +114,12 @@ return off, err
|
||||||
o("off, err = packDataOpt(rr.%s, msg, off)\n")
|
o("off, err = packDataOpt(rr.%s, msg, off)\n")
|
||||||
case `dns:"nsec"`:
|
case `dns:"nsec"`:
|
||||||
o("off, err = packDataNsec(rr.%s, msg, off)\n")
|
o("off, err = packDataNsec(rr.%s, msg, off)\n")
|
||||||
|
case `dns:"pairs"`:
|
||||||
|
o("off, err = packDataSVCB(rr.%s, msg, off)\n")
|
||||||
case `dns:"domain-name"`:
|
case `dns:"domain-name"`:
|
||||||
o("off, err = packDataDomainNames(rr.%s, msg, off, compression, false)\n")
|
o("off, err = packDataDomainNames(rr.%s, msg, off, compression, false)\n")
|
||||||
|
case `dns:"apl"`:
|
||||||
|
o("off, err = packDataApl(rr.%s, msg, off)\n")
|
||||||
default:
|
default:
|
||||||
log.Fatalln(name, st.Field(i).Name(), st.Tag(i))
|
log.Fatalln(name, st.Field(i).Name(), st.Tag(i))
|
||||||
}
|
}
|
||||||
|
@ -223,8 +242,12 @@ return off, err
|
||||||
o("rr.%s, off, err = unpackDataOpt(msg, off)\n")
|
o("rr.%s, off, err = unpackDataOpt(msg, off)\n")
|
||||||
case `dns:"nsec"`:
|
case `dns:"nsec"`:
|
||||||
o("rr.%s, off, err = unpackDataNsec(msg, off)\n")
|
o("rr.%s, off, err = unpackDataNsec(msg, off)\n")
|
||||||
|
case `dns:"pairs"`:
|
||||||
|
o("rr.%s, off, err = unpackDataSVCB(msg, off)\n")
|
||||||
case `dns:"domain-name"`:
|
case `dns:"domain-name"`:
|
||||||
o("rr.%s, off, err = unpackDataDomainNames(msg, off, rdStart + int(rr.Hdr.Rdlength))\n")
|
o("rr.%s, off, err = unpackDataDomainNames(msg, off, rdStart + int(rr.Hdr.Rdlength))\n")
|
||||||
|
case `dns:"apl"`:
|
||||||
|
o("rr.%s, off, err = unpackDataApl(msg, off)\n")
|
||||||
default:
|
default:
|
||||||
log.Fatalln(name, st.Field(i).Name(), st.Tag(i))
|
log.Fatalln(name, st.Field(i).Name(), st.Tag(i))
|
||||||
}
|
}
|
||||||
|
|
357
msg_helpers.go
357
msg_helpers.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"net"
|
"net"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,12 +26,13 @@ func unpackDataA(msg []byte, off int) (net.IP, int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func packDataA(a net.IP, msg []byte, off int) (int, error) {
|
func packDataA(a net.IP, msg []byte, off int) (int, error) {
|
||||||
// It must be a slice of 4, even if it is 16, we encode only the first 4
|
|
||||||
if off+net.IPv4len > len(msg) {
|
|
||||||
return len(msg), &Error{err: "overflow packing a"}
|
|
||||||
}
|
|
||||||
switch len(a) {
|
switch len(a) {
|
||||||
case net.IPv4len, net.IPv6len:
|
case net.IPv4len, net.IPv6len:
|
||||||
|
// It must be a slice of 4, even if it is 16, we encode only the first 4
|
||||||
|
if off+net.IPv4len > len(msg) {
|
||||||
|
return len(msg), &Error{err: "overflow packing a"}
|
||||||
|
}
|
||||||
|
|
||||||
copy(msg[off:], a.To4())
|
copy(msg[off:], a.To4())
|
||||||
off += net.IPv4len
|
off += net.IPv4len
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -51,12 +53,12 @@ func unpackDataAAAA(msg []byte, off int) (net.IP, int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func packDataAAAA(aaaa net.IP, msg []byte, off int) (int, error) {
|
func packDataAAAA(aaaa net.IP, msg []byte, off int) (int, error) {
|
||||||
if off+net.IPv6len > len(msg) {
|
|
||||||
return len(msg), &Error{err: "overflow packing aaaa"}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(aaaa) {
|
switch len(aaaa) {
|
||||||
case net.IPv6len:
|
case net.IPv6len:
|
||||||
|
if off+net.IPv6len > len(msg) {
|
||||||
|
return len(msg), &Error{err: "overflow packing aaaa"}
|
||||||
|
}
|
||||||
|
|
||||||
copy(msg[off:], aaaa)
|
copy(msg[off:], aaaa)
|
||||||
off += net.IPv6len
|
off += net.IPv6len
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -264,24 +266,36 @@ func unpackString(msg []byte, off int) (string, int, error) {
|
||||||
return "", off, &Error{err: "overflow unpacking txt"}
|
return "", off, &Error{err: "overflow unpacking txt"}
|
||||||
}
|
}
|
||||||
l := int(msg[off])
|
l := int(msg[off])
|
||||||
if off+l+1 > len(msg) {
|
off++
|
||||||
|
if off+l > len(msg) {
|
||||||
return "", off, &Error{err: "overflow unpacking txt"}
|
return "", off, &Error{err: "overflow unpacking txt"}
|
||||||
}
|
}
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
s.Grow(l)
|
consumed := 0
|
||||||
for _, b := range msg[off+1 : off+1+l] {
|
for i, b := range msg[off : off+l] {
|
||||||
switch {
|
switch {
|
||||||
case b == '"' || b == '\\':
|
case b == '"' || b == '\\':
|
||||||
|
if consumed == 0 {
|
||||||
|
s.Grow(l * 2)
|
||||||
|
}
|
||||||
|
s.Write(msg[off+consumed : off+i])
|
||||||
s.WriteByte('\\')
|
s.WriteByte('\\')
|
||||||
s.WriteByte(b)
|
s.WriteByte(b)
|
||||||
|
consumed = i + 1
|
||||||
case b < ' ' || b > '~': // unprintable
|
case b < ' ' || b > '~': // unprintable
|
||||||
|
if consumed == 0 {
|
||||||
|
s.Grow(l * 2)
|
||||||
|
}
|
||||||
|
s.Write(msg[off+consumed : off+i])
|
||||||
s.WriteString(escapeByte(b))
|
s.WriteString(escapeByte(b))
|
||||||
default:
|
consumed = i + 1
|
||||||
s.WriteByte(b)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
off += 1 + l
|
if consumed == 0 { // no escaping needed
|
||||||
return s.String(), off, nil
|
return string(msg[off : off+l]), off + l, nil
|
||||||
|
}
|
||||||
|
s.Write(msg[off+consumed : off+l])
|
||||||
|
return s.String(), off + l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func packString(s string, msg []byte, off int) (int, error) {
|
func packString(s string, msg []byte, off int) (int, error) {
|
||||||
|
@ -410,79 +424,12 @@ Option:
|
||||||
if off+int(optlen) > len(msg) {
|
if off+int(optlen) > len(msg) {
|
||||||
return nil, len(msg), &Error{err: "overflow unpacking opt"}
|
return nil, len(msg), &Error{err: "overflow unpacking opt"}
|
||||||
}
|
}
|
||||||
switch code {
|
e := makeDataOpt(code)
|
||||||
case EDNS0NSID:
|
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
||||||
e := new(EDNS0_NSID)
|
return nil, len(msg), err
|
||||||
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
|
||||||
return nil, len(msg), err
|
|
||||||
}
|
|
||||||
edns = append(edns, e)
|
|
||||||
off += int(optlen)
|
|
||||||
case EDNS0SUBNET:
|
|
||||||
e := new(EDNS0_SUBNET)
|
|
||||||
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
|
||||||
return nil, len(msg), err
|
|
||||||
}
|
|
||||||
edns = append(edns, e)
|
|
||||||
off += int(optlen)
|
|
||||||
case EDNS0COOKIE:
|
|
||||||
e := new(EDNS0_COOKIE)
|
|
||||||
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
|
||||||
return nil, len(msg), err
|
|
||||||
}
|
|
||||||
edns = append(edns, e)
|
|
||||||
off += int(optlen)
|
|
||||||
case EDNS0UL:
|
|
||||||
e := new(EDNS0_UL)
|
|
||||||
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
|
||||||
return nil, len(msg), err
|
|
||||||
}
|
|
||||||
edns = append(edns, e)
|
|
||||||
off += int(optlen)
|
|
||||||
case EDNS0LLQ:
|
|
||||||
e := new(EDNS0_LLQ)
|
|
||||||
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
|
||||||
return nil, len(msg), err
|
|
||||||
}
|
|
||||||
edns = append(edns, e)
|
|
||||||
off += int(optlen)
|
|
||||||
case EDNS0DAU:
|
|
||||||
e := new(EDNS0_DAU)
|
|
||||||
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
|
||||||
return nil, len(msg), err
|
|
||||||
}
|
|
||||||
edns = append(edns, e)
|
|
||||||
off += int(optlen)
|
|
||||||
case EDNS0DHU:
|
|
||||||
e := new(EDNS0_DHU)
|
|
||||||
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
|
||||||
return nil, len(msg), err
|
|
||||||
}
|
|
||||||
edns = append(edns, e)
|
|
||||||
off += int(optlen)
|
|
||||||
case EDNS0N3U:
|
|
||||||
e := new(EDNS0_N3U)
|
|
||||||
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
|
||||||
return nil, len(msg), err
|
|
||||||
}
|
|
||||||
edns = append(edns, e)
|
|
||||||
off += int(optlen)
|
|
||||||
case EDNS0PADDING:
|
|
||||||
e := new(EDNS0_PADDING)
|
|
||||||
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
|
||||||
return nil, len(msg), err
|
|
||||||
}
|
|
||||||
edns = append(edns, e)
|
|
||||||
off += int(optlen)
|
|
||||||
default:
|
|
||||||
e := new(EDNS0_LOCAL)
|
|
||||||
e.Code = code
|
|
||||||
if err := e.unpack(msg[off : off+int(optlen)]); err != nil {
|
|
||||||
return nil, len(msg), err
|
|
||||||
}
|
|
||||||
edns = append(edns, e)
|
|
||||||
off += int(optlen)
|
|
||||||
}
|
}
|
||||||
|
edns = append(edns, e)
|
||||||
|
off += int(optlen)
|
||||||
|
|
||||||
if off < len(msg) {
|
if off < len(msg) {
|
||||||
goto Option
|
goto Option
|
||||||
|
@ -494,16 +441,14 @@ Option:
|
||||||
func packDataOpt(options []EDNS0, msg []byte, off int) (int, error) {
|
func packDataOpt(options []EDNS0, msg []byte, off int) (int, error) {
|
||||||
for _, el := range options {
|
for _, el := range options {
|
||||||
b, err := el.pack()
|
b, err := el.pack()
|
||||||
if err != nil || off+3 > len(msg) {
|
if err != nil || off+4 > len(msg) {
|
||||||
return len(msg), &Error{err: "overflow packing opt"}
|
return len(msg), &Error{err: "overflow packing opt"}
|
||||||
}
|
}
|
||||||
binary.BigEndian.PutUint16(msg[off:], el.Option()) // Option code
|
binary.BigEndian.PutUint16(msg[off:], el.Option()) // Option code
|
||||||
binary.BigEndian.PutUint16(msg[off+2:], uint16(len(b))) // Length
|
binary.BigEndian.PutUint16(msg[off+2:], uint16(len(b))) // Length
|
||||||
off += 4
|
off += 4
|
||||||
if off+len(b) > len(msg) {
|
if off+len(b) > len(msg) {
|
||||||
copy(msg[off:], b)
|
return len(msg), &Error{err: "overflow packing opt"}
|
||||||
off = len(msg)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
// Actual data
|
// Actual data
|
||||||
copy(msg[off:off+len(b)], b)
|
copy(msg[off:off+len(b)], b)
|
||||||
|
@ -531,7 +476,7 @@ func unpackDataNsec(msg []byte, off int) ([]uint16, int, error) {
|
||||||
length, window, lastwindow := 0, 0, -1
|
length, window, lastwindow := 0, 0, -1
|
||||||
for off < len(msg) {
|
for off < len(msg) {
|
||||||
if off+2 > len(msg) {
|
if off+2 > len(msg) {
|
||||||
return nsec, len(msg), &Error{err: "overflow unpacking nsecx"}
|
return nsec, len(msg), &Error{err: "overflow unpacking NSEC(3)"}
|
||||||
}
|
}
|
||||||
window = int(msg[off])
|
window = int(msg[off])
|
||||||
length = int(msg[off+1])
|
length = int(msg[off+1])
|
||||||
|
@ -539,17 +484,17 @@ func unpackDataNsec(msg []byte, off int) ([]uint16, int, error) {
|
||||||
if window <= lastwindow {
|
if window <= lastwindow {
|
||||||
// RFC 4034: Blocks are present in the NSEC RR RDATA in
|
// RFC 4034: Blocks are present in the NSEC RR RDATA in
|
||||||
// increasing numerical order.
|
// increasing numerical order.
|
||||||
return nsec, len(msg), &Error{err: "out of order NSEC block"}
|
return nsec, len(msg), &Error{err: "out of order NSEC(3) block in type bitmap"}
|
||||||
}
|
}
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
// RFC 4034: Blocks with no types present MUST NOT be included.
|
// RFC 4034: Blocks with no types present MUST NOT be included.
|
||||||
return nsec, len(msg), &Error{err: "empty NSEC block"}
|
return nsec, len(msg), &Error{err: "empty NSEC(3) block in type bitmap"}
|
||||||
}
|
}
|
||||||
if length > 32 {
|
if length > 32 {
|
||||||
return nsec, len(msg), &Error{err: "NSEC block too long"}
|
return nsec, len(msg), &Error{err: "NSEC(3) block too long in type bitmap"}
|
||||||
}
|
}
|
||||||
if off+length > len(msg) {
|
if off+length > len(msg) {
|
||||||
return nsec, len(msg), &Error{err: "overflowing NSEC block"}
|
return nsec, len(msg), &Error{err: "overflowing NSEC(3) block in type bitmap"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk the bytes in the window and extract the type bits
|
// Walk the bytes in the window and extract the type bits
|
||||||
|
@ -586,10 +531,43 @@ func unpackDataNsec(msg []byte, off int) ([]uint16, int, error) {
|
||||||
return nsec, off, nil
|
return nsec, off, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// typeBitMapLen is a helper function which computes the "maximum" length of
|
||||||
|
// a the NSEC Type BitMap field.
|
||||||
|
func typeBitMapLen(bitmap []uint16) int {
|
||||||
|
var l int
|
||||||
|
var lastwindow, lastlength uint16
|
||||||
|
for _, t := range bitmap {
|
||||||
|
window := t / 256
|
||||||
|
length := (t-window*256)/8 + 1
|
||||||
|
if window > lastwindow && lastlength != 0 { // New window, jump to the new offset
|
||||||
|
l += int(lastlength) + 2
|
||||||
|
lastlength = 0
|
||||||
|
}
|
||||||
|
if window < lastwindow || length < lastlength {
|
||||||
|
// packDataNsec would return Error{err: "nsec bits out of order"} here, but
|
||||||
|
// when computing the length, we want do be liberal.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lastwindow, lastlength = window, length
|
||||||
|
}
|
||||||
|
l += int(lastlength) + 2
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
func packDataNsec(bitmap []uint16, msg []byte, off int) (int, error) {
|
func packDataNsec(bitmap []uint16, msg []byte, off int) (int, error) {
|
||||||
if len(bitmap) == 0 {
|
if len(bitmap) == 0 {
|
||||||
return off, nil
|
return off, nil
|
||||||
}
|
}
|
||||||
|
if off > len(msg) {
|
||||||
|
return off, &Error{err: "overflow packing nsec"}
|
||||||
|
}
|
||||||
|
toZero := msg[off:]
|
||||||
|
if maxLen := typeBitMapLen(bitmap); maxLen < len(toZero) {
|
||||||
|
toZero = toZero[:maxLen]
|
||||||
|
}
|
||||||
|
for i := range toZero {
|
||||||
|
toZero[i] = 0
|
||||||
|
}
|
||||||
var lastwindow, lastlength uint16
|
var lastwindow, lastlength uint16
|
||||||
for _, t := range bitmap {
|
for _, t := range bitmap {
|
||||||
window := t / 256
|
window := t / 256
|
||||||
|
@ -616,6 +594,65 @@ func packDataNsec(bitmap []uint16, msg []byte, off int) (int, error) {
|
||||||
return off, nil
|
return off, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unpackDataSVCB(msg []byte, off int) ([]SVCBKeyValue, int, error) {
|
||||||
|
var xs []SVCBKeyValue
|
||||||
|
var code uint16
|
||||||
|
var length uint16
|
||||||
|
var err error
|
||||||
|
for off < len(msg) {
|
||||||
|
code, off, err = unpackUint16(msg, off)
|
||||||
|
if err != nil {
|
||||||
|
return nil, len(msg), &Error{err: "overflow unpacking SVCB"}
|
||||||
|
}
|
||||||
|
length, off, err = unpackUint16(msg, off)
|
||||||
|
if err != nil || off+int(length) > len(msg) {
|
||||||
|
return nil, len(msg), &Error{err: "overflow unpacking SVCB"}
|
||||||
|
}
|
||||||
|
e := makeSVCBKeyValue(SVCBKey(code))
|
||||||
|
if e == nil {
|
||||||
|
return nil, len(msg), &Error{err: "bad SVCB key"}
|
||||||
|
}
|
||||||
|
if err := e.unpack(msg[off : off+int(length)]); err != nil {
|
||||||
|
return nil, len(msg), err
|
||||||
|
}
|
||||||
|
if len(xs) > 0 && e.Key() <= xs[len(xs)-1].Key() {
|
||||||
|
return nil, len(msg), &Error{err: "SVCB keys not in strictly increasing order"}
|
||||||
|
}
|
||||||
|
xs = append(xs, e)
|
||||||
|
off += int(length)
|
||||||
|
}
|
||||||
|
return xs, off, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func packDataSVCB(pairs []SVCBKeyValue, msg []byte, off int) (int, error) {
|
||||||
|
pairs = append([]SVCBKeyValue(nil), pairs...)
|
||||||
|
sort.Slice(pairs, func(i, j int) bool {
|
||||||
|
return pairs[i].Key() < pairs[j].Key()
|
||||||
|
})
|
||||||
|
prev := svcb_RESERVED
|
||||||
|
for _, el := range pairs {
|
||||||
|
if el.Key() == prev {
|
||||||
|
return len(msg), &Error{err: "repeated SVCB keys are not allowed"}
|
||||||
|
}
|
||||||
|
prev = el.Key()
|
||||||
|
packed, err := el.pack()
|
||||||
|
if err != nil {
|
||||||
|
return len(msg), err
|
||||||
|
}
|
||||||
|
off, err = packUint16(uint16(el.Key()), msg, off)
|
||||||
|
if err != nil {
|
||||||
|
return len(msg), &Error{err: "overflow packing SVCB"}
|
||||||
|
}
|
||||||
|
off, err = packUint16(uint16(len(packed)), msg, off)
|
||||||
|
if err != nil || off+len(packed) > len(msg) {
|
||||||
|
return len(msg), &Error{err: "overflow packing SVCB"}
|
||||||
|
}
|
||||||
|
copy(msg[off:off+len(packed)], packed)
|
||||||
|
off += len(packed)
|
||||||
|
}
|
||||||
|
return off, nil
|
||||||
|
}
|
||||||
|
|
||||||
func unpackDataDomainNames(msg []byte, off, end int) ([]string, int, error) {
|
func unpackDataDomainNames(msg []byte, off, end int) ([]string, int, error) {
|
||||||
var (
|
var (
|
||||||
servers []string
|
servers []string
|
||||||
|
@ -645,3 +682,131 @@ func packDataDomainNames(names []string, msg []byte, off int, compression compre
|
||||||
}
|
}
|
||||||
return off, nil
|
return off, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func packDataApl(data []APLPrefix, msg []byte, off int) (int, error) {
|
||||||
|
var err error
|
||||||
|
for i := range data {
|
||||||
|
off, err = packDataAplPrefix(&data[i], msg, off)
|
||||||
|
if err != nil {
|
||||||
|
return len(msg), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return off, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func packDataAplPrefix(p *APLPrefix, msg []byte, off int) (int, error) {
|
||||||
|
if len(p.Network.IP) != len(p.Network.Mask) {
|
||||||
|
return len(msg), &Error{err: "address and mask lengths don't match"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
prefix, _ := p.Network.Mask.Size()
|
||||||
|
addr := p.Network.IP.Mask(p.Network.Mask)[:(prefix+7)/8]
|
||||||
|
|
||||||
|
switch len(p.Network.IP) {
|
||||||
|
case net.IPv4len:
|
||||||
|
off, err = packUint16(1, msg, off)
|
||||||
|
case net.IPv6len:
|
||||||
|
off, err = packUint16(2, msg, off)
|
||||||
|
default:
|
||||||
|
err = &Error{err: "unrecognized address family"}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return len(msg), err
|
||||||
|
}
|
||||||
|
|
||||||
|
off, err = packUint8(uint8(prefix), msg, off)
|
||||||
|
if err != nil {
|
||||||
|
return len(msg), err
|
||||||
|
}
|
||||||
|
|
||||||
|
var n uint8
|
||||||
|
if p.Negation {
|
||||||
|
n = 0x80
|
||||||
|
}
|
||||||
|
|
||||||
|
// trim trailing zero bytes as specified in RFC3123 Sections 4.1 and 4.2.
|
||||||
|
i := len(addr) - 1
|
||||||
|
for ; i >= 0 && addr[i] == 0; i-- {
|
||||||
|
}
|
||||||
|
addr = addr[:i+1]
|
||||||
|
|
||||||
|
adflen := uint8(len(addr)) & 0x7f
|
||||||
|
off, err = packUint8(n|adflen, msg, off)
|
||||||
|
if err != nil {
|
||||||
|
return len(msg), err
|
||||||
|
}
|
||||||
|
|
||||||
|
if off+len(addr) > len(msg) {
|
||||||
|
return len(msg), &Error{err: "overflow packing APL prefix"}
|
||||||
|
}
|
||||||
|
off += copy(msg[off:], addr)
|
||||||
|
|
||||||
|
return off, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackDataApl(msg []byte, off int) ([]APLPrefix, int, error) {
|
||||||
|
var result []APLPrefix
|
||||||
|
for off < len(msg) {
|
||||||
|
prefix, end, err := unpackDataAplPrefix(msg, off)
|
||||||
|
if err != nil {
|
||||||
|
return nil, len(msg), err
|
||||||
|
}
|
||||||
|
off = end
|
||||||
|
result = append(result, prefix)
|
||||||
|
}
|
||||||
|
return result, off, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackDataAplPrefix(msg []byte, off int) (APLPrefix, int, error) {
|
||||||
|
family, off, err := unpackUint16(msg, off)
|
||||||
|
if err != nil {
|
||||||
|
return APLPrefix{}, len(msg), &Error{err: "overflow unpacking APL prefix"}
|
||||||
|
}
|
||||||
|
prefix, off, err := unpackUint8(msg, off)
|
||||||
|
if err != nil {
|
||||||
|
return APLPrefix{}, len(msg), &Error{err: "overflow unpacking APL prefix"}
|
||||||
|
}
|
||||||
|
nlen, off, err := unpackUint8(msg, off)
|
||||||
|
if err != nil {
|
||||||
|
return APLPrefix{}, len(msg), &Error{err: "overflow unpacking APL prefix"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ip []byte
|
||||||
|
switch family {
|
||||||
|
case 1:
|
||||||
|
ip = make([]byte, net.IPv4len)
|
||||||
|
case 2:
|
||||||
|
ip = make([]byte, net.IPv6len)
|
||||||
|
default:
|
||||||
|
return APLPrefix{}, len(msg), &Error{err: "unrecognized APL address family"}
|
||||||
|
}
|
||||||
|
if int(prefix) > 8*len(ip) {
|
||||||
|
return APLPrefix{}, len(msg), &Error{err: "APL prefix too long"}
|
||||||
|
}
|
||||||
|
afdlen := int(nlen & 0x7f)
|
||||||
|
if afdlen > len(ip) {
|
||||||
|
return APLPrefix{}, len(msg), &Error{err: "APL length too long"}
|
||||||
|
}
|
||||||
|
if off+afdlen > len(msg) {
|
||||||
|
return APLPrefix{}, len(msg), &Error{err: "overflow unpacking APL address"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address MUST NOT contain trailing zero bytes per RFC3123 Sections 4.1 and 4.2.
|
||||||
|
off += copy(ip, msg[off:off+afdlen])
|
||||||
|
if afdlen > 0 {
|
||||||
|
last := ip[afdlen-1]
|
||||||
|
if last == 0 {
|
||||||
|
return APLPrefix{}, len(msg), &Error{err: "extra APL address bits"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ipnet := net.IPNet{
|
||||||
|
IP: ip,
|
||||||
|
Mask: net.CIDRMask(int(prefix), 8*len(ip)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return APLPrefix{
|
||||||
|
Negation: (nlen & 0x80) != 0,
|
||||||
|
Network: ipnet,
|
||||||
|
}, off, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
// TestPacketDataNsec tests generated using fuzz.go and with a message pack
|
// TestPacketDataNsec tests generated using fuzz.go and with a message pack
|
||||||
// containing the following bytes: 0000\x00\x00000000\x00\x002000000\x0060000\x00\x130000000000000000000"
|
// containing the following bytes: 0000\x00\x00000000\x00\x002000000\x0060000\x00\x130000000000000000000"
|
||||||
|
@ -15,7 +19,8 @@ func TestPackDataNsec(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
want int
|
wantOff int
|
||||||
|
wantBytes []byte
|
||||||
wantErr bool
|
wantErr bool
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
}{
|
}{
|
||||||
|
@ -40,14 +45,14 @@ func TestPackDataNsec(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
wantErrMsg: "dns: overflow packing nsec",
|
wantErrMsg: "dns: overflow packing nsec",
|
||||||
want: 31,
|
wantOff: 48,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "disordered nsec bits",
|
name: "disordered nsec bits",
|
||||||
args: args{
|
args: args{
|
||||||
bitmap: []uint16{
|
bitmap: []uint16{
|
||||||
8962,
|
8962,
|
||||||
0,
|
1,
|
||||||
},
|
},
|
||||||
msg: []byte{
|
msg: []byte{
|
||||||
48, 48, 48, 48, 0, 0, 0, 1, 0, 0, 0, 0,
|
48, 48, 48, 48, 0, 0, 0, 1, 0, 0, 0, 0,
|
||||||
|
@ -68,13 +73,13 @@ func TestPackDataNsec(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
wantErrMsg: "dns: nsec bits out of order",
|
wantErrMsg: "dns: nsec bits out of order",
|
||||||
want: 155,
|
wantOff: 155,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "simple message with only one window",
|
name: "simple message with only one window",
|
||||||
args: args{
|
args: args{
|
||||||
bitmap: []uint16{
|
bitmap: []uint16{
|
||||||
0,
|
1,
|
||||||
},
|
},
|
||||||
msg: []byte{
|
msg: []byte{
|
||||||
48, 48, 48, 48, 0, 0,
|
48, 48, 48, 48, 0, 0,
|
||||||
|
@ -85,13 +90,33 @@ func TestPackDataNsec(t *testing.T) {
|
||||||
},
|
},
|
||||||
off: 0,
|
off: 0,
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
want: 3,
|
wantOff: 3,
|
||||||
|
wantBytes: []byte{0, 1, 64},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple types",
|
||||||
|
args: args{
|
||||||
|
bitmap: []uint16{
|
||||||
|
TypeNS, TypeSOA, TypeRRSIG, TypeDNSKEY, TypeNSEC3PARAM,
|
||||||
|
},
|
||||||
|
msg: []byte{
|
||||||
|
48, 48, 48, 48, 0, 0,
|
||||||
|
0, 1, 0, 0, 0, 0,
|
||||||
|
0, 0, 50, 48, 48, 48,
|
||||||
|
48, 48, 48, 0, 54, 48,
|
||||||
|
48, 48, 48, 0, 19, 48, 48,
|
||||||
|
},
|
||||||
|
off: 0,
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
wantOff: 9,
|
||||||
|
wantBytes: []byte{0, 7, 34, 0, 0, 0, 0, 2, 144},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := packDataNsec(tt.args.bitmap, tt.args.msg, tt.args.off)
|
gotOff, err := packDataNsec(tt.args.bitmap, tt.args.msg, tt.args.off)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("packDataNsec() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("packDataNsec() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
|
@ -100,13 +125,49 @@ func TestPackDataNsec(t *testing.T) {
|
||||||
t.Errorf("packDataNsec() error msg = %v, wantErrMsg %v", err.Error(), tt.wantErrMsg)
|
t.Errorf("packDataNsec() error msg = %v, wantErrMsg %v", err.Error(), tt.wantErrMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if got != tt.want {
|
if gotOff != tt.wantOff {
|
||||||
t.Errorf("packDataNsec() = %v, want %v", got, tt.want)
|
t.Errorf("packDataNsec() = %v, want off %v", gotOff, tt.wantOff)
|
||||||
|
}
|
||||||
|
if err == nil && tt.args.off < len(tt.args.msg) && gotOff < len(tt.args.msg) {
|
||||||
|
if want, got := tt.wantBytes, tt.args.msg[tt.args.off:gotOff]; !bytes.Equal(got, want) {
|
||||||
|
t.Errorf("packDataNsec() = %v, want bytes %v", got, want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPackDataNsecDirtyBuffer(t *testing.T) {
|
||||||
|
zeroBuf := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
dirtyBuf := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||||
|
off1, _ := packDataNsec([]uint16{TypeNS, TypeSOA, TypeRRSIG}, zeroBuf, 0)
|
||||||
|
off2, _ := packDataNsec([]uint16{TypeNS, TypeSOA, TypeRRSIG}, dirtyBuf, 0)
|
||||||
|
if off1 != off2 {
|
||||||
|
t.Errorf("off1 %v != off2 %v", off1, off2)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(zeroBuf[:off1], dirtyBuf[:off2]) {
|
||||||
|
t.Errorf("dirty buffer differs from zero buffer: %v, %v", zeroBuf[:off1], dirtyBuf[:off2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPackDataNsec(b *testing.B) {
|
||||||
|
benches := []struct {
|
||||||
|
name string
|
||||||
|
types []uint16
|
||||||
|
}{
|
||||||
|
{"empty", nil},
|
||||||
|
{"typical", []uint16{TypeNS, TypeSOA, TypeRRSIG, TypeDNSKEY, TypeNSEC3PARAM}},
|
||||||
|
{"multiple_windows", []uint16{1, 300, 350, 10000, 20000}},
|
||||||
|
}
|
||||||
|
for _, bb := range benches {
|
||||||
|
b.Run(bb.name, func(b *testing.B) {
|
||||||
|
buf := make([]byte, 100)
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
packDataNsec(bb.types, buf, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
func TestUnpackString(t *testing.T) {
|
func TestUnpackString(t *testing.T) {
|
||||||
msg := []byte("\x00abcdef\x0f\\\"ghi\x04mmm\x7f")
|
msg := []byte("\x00abcdef\x0f\\\"ghi\x04mmm\x7f")
|
||||||
msg[0] = byte(len(msg) - 1)
|
msg[0] = byte(len(msg) - 1)
|
||||||
|
@ -122,17 +183,411 @@ func TestUnpackString(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkUnpackString(b *testing.B) {
|
func BenchmarkUnpackString(b *testing.B) {
|
||||||
msg := []byte("\x00abcdef\x0f\\\"ghi\x04mmm")
|
b.Run("Escaped", func(b *testing.B) {
|
||||||
msg[0] = byte(len(msg) - 1)
|
msg := []byte("\x00abcdef\x0f\\\"ghi\x04mmm")
|
||||||
|
msg[0] = byte(len(msg) - 1)
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
got, _, err := unpackString(msg, 0)
|
got, _, err := unpackString(msg, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if want := `abcdef\015\\\"ghi\004mmm`; want != got {
|
||||||
|
b.Errorf("expected %q, got %q", want, got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
b.Run("Unescaped", func(b *testing.B) {
|
||||||
|
msg := []byte("\x00large.example.com")
|
||||||
|
msg[0] = byte(len(msg) - 1)
|
||||||
|
|
||||||
if want := `abcdef\015\\\"ghi\004mmm`; want != got {
|
for n := 0; n < b.N; n++ {
|
||||||
b.Errorf("expected %q, got %q", want, got)
|
got, _, err := unpackString(msg, 0)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if want := "large.example.com"; want != got {
|
||||||
|
b.Errorf("expected %q, got %q", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackDataAplPrefix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
negation bool
|
||||||
|
ip net.IP
|
||||||
|
mask net.IPMask
|
||||||
|
expect []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"1:192.0.2.0/24",
|
||||||
|
false,
|
||||||
|
net.ParseIP("192.0.2.0").To4(),
|
||||||
|
net.CIDRMask(24, 32),
|
||||||
|
[]byte{0x00, 0x01, 0x18, 0x03, 192, 0, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"2:2001:db8:cafe::0/48",
|
||||||
|
false,
|
||||||
|
net.ParseIP("2001:db8:cafe::"),
|
||||||
|
net.CIDRMask(48, 128),
|
||||||
|
[]byte{0x00, 0x02, 0x30, 0x06, 0x20, 0x01, 0x0d, 0xb8, 0xca, 0xfe},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"with trailing zero bytes 2:2001:db8:cafe::0/64",
|
||||||
|
false,
|
||||||
|
net.ParseIP("2001:db8:cafe::"),
|
||||||
|
net.CIDRMask(64, 128),
|
||||||
|
[]byte{0x00, 0x02, 0x40, 0x06, 0x20, 0x01, 0x0d, 0xb8, 0xca, 0xfe},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no non-zero bytes 2::/16",
|
||||||
|
false,
|
||||||
|
net.ParseIP("::"),
|
||||||
|
net.CIDRMask(16, 128),
|
||||||
|
[]byte{0x00, 0x02, 0x10, 0x00},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"!2:2001:db8::/32",
|
||||||
|
true,
|
||||||
|
net.ParseIP("2001:db8::"),
|
||||||
|
net.CIDRMask(32, 128),
|
||||||
|
[]byte{0x00, 0x02, 0x20, 0x84, 0x20, 0x01, 0x0d, 0xb8},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalize 1:198.51.103.255/22",
|
||||||
|
false,
|
||||||
|
net.ParseIP("198.51.103.255").To4(),
|
||||||
|
net.CIDRMask(22, 32),
|
||||||
|
[]byte{0x00, 0x01, 0x16, 0x03, 198, 51, 100}, // 1:198.51.100.0/22
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ap := &APLPrefix{
|
||||||
|
Negation: tt.negation,
|
||||||
|
Network: net.IPNet{IP: tt.ip, Mask: tt.mask},
|
||||||
|
}
|
||||||
|
out := make([]byte, 16)
|
||||||
|
off, err := packDataAplPrefix(ap, out, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %q", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(tt.expect, out[:off]) {
|
||||||
|
t.Fatalf("expected output %02x, got %02x", tt.expect, out[:off])
|
||||||
|
}
|
||||||
|
// Make sure the packed bytes would be accepted by its own unpack
|
||||||
|
_, _, err = unpackDataAplPrefix(out, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %q", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackDataAplPrefix_Failures(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ip net.IP
|
||||||
|
mask net.IPMask
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"family mismatch",
|
||||||
|
net.ParseIP("2001:db8::"),
|
||||||
|
net.CIDRMask(24, 32),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unrecognized family",
|
||||||
|
[]byte{0x42},
|
||||||
|
[]byte{0xff},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ap := &APLPrefix{Network: net.IPNet{IP: tt.ip, Mask: tt.mask}}
|
||||||
|
msg := make([]byte, 16)
|
||||||
|
off, err := packDataAplPrefix(ap, msg, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got none")
|
||||||
|
}
|
||||||
|
if off != len(msg) {
|
||||||
|
t.Fatalf("expected %d, got %d", len(msg), off)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackDataAplPrefix_BufferBounds(t *testing.T) {
|
||||||
|
ap := &APLPrefix{
|
||||||
|
Negation: false,
|
||||||
|
Network: net.IPNet{
|
||||||
|
IP: net.ParseIP("2001:db8::"),
|
||||||
|
Mask: net.CIDRMask(32, 128),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wire := []byte{0x00, 0x02, 0x20, 0x04, 0x20, 0x01, 0x0d, 0xb8}
|
||||||
|
|
||||||
|
t.Run("small", func(t *testing.T) {
|
||||||
|
msg := make([]byte, len(wire))
|
||||||
|
_, err := packDataAplPrefix(ap, msg, 1) // offset
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("exact fit", func(t *testing.T) {
|
||||||
|
msg := make([]byte, len(wire))
|
||||||
|
off, err := packDataAplPrefix(ap, msg, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %q", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(wire, msg[:off]) {
|
||||||
|
t.Fatalf("expected %02x, got %02x", wire, msg[:off])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackDataApl(t *testing.T) {
|
||||||
|
in := []APLPrefix{
|
||||||
|
{
|
||||||
|
Negation: true,
|
||||||
|
Network: net.IPNet{
|
||||||
|
IP: net.ParseIP("198.51.0.0").To4(),
|
||||||
|
Mask: net.CIDRMask(16, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Negation: false,
|
||||||
|
Network: net.IPNet{
|
||||||
|
IP: net.ParseIP("2001:db8:beef::"),
|
||||||
|
Mask: net.CIDRMask(48, 128),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expect := []byte{
|
||||||
|
// 1:192.51.0.0/16
|
||||||
|
0x00, 0x01, 0x10, 0x82, 0xc6, 0x33,
|
||||||
|
// 2:2001:db8:beef::0/48
|
||||||
|
0x00, 0x02, 0x30, 0x06, 0x20, 0x01, 0x0d, 0xb8, 0xbe, 0xef,
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := make([]byte, 32)
|
||||||
|
off, err := packDataApl(in, msg, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %q", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(expect, msg[:off]) {
|
||||||
|
t.Fatalf("expected %02x, got %02x", expect, msg[:off])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnpackDataAplPrefix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
wire []byte
|
||||||
|
negation bool
|
||||||
|
ip net.IP
|
||||||
|
mask net.IPMask
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"1:192.0.2.0/24",
|
||||||
|
[]byte{0x00, 0x01, 0x18, 0x03, 192, 0, 2},
|
||||||
|
false,
|
||||||
|
net.ParseIP("192.0.2.0").To4(),
|
||||||
|
net.CIDRMask(24, 32),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"2:2001:db8::/32",
|
||||||
|
[]byte{0x00, 0x02, 0x20, 0x04, 0x20, 0x01, 0x0d, 0xb8},
|
||||||
|
false,
|
||||||
|
net.ParseIP("2001:db8::"),
|
||||||
|
net.CIDRMask(32, 128),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"!2:2001:db8:8000::/33",
|
||||||
|
[]byte{0x00, 0x02, 0x21, 0x85, 0x20, 0x01, 0x0d, 0xb8, 0x80},
|
||||||
|
true,
|
||||||
|
net.ParseIP("2001:db8:8000::"),
|
||||||
|
net.CIDRMask(33, 128),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"1:0.0.0.0/0",
|
||||||
|
[]byte{0x00, 0x01, 0x00, 0x00},
|
||||||
|
false,
|
||||||
|
net.ParseIP("0.0.0.0").To4(),
|
||||||
|
net.CIDRMask(0, 32),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, off, err := unpackDataAplPrefix(tt.wire, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %q", err)
|
||||||
|
}
|
||||||
|
if off != len(tt.wire) {
|
||||||
|
t.Fatalf("expected offset %d, got %d", len(tt.wire), off)
|
||||||
|
}
|
||||||
|
if got.Negation != tt.negation {
|
||||||
|
t.Errorf("expected negation %v, got %v", tt.negation, got.Negation)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got.Network.IP, tt.ip) {
|
||||||
|
t.Errorf("expected IP %02x, got %02x", tt.ip, got.Network.IP)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got.Network.Mask, tt.mask) {
|
||||||
|
t.Errorf("expected mask %02x, got %02x", tt.mask, got.Network.Mask)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnpackDataAplPrefix_Errors(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
wire []byte
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"incomplete header",
|
||||||
|
[]byte{0x00, 0x01, 0x18},
|
||||||
|
"dns: overflow unpacking APL prefix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unrecognized family",
|
||||||
|
[]byte{0x00, 0x03, 0x00, 0x00},
|
||||||
|
"dns: unrecognized APL address family",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prefix too large for family IPv4",
|
||||||
|
[]byte{0x00, 0x01, 0x21, 0x04, 192, 0, 2, 0},
|
||||||
|
"dns: APL prefix too long",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"prefix too large for family IPv6",
|
||||||
|
[]byte{0x00, 0x02, 0x81, 0x85, 0x20, 0x01, 0x0d, 0xb8, 0x80},
|
||||||
|
"dns: APL prefix too long",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"afdlen too long for address family IPv4",
|
||||||
|
[]byte{0x00, 0x01, 22, 0x05, 192, 0, 2, 0, 0},
|
||||||
|
"dns: APL length too long",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"overflow unpacking APL address",
|
||||||
|
[]byte{0x00, 0x01, 0x10, 0x02, 192},
|
||||||
|
"dns: overflow unpacking APL address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address included trailing zeros",
|
||||||
|
[]byte{0x00, 0x01, 0x10, 0x04, 192, 0, 2, 0},
|
||||||
|
"dns: extra APL address bits",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, _, err := unpackDataAplPrefix(tt.wire, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Error() != tt.want {
|
||||||
|
t.Errorf("expected %s, got %s", tt.want, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnpackDataApl(t *testing.T) {
|
||||||
|
wire := []byte{
|
||||||
|
// 2:2001:db8:cafe:4200:0/56
|
||||||
|
0x00, 0x02, 0x38, 0x07, 0x20, 0x01, 0x0d, 0xb8, 0xca, 0xfe, 0x42,
|
||||||
|
// 1:192.0.2.0/24
|
||||||
|
0x00, 0x01, 0x18, 0x03, 192, 0, 2,
|
||||||
|
// !1:192.0.2.128/25
|
||||||
|
0x00, 0x01, 0x19, 0x84, 192, 0, 2, 128,
|
||||||
|
// 1:10.0.0.0/24
|
||||||
|
0x00, 0x01, 0x18, 0x01, 0x0a,
|
||||||
|
// !1:10.0.0.1/32
|
||||||
|
0x00, 0x01, 0x20, 0x84, 0x0a, 0, 0, 1,
|
||||||
|
// !1:0.0.0.0/0
|
||||||
|
0x00, 0x01, 0x00, 0x80,
|
||||||
|
// 2::0/0
|
||||||
|
0x00, 0x02, 0x00, 0x00,
|
||||||
|
}
|
||||||
|
expect := []APLPrefix{
|
||||||
|
{
|
||||||
|
Negation: false,
|
||||||
|
Network: net.IPNet{
|
||||||
|
IP: net.ParseIP("2001:db8:cafe:4200::"),
|
||||||
|
Mask: net.CIDRMask(56, 128),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Negation: false,
|
||||||
|
Network: net.IPNet{
|
||||||
|
IP: net.ParseIP("192.0.2.0").To4(),
|
||||||
|
Mask: net.CIDRMask(24, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Negation: true,
|
||||||
|
Network: net.IPNet{
|
||||||
|
IP: net.ParseIP("192.0.2.128").To4(),
|
||||||
|
Mask: net.CIDRMask(25, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Negation: false,
|
||||||
|
Network: net.IPNet{
|
||||||
|
IP: net.ParseIP("10.0.0.0").To4(),
|
||||||
|
Mask: net.CIDRMask(24, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Negation: true,
|
||||||
|
Network: net.IPNet{
|
||||||
|
IP: net.ParseIP("10.0.0.1").To4(),
|
||||||
|
Mask: net.CIDRMask(32, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Negation: true,
|
||||||
|
Network: net.IPNet{
|
||||||
|
IP: net.ParseIP("0.0.0.0").To4(),
|
||||||
|
Mask: net.CIDRMask(0, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Negation: false,
|
||||||
|
Network: net.IPNet{
|
||||||
|
IP: net.ParseIP("::").To16(),
|
||||||
|
Mask: net.CIDRMask(0, 128),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, off, err := unpackDataApl(wire, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %q", err)
|
||||||
|
}
|
||||||
|
if off != len(wire) {
|
||||||
|
t.Fatalf("expected offset %d, got %d", len(wire), off)
|
||||||
|
}
|
||||||
|
if len(got) != len(expect) {
|
||||||
|
t.Fatalf("expected %d prefixes, got %d", len(expect), len(got))
|
||||||
|
}
|
||||||
|
for i, exp := range expect {
|
||||||
|
if got[i].Negation != exp.Negation {
|
||||||
|
t.Errorf("[%d] expected negation %v, got %v", i, exp.Negation, got[i].Negation)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got[i].Network.IP, exp.Network.IP) {
|
||||||
|
t.Errorf("[%d] expected IP %02x, got %02x", i, exp.Network.IP, got[i].Network.IP)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got[i].Network.Mask, exp.Network.Mask) {
|
||||||
|
t.Errorf("[%d] expected mask %02x, got %02x", i, exp.Network.Mask, got[i].Network.Mask)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
38
msg_test.go
38
msg_test.go
|
@ -133,19 +133,19 @@ func TestUnpackDomainName(t *testing.T) {
|
||||||
".",
|
".",
|
||||||
""},
|
""},
|
||||||
{"long label",
|
{"long label",
|
||||||
string(63) + maxPrintableLabel + "\x00",
|
"?" + maxPrintableLabel + "\x00",
|
||||||
maxPrintableLabel + ".",
|
maxPrintableLabel + ".",
|
||||||
""},
|
""},
|
||||||
{"unprintable label",
|
{"unprintable label",
|
||||||
string(63) + regexp.MustCompile(`\\[0-9]+`).ReplaceAllStringFunc(maxUnprintableLabel,
|
"?" + regexp.MustCompile(`\\[0-9]+`).ReplaceAllStringFunc(maxUnprintableLabel,
|
||||||
func(escape string) string {
|
func(escape string) string {
|
||||||
n, _ := strconv.ParseInt(escape[1:], 10, 8)
|
n, _ := strconv.ParseInt(escape[1:], 10, 8)
|
||||||
return string(n)
|
return string(rune(n))
|
||||||
}) + "\x00",
|
}) + "\x00",
|
||||||
maxUnprintableLabel + ".",
|
maxUnprintableLabel + ".",
|
||||||
""},
|
""},
|
||||||
{"long domain",
|
{"long domain",
|
||||||
string(53) + strings.Replace(longDomain, ".", string(49), -1) + "\x00",
|
"5" + strings.Replace(longDomain, ".", "1", -1) + "\x00",
|
||||||
longDomain + ".",
|
longDomain + ".",
|
||||||
""},
|
""},
|
||||||
{"compression pointer",
|
{"compression pointer",
|
||||||
|
@ -154,7 +154,7 @@ func TestUnpackDomainName(t *testing.T) {
|
||||||
"foo.\\003com\\000.example.com.",
|
"foo.\\003com\\000.example.com.",
|
||||||
""},
|
""},
|
||||||
{"too long domain",
|
{"too long domain",
|
||||||
string(54) + "x" + strings.Replace(longDomain, ".", string(49), -1) + "\x00",
|
"6" + "x" + strings.Replace(longDomain, ".", "1", -1) + "\x00",
|
||||||
"",
|
"",
|
||||||
ErrLongDomain.Error()},
|
ErrLongDomain.Error()},
|
||||||
{"too long by pointer",
|
{"too long by pointer",
|
||||||
|
@ -202,6 +202,7 @@ func TestUnpackDomainName(t *testing.T) {
|
||||||
"\x03foo" + "\x03bar" + "\x07example" + "\xC0\x04",
|
"\x03foo" + "\x03bar" + "\x07example" + "\xC0\x04",
|
||||||
"",
|
"",
|
||||||
ErrLongDomain.Error()},
|
ErrLongDomain.Error()},
|
||||||
|
{"forward compression pointer", "\x02\xC0\xFF\xC0\x01", "", ErrBuf.Error()},
|
||||||
{"reserved compression pointer 0b10", "\x07example\x80", "", "dns: bad rdata"},
|
{"reserved compression pointer 0b10", "\x07example\x80", "", "dns: bad rdata"},
|
||||||
{"reserved compression pointer 0b01", "\x07example\x40", "", "dns: bad rdata"},
|
{"reserved compression pointer 0b01", "\x07example\x40", "", "dns: bad rdata"},
|
||||||
}
|
}
|
||||||
|
@ -220,11 +221,11 @@ func TestUnpackDomainName(t *testing.T) {
|
||||||
|
|
||||||
func TestPackDomainNameCompressionMap(t *testing.T) {
|
func TestPackDomainNameCompressionMap(t *testing.T) {
|
||||||
expected := map[string]struct{}{
|
expected := map[string]struct{}{
|
||||||
`www\.this.is.\131an.example.org.`: struct{}{},
|
`www\.this.is.\131an.example.org.`: {},
|
||||||
`is.\131an.example.org.`: struct{}{},
|
`is.\131an.example.org.`: {},
|
||||||
`\131an.example.org.`: struct{}{},
|
`\131an.example.org.`: {},
|
||||||
`example.org.`: struct{}{},
|
`example.org.`: {},
|
||||||
`org.`: struct{}{},
|
`org.`: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := make([]byte, 256)
|
msg := make([]byte, 256)
|
||||||
|
@ -306,3 +307,20 @@ func TestPackUnpackManyCompressionPointers(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLenDynamicA(t *testing.T) {
|
||||||
|
for _, rr := range []RR{
|
||||||
|
testRR("example.org. A"),
|
||||||
|
testRR("example.org. AAAA"),
|
||||||
|
testRR("example.org. L32"),
|
||||||
|
} {
|
||||||
|
msg := make([]byte, Len(rr))
|
||||||
|
off, err := PackRR(rr, msg, 0, nil, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PackRR failed for %T: %v", rr, err)
|
||||||
|
}
|
||||||
|
if off != len(msg) {
|
||||||
|
t.Errorf("Len(rr) wrong for %T: Len(rr) = %d, PackRR(rr) = %d", rr, len(msg), off)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
package dns
|
||||||
|
|
||||||
|
// Truncate ensures the reply message will fit into the requested buffer
|
||||||
|
// size by removing records that exceed the requested size.
|
||||||
|
//
|
||||||
|
// It will first check if the reply fits without compression and then with
|
||||||
|
// compression. If it won't fit with compression, Truncate then walks the
|
||||||
|
// record adding as many records as possible without exceeding the
|
||||||
|
// requested buffer size.
|
||||||
|
//
|
||||||
|
// If the message fits within the requested size without compression,
|
||||||
|
// Truncate will set the message's Compress attribute to false. It is
|
||||||
|
// the caller's responsibility to set it back to true if they wish to
|
||||||
|
// compress the payload regardless of size.
|
||||||
|
//
|
||||||
|
// The TC bit will be set if any records were excluded from the message.
|
||||||
|
// If the TC bit is already set on the message it will be retained.
|
||||||
|
// TC indicates that the client should retry over TCP.
|
||||||
|
//
|
||||||
|
// According to RFC 2181, the TC bit should only be set if not all of the
|
||||||
|
// "required" RRs can be included in the response. Unfortunately, we have
|
||||||
|
// no way of knowing which RRs are required so we set the TC bit if any RR
|
||||||
|
// had to be omitted from the response.
|
||||||
|
//
|
||||||
|
// The appropriate buffer size can be retrieved from the requests OPT
|
||||||
|
// record, if present, and is transport specific otherwise. dns.MinMsgSize
|
||||||
|
// should be used for UDP requests without an OPT record, and
|
||||||
|
// dns.MaxMsgSize for TCP requests without an OPT record.
|
||||||
|
func (dns *Msg) Truncate(size int) {
|
||||||
|
if dns.IsTsig() != nil {
|
||||||
|
// To simplify this implementation, we don't perform
|
||||||
|
// truncation on responses with a TSIG record.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 6891 mandates that the payload size in an OPT record
|
||||||
|
// less than 512 (MinMsgSize) bytes must be treated as equal to 512 bytes.
|
||||||
|
//
|
||||||
|
// For ease of use, we impose that restriction here.
|
||||||
|
if size < MinMsgSize {
|
||||||
|
size = MinMsgSize
|
||||||
|
}
|
||||||
|
|
||||||
|
l := msgLenWithCompressionMap(dns, nil) // uncompressed length
|
||||||
|
if l <= size {
|
||||||
|
// Don't waste effort compressing this message.
|
||||||
|
dns.Compress = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dns.Compress = true
|
||||||
|
|
||||||
|
edns0 := dns.popEdns0()
|
||||||
|
if edns0 != nil {
|
||||||
|
// Account for the OPT record that gets added at the end,
|
||||||
|
// by subtracting that length from our budget.
|
||||||
|
//
|
||||||
|
// The EDNS(0) OPT record must have the root domain and
|
||||||
|
// it's length is thus unaffected by compression.
|
||||||
|
size -= Len(edns0)
|
||||||
|
}
|
||||||
|
|
||||||
|
compression := make(map[string]struct{})
|
||||||
|
|
||||||
|
l = headerSize
|
||||||
|
for _, r := range dns.Question {
|
||||||
|
l += r.len(l, compression)
|
||||||
|
}
|
||||||
|
|
||||||
|
var numAnswer int
|
||||||
|
if l < size {
|
||||||
|
l, numAnswer = truncateLoop(dns.Answer, size, l, compression)
|
||||||
|
}
|
||||||
|
|
||||||
|
var numNS int
|
||||||
|
if l < size {
|
||||||
|
l, numNS = truncateLoop(dns.Ns, size, l, compression)
|
||||||
|
}
|
||||||
|
|
||||||
|
var numExtra int
|
||||||
|
if l < size {
|
||||||
|
_, numExtra = truncateLoop(dns.Extra, size, l, compression)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See the function documentation for when we set this.
|
||||||
|
dns.Truncated = dns.Truncated || len(dns.Answer) > numAnswer ||
|
||||||
|
len(dns.Ns) > numNS || len(dns.Extra) > numExtra
|
||||||
|
|
||||||
|
dns.Answer = dns.Answer[:numAnswer]
|
||||||
|
dns.Ns = dns.Ns[:numNS]
|
||||||
|
dns.Extra = dns.Extra[:numExtra]
|
||||||
|
|
||||||
|
if edns0 != nil {
|
||||||
|
// Add the OPT record back onto the additional section.
|
||||||
|
dns.Extra = append(dns.Extra, edns0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateLoop(rrs []RR, size, l int, compression map[string]struct{}) (int, int) {
|
||||||
|
for i, r := range rrs {
|
||||||
|
if r == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l += r.len(l, compression)
|
||||||
|
if l > size {
|
||||||
|
// Return size, rather than l prior to this record,
|
||||||
|
// to prevent any further records being added.
|
||||||
|
return size, i
|
||||||
|
}
|
||||||
|
if l == size {
|
||||||
|
return l, i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return l, len(rrs)
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRequestTruncateAnswer(t *testing.T) {
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("large.example.com.", TypeSRV)
|
||||||
|
|
||||||
|
reply := new(Msg)
|
||||||
|
reply.SetReply(m)
|
||||||
|
for i := 1; i < 200; i++ {
|
||||||
|
reply.Answer = append(reply.Answer, testRR(
|
||||||
|
fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Truncate(MinMsgSize)
|
||||||
|
if want, got := MinMsgSize, reply.Len(); want < got {
|
||||||
|
t.Errorf("message length should be below %d bytes, got %d bytes", want, got)
|
||||||
|
}
|
||||||
|
if !reply.Truncated {
|
||||||
|
t.Errorf("truncated bit should be set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestTruncateExtra(t *testing.T) {
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("large.example.com.", TypeSRV)
|
||||||
|
|
||||||
|
reply := new(Msg)
|
||||||
|
reply.SetReply(m)
|
||||||
|
for i := 1; i < 200; i++ {
|
||||||
|
reply.Extra = append(reply.Extra, testRR(
|
||||||
|
fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Truncate(MinMsgSize)
|
||||||
|
if want, got := MinMsgSize, reply.Len(); want < got {
|
||||||
|
t.Errorf("message length should be below %d bytes, got %d bytes", want, got)
|
||||||
|
}
|
||||||
|
if !reply.Truncated {
|
||||||
|
t.Errorf("truncated bit should be set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestTruncateExtraEdns0(t *testing.T) {
|
||||||
|
const size = 4096
|
||||||
|
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("large.example.com.", TypeSRV)
|
||||||
|
m.SetEdns0(size, true)
|
||||||
|
|
||||||
|
reply := new(Msg)
|
||||||
|
reply.SetReply(m)
|
||||||
|
for i := 1; i < 200; i++ {
|
||||||
|
reply.Extra = append(reply.Extra, testRR(
|
||||||
|
fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
|
||||||
|
}
|
||||||
|
reply.SetEdns0(size, true)
|
||||||
|
|
||||||
|
reply.Truncate(size)
|
||||||
|
if want, got := size, reply.Len(); want < got {
|
||||||
|
t.Errorf("message length should be below %d bytes, got %d bytes", want, got)
|
||||||
|
}
|
||||||
|
if !reply.Truncated {
|
||||||
|
t.Errorf("truncated bit should be set")
|
||||||
|
}
|
||||||
|
opt := reply.Extra[len(reply.Extra)-1]
|
||||||
|
if opt.Header().Rrtype != TypeOPT {
|
||||||
|
t.Errorf("expected last RR to be OPT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestTruncateExtraRegression(t *testing.T) {
|
||||||
|
const size = 2048
|
||||||
|
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("large.example.com.", TypeSRV)
|
||||||
|
m.SetEdns0(size, true)
|
||||||
|
|
||||||
|
reply := new(Msg)
|
||||||
|
reply.SetReply(m)
|
||||||
|
for i := 1; i < 33; i++ {
|
||||||
|
reply.Answer = append(reply.Answer, testRR(
|
||||||
|
fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
|
||||||
|
}
|
||||||
|
for i := 1; i < 33; i++ {
|
||||||
|
reply.Extra = append(reply.Extra, testRR(
|
||||||
|
fmt.Sprintf("10-0-0-%d.default.pod.k8s.example.com. 10 IN A 10.0.0.%d", i, i)))
|
||||||
|
}
|
||||||
|
reply.SetEdns0(size, true)
|
||||||
|
|
||||||
|
reply.Truncate(size)
|
||||||
|
if want, got := size, reply.Len(); want < got {
|
||||||
|
t.Errorf("message length should be below %d bytes, got %d bytes", want, got)
|
||||||
|
}
|
||||||
|
if !reply.Truncated {
|
||||||
|
t.Errorf("truncated bit should be set")
|
||||||
|
}
|
||||||
|
opt := reply.Extra[len(reply.Extra)-1]
|
||||||
|
if opt.Header().Rrtype != TypeOPT {
|
||||||
|
t.Errorf("expected last RR to be OPT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncation(t *testing.T) {
|
||||||
|
reply := new(Msg)
|
||||||
|
|
||||||
|
for i := 0; i < 61; i++ {
|
||||||
|
reply.Answer = append(reply.Answer, testRR(fmt.Sprintf("http.service.tcp.srv.k8s.example.org. 5 IN SRV 0 0 80 10-144-230-%d.default.pod.k8s.example.org.", i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
reply.Extra = append(reply.Extra, testRR(fmt.Sprintf("ip-10-10-52-5%d.subdomain.example.org. 5 IN A 10.10.52.5%d", i, i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
reply.Ns = append(reply.Ns, testRR(fmt.Sprintf("srv.subdomain.example.org. 5 IN NS ip-10-10-33-6%d.subdomain.example.org.", i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for bufsize := 1024; bufsize <= 4096; bufsize += 12 {
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("http.service.tcp.srv.k8s.example.org.", TypeSRV)
|
||||||
|
m.SetEdns0(uint16(bufsize), true)
|
||||||
|
|
||||||
|
copy := reply.Copy()
|
||||||
|
copy.SetReply(m)
|
||||||
|
|
||||||
|
copy.Truncate(bufsize)
|
||||||
|
if want, got := bufsize, copy.Len(); want < got {
|
||||||
|
t.Errorf("message length should be below %d bytes, got %d bytes", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequestTruncateAnswerExact(t *testing.T) {
|
||||||
|
const size = 867 // Bit fiddly, but this hits the rl == size break clause in Truncate, 52 RRs should remain.
|
||||||
|
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("large.example.com.", TypeSRV)
|
||||||
|
m.SetEdns0(size, false)
|
||||||
|
|
||||||
|
reply := new(Msg)
|
||||||
|
reply.SetReply(m)
|
||||||
|
for i := 1; i < 200; i++ {
|
||||||
|
reply.Answer = append(reply.Answer, testRR(fmt.Sprintf("large.example.com. 10 IN A 127.0.0.%d", i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Truncate(size)
|
||||||
|
if want, got := size, reply.Len(); want < got {
|
||||||
|
t.Errorf("message length should be below %d bytes, got %d bytes", want, got)
|
||||||
|
}
|
||||||
|
if expected := 52; len(reply.Answer) != expected {
|
||||||
|
t.Errorf("wrong number of answers; expected %d, got %d", expected, len(reply.Answer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMsgTruncate(b *testing.B) {
|
||||||
|
const size = 2048
|
||||||
|
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("example.com.", TypeA)
|
||||||
|
m.SetEdns0(size, true)
|
||||||
|
|
||||||
|
reply := new(Msg)
|
||||||
|
reply.SetReply(m)
|
||||||
|
for i := 1; i < 33; i++ {
|
||||||
|
reply.Answer = append(reply.Answer, testRR(
|
||||||
|
fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
|
||||||
|
}
|
||||||
|
for i := 1; i < 33; i++ {
|
||||||
|
reply.Extra = append(reply.Extra, testRR(
|
||||||
|
fmt.Sprintf("10-0-0-%d.default.pod.k8s.example.com. 10 IN A 10.0.0.%d", i, i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
b.StopTimer()
|
||||||
|
copy := reply.Copy()
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
copy.Truncate(size)
|
||||||
|
}
|
||||||
|
}
|
2
nsecx.go
2
nsecx.go
|
@ -43,7 +43,7 @@ func HashName(label string, ha uint8, iter uint16, salt string) string {
|
||||||
return toBase32(nsec3)
|
return toBase32(nsec3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cover returns true if a name is covered by the NSEC3 record
|
// Cover returns true if a name is covered by the NSEC3 record.
|
||||||
func (rr *NSEC3) Cover(name string) bool {
|
func (rr *NSEC3) Cover(name string) bool {
|
||||||
nameHash := HashName(name, rr.Hash, rr.Iterations, rr.Salt)
|
nameHash := HashName(name, rr.Hash, rr.Iterations, rr.Salt)
|
||||||
owner := strings.ToUpper(rr.Hdr.Name)
|
owner := strings.ToUpper(rr.Hdr.Name)
|
||||||
|
|
526
parse_test.go
526
parse_test.go
|
@ -245,8 +245,7 @@ func testTXTRRQuick(t *testing.T) {
|
||||||
rrbytes := make([]byte, 0, len(owner)+2+2+4+2+len(rdata))
|
rrbytes := make([]byte, 0, len(owner)+2+2+4+2+len(rdata))
|
||||||
rrbytes = append(rrbytes, owner...)
|
rrbytes = append(rrbytes, owner...)
|
||||||
rrbytes = append(rrbytes, typeAndClass...)
|
rrbytes = append(rrbytes, typeAndClass...)
|
||||||
rrbytes = append(rrbytes, byte(len(rdata)>>8))
|
rrbytes = append(rrbytes, byte(len(rdata)>>8), byte(len(rdata)))
|
||||||
rrbytes = append(rrbytes, byte(len(rdata)))
|
|
||||||
rrbytes = append(rrbytes, rdata...)
|
rrbytes = append(rrbytes, rdata...)
|
||||||
rr, _, err := UnpackRR(rrbytes, 0)
|
rr, _, err := UnpackRR(rrbytes, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -267,7 +266,7 @@ func testTXTRRQuick(t *testing.T) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if len(rdata) == 0 {
|
if len(rdata) == 0 {
|
||||||
// string'ing won't produce any data to parse
|
// stringifying won't produce any data to parse
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
rrString := rr.String()
|
rrString := rr.String()
|
||||||
|
@ -358,12 +357,29 @@ func TestNSEC(t *testing.T) {
|
||||||
t.Errorf("`%s' should be equal to\n`%s', but is `%s'", i, o, rr.String())
|
t.Errorf("`%s' should be equal to\n`%s', but is `%s'", i, o, rr.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
rr, err := NewRR("nl. IN NSEC3PARAM 1 0 5 30923C44C6CBBB8F")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to parse RR: ", err)
|
||||||
|
}
|
||||||
|
if nsec3param, ok := rr.(*NSEC3PARAM); ok {
|
||||||
|
if nsec3param.SaltLength != 8 {
|
||||||
|
t.Fatalf("nsec3param saltlen %d != 8", nsec3param.SaltLength)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Fatal("not nsec3 param: ", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLOC(t *testing.T) {
|
func TestParseLOC(t *testing.T) {
|
||||||
lt := map[string]string{
|
lt := map[string]string{
|
||||||
"SW1A2AA.find.me.uk. LOC 51 30 12.748 N 00 07 39.611 W 0.00m 0.00m 0.00m 0.00m": "SW1A2AA.find.me.uk.\t3600\tIN\tLOC\t51 30 12.748 N 00 07 39.611 W 0m 0.00m 0.00m 0.00m",
|
"SW1A2AA.find.me.uk. LOC 51 30 12.748 N 00 07 39.611 W 0.00m 0.00m 0.00m 0.00m": "SW1A2AA.find.me.uk.\t3600\tIN\tLOC\t51 30 12.748 N 00 07 39.611 W 0m 0.00m 0.00m 0.00m",
|
||||||
"SW1A2AA.find.me.uk. LOC 51 0 0.0 N 00 07 39.611 W 0.00m 0.00m 0.00m 0.00m": "SW1A2AA.find.me.uk.\t3600\tIN\tLOC\t51 00 0.000 N 00 07 39.611 W 0m 0.00m 0.00m 0.00m",
|
"SW1A2AA.find.me.uk. LOC 51 0 0.0 N 00 07 39.611 W 0.00m 0.00m 0.00m 0.00m": "SW1A2AA.find.me.uk.\t3600\tIN\tLOC\t51 00 0.000 N 00 07 39.611 W 0m 0.00m 0.00m 0.00m",
|
||||||
|
"SW1A2AA.find.me.uk. LOC 51 30 12.748 N 00 07 39.611 W 0.00m": "SW1A2AA.find.me.uk.\t3600\tIN\tLOC\t51 30 12.748 N 00 07 39.611 W 0m 1m 10000m 10m",
|
||||||
|
// Exercise boundary cases
|
||||||
|
"SW1A2AA.find.me.uk. LOC 90 0 0.0 N 180 0 0.0 W 42849672.95 90000000.00m 90000000.00m 90000000.00m": "SW1A2AA.find.me.uk.\t3600\tIN\tLOC\t90 00 0.000 N 180 00 0.000 W 42849672.95m 90000000m 90000000m 90000000m",
|
||||||
|
"SW1A2AA.find.me.uk. LOC 89 59 59.999 N 179 59 59.999 W -100000 90000000.00m 90000000.00m 90000000m": "SW1A2AA.find.me.uk.\t3600\tIN\tLOC\t89 59 59.999 N 179 59 59.999 W -100000m 90000000m 90000000m 90000000m",
|
||||||
|
// use float64 to have enough precision.
|
||||||
|
"example.com. LOC 42 21 43.952 N 71 5 6.344 W -24m 1m 200m 10m": "example.com.\t3600\tIN\tLOC\t42 21 43.952 N 71 05 6.344 W -24m 1m 200m 10m",
|
||||||
}
|
}
|
||||||
for i, o := range lt {
|
for i, o := range lt {
|
||||||
rr, err := NewRR(i)
|
rr, err := NewRR(i)
|
||||||
|
@ -375,6 +391,90 @@ func TestParseLOC(t *testing.T) {
|
||||||
t.Errorf("`%s' should be equal to\n`%s', but is `%s'", i, o, rr.String())
|
t.Errorf("`%s' should be equal to\n`%s', but is `%s'", i, o, rr.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalid cases (out of range values)
|
||||||
|
lt = map[string]string{ // Pair of (invalid) RDATA and the bad field name
|
||||||
|
// One of the subfields is out of range.
|
||||||
|
"91 0 0.0 N 00 07 39.611 W 0m": "Latitude",
|
||||||
|
"89 60 0.0 N 00 07 39.611 W 0m": "Latitude",
|
||||||
|
"89 00 60.0 N 00 07 39.611 W 0m": "Latitude",
|
||||||
|
"1 00 -1 N 00 07 39.611 W 0m": "Latitude",
|
||||||
|
"0 0 0.0 N 181 00 0.0 W 0m": "Longitude",
|
||||||
|
"0 0 0.0 N 179 60 0.0 W 0m": "Longitude",
|
||||||
|
"0 0 0.0 N 179 00 60.0 W 0m": "Longitude",
|
||||||
|
"0 0 0.0 N 1 00 -1 W 0m": "Longitude",
|
||||||
|
|
||||||
|
// Each subfield is valid, but resulting latitude would be out of range.
|
||||||
|
"90 01 00.0 N 00 07 39.611 W 0m": "Latitude",
|
||||||
|
"0 0 0.0 N 180 01 0.0 W 0m": "Longitude",
|
||||||
|
}
|
||||||
|
for rdata, field := range lt {
|
||||||
|
_, err := NewRR(fmt.Sprintf("example.com. LOC %s", rdata))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), field) {
|
||||||
|
t.Errorf("expected error to contain %q, but got %v", field, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this tests a subroutine for the LOC RR parser. It's complicated enough to test separately.
|
||||||
|
func TestStringToCm(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
// Test description: the input token and the expected return values from stringToCm.
|
||||||
|
token string
|
||||||
|
e uint8
|
||||||
|
m uint8
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"100", 4, 1, true},
|
||||||
|
{"0100", 4, 1, true}, // leading 0 (allowed)
|
||||||
|
{"100.99", 4, 1, true},
|
||||||
|
{"90000000", 9, 9, true},
|
||||||
|
{"90000000.00", 9, 9, true},
|
||||||
|
{"0", 0, 0, true},
|
||||||
|
{"0.00", 0, 0, true},
|
||||||
|
{"0.01", 0, 1, true},
|
||||||
|
{".01", 0, 1, true}, // empty 'meter' part (allowed)
|
||||||
|
{"0.1", 1, 1, true},
|
||||||
|
|
||||||
|
// out of range (too large)
|
||||||
|
{"90000001", 0, 0, false},
|
||||||
|
{"90000000.01", 0, 0, false},
|
||||||
|
|
||||||
|
// more than 2 digits in 'cmeter' part
|
||||||
|
{"0.000", 0, 0, false},
|
||||||
|
{"0.001", 0, 0, false},
|
||||||
|
{"0.999", 0, 0, false},
|
||||||
|
// with plus or minus sign (disallowed)
|
||||||
|
{"-100", 0, 0, false},
|
||||||
|
{"+100", 0, 0, false},
|
||||||
|
{"0.-10", 0, 0, false},
|
||||||
|
{"0.+10", 0, 0, false},
|
||||||
|
{"0a.00", 0, 0, false}, // invalid string for 'meter' part
|
||||||
|
{".1x", 0, 0, false}, // invalid string for 'cmeter' part
|
||||||
|
{".", 0, 0, false}, // empty 'cmeter' part (disallowed)
|
||||||
|
{"1.", 0, 0, false}, // ditto
|
||||||
|
{"m", 0, 0, false}, // only the "m" suffix
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.token, func(t *testing.T) {
|
||||||
|
// In all cases the expected result is the same with or without the 'm' suffix.
|
||||||
|
// So we test both cases using the same test code.
|
||||||
|
for _, sfx := range []string{"", "m"} {
|
||||||
|
token := tc.token + sfx
|
||||||
|
e, m, ok := stringToCm(token)
|
||||||
|
if ok != tc.ok {
|
||||||
|
t.Fatal("unexpected validation result")
|
||||||
|
}
|
||||||
|
if m != tc.m {
|
||||||
|
t.Fatalf("Expected %d, got %d", tc.m, m)
|
||||||
|
}
|
||||||
|
if e != tc.e {
|
||||||
|
t.Fatalf("Expected %d, got %d", tc.e, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseDS(t *testing.T) {
|
func TestParseDS(t *testing.T) {
|
||||||
|
@ -432,7 +532,7 @@ func TestParseClass(t *testing.T) {
|
||||||
"t.example.com. CH A 127.0.0.1": "t.example.com. 3600 CH A 127.0.0.1",
|
"t.example.com. CH A 127.0.0.1": "t.example.com. 3600 CH A 127.0.0.1",
|
||||||
// ClassANY can not occur in zone files
|
// ClassANY can not occur in zone files
|
||||||
// "t.example.com. ANY A 127.0.0.1": "t.example.com. 3600 ANY A 127.0.0.1",
|
// "t.example.com. ANY A 127.0.0.1": "t.example.com. 3600 ANY A 127.0.0.1",
|
||||||
"t.example.com. NONE A 127.0.0.1": "t.example.com. 3600 NONE A 127.0.0.1",
|
"t.example.com. NONE A 127.0.0.1": "t.example.com. 3600 NONE A 127.0.0.1",
|
||||||
"t.example.com. CLASS255 A 127.0.0.1": "t.example.com. 3600 CLASS255 A 127.0.0.1",
|
"t.example.com. CLASS255 A 127.0.0.1": "t.example.com. 3600 CLASS255 A 127.0.0.1",
|
||||||
}
|
}
|
||||||
for i, o := range tests {
|
for i, o := range tests {
|
||||||
|
@ -534,25 +634,25 @@ $TTL 314
|
||||||
example.com. DNAME 10 ; TTL=314 after second $TTL
|
example.com. DNAME 10 ; TTL=314 after second $TTL
|
||||||
`
|
`
|
||||||
reCaseFromComment := regexp.MustCompile(`TTL=(\d+)\s+(.*)`)
|
reCaseFromComment := regexp.MustCompile(`TTL=(\d+)\s+(.*)`)
|
||||||
records := ParseZone(strings.NewReader(zone), "", "")
|
z := NewZoneParser(strings.NewReader(zone), "", "")
|
||||||
var i int
|
var i int
|
||||||
for record := range records {
|
|
||||||
|
for rr, ok := z.Next(); ok; rr, ok = z.Next() {
|
||||||
i++
|
i++
|
||||||
if record.Error != nil {
|
expected := reCaseFromComment.FindStringSubmatch(z.Comment())
|
||||||
t.Error(record.Error)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
expected := reCaseFromComment.FindStringSubmatch(record.Comment)
|
|
||||||
if len(expected) != 3 {
|
if len(expected) != 3 {
|
||||||
t.Errorf("regexp didn't match for record %d", i)
|
t.Errorf("regexp didn't match for record %d", i)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
expectedTTL, _ := strconv.ParseUint(expected[1], 10, 32)
|
expectedTTL, _ := strconv.ParseUint(expected[1], 10, 32)
|
||||||
ttl := record.RR.Header().Ttl
|
ttl := rr.Header().Ttl
|
||||||
if ttl != uint32(expectedTTL) {
|
if ttl != uint32(expectedTTL) {
|
||||||
t.Errorf("%s: expected TTL %d, got %d", expected[2], expectedTTL, ttl)
|
t.Errorf("%s: expected TTL %d, got %d", expected[2], expectedTTL, ttl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := z.Err(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
if i != 10 {
|
if i != 10 {
|
||||||
t.Errorf("expected %d records, got %d", 5, i)
|
t.Errorf("expected %d records, got %d", 5, i)
|
||||||
}
|
}
|
||||||
|
@ -591,16 +691,12 @@ func TestRelativeNameErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, errorCase := range badZones {
|
for _, errorCase := range badZones {
|
||||||
entries := ParseZone(strings.NewReader(errorCase.zoneContents), "", "")
|
z := NewZoneParser(strings.NewReader(errorCase.zoneContents), "", "")
|
||||||
for entry := range entries {
|
z.Next()
|
||||||
if entry.Error == nil {
|
if err := z.Err(); err == nil {
|
||||||
t.Errorf("%s: expected error, got nil", errorCase.label)
|
t.Errorf("%s: expected error, got nil", errorCase.label)
|
||||||
continue
|
} else if !strings.Contains(err.Error(), errorCase.expectedErr) {
|
||||||
}
|
t.Errorf("%s: expected error `%s`, got `%s`", errorCase.label, errorCase.expectedErr, err)
|
||||||
err := entry.Error.err
|
|
||||||
if err != errorCase.expectedErr {
|
|
||||||
t.Errorf("%s: expected error `%s`, got `%s`", errorCase.label, errorCase.expectedErr, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -707,9 +803,13 @@ func TestRfc1982(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmpty(t *testing.T) {
|
func TestEmpty(t *testing.T) {
|
||||||
for range ParseZone(strings.NewReader(""), "", "") {
|
z := NewZoneParser(strings.NewReader(""), "", "")
|
||||||
|
for _, ok := z.Next(); ok; _, ok = z.Next() {
|
||||||
t.Errorf("should be empty")
|
t.Errorf("should be empty")
|
||||||
}
|
}
|
||||||
|
if err := z.Err(); err != nil {
|
||||||
|
t.Error("got an error when it shouldn't")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLowercaseTokens(t *testing.T) {
|
func TestLowercaseTokens(t *testing.T) {
|
||||||
|
@ -877,18 +977,20 @@ foo. IN DNSKEY 256 3 5 AwEAAb+8l ; this is comment 6
|
||||||
foo. IN NSEC miek.nl. TXT RRSIG NSEC; this is comment 7
|
foo. IN NSEC miek.nl. TXT RRSIG NSEC; this is comment 7
|
||||||
foo. IN TXT "THIS IS TEXT MAN"; this is comment 8
|
foo. IN TXT "THIS IS TEXT MAN"; this is comment 8
|
||||||
`
|
`
|
||||||
for x := range ParseZone(strings.NewReader(zone), ".", "") {
|
z := NewZoneParser(strings.NewReader(zone), ".", "")
|
||||||
if x.Error == nil {
|
for _, ok := z.Next(); ok; _, ok = z.Next() {
|
||||||
if x.Comment != "" {
|
if z.Comment() != "" {
|
||||||
if _, ok := comments[x.Comment]; !ok {
|
if _, okC := comments[z.Comment()]; !okC {
|
||||||
t.Errorf("wrong comment %q", x.Comment)
|
t.Errorf("wrong comment %q", z.Comment())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := z.Err(); err != nil {
|
||||||
|
t.Error("got an error when it shouldn't")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseZoneComments(t *testing.T) {
|
func TestZoneParserComments(t *testing.T) {
|
||||||
for i, test := range []struct {
|
for i, test := range []struct {
|
||||||
zone string
|
zone string
|
||||||
comments []string
|
comments []string
|
||||||
|
@ -963,24 +1065,25 @@ func TestParseZoneComments(t *testing.T) {
|
||||||
r := strings.NewReader(test.zone)
|
r := strings.NewReader(test.zone)
|
||||||
|
|
||||||
var j int
|
var j int
|
||||||
for r := range ParseZone(r, "", "") {
|
z := NewZoneParser(r, "", "")
|
||||||
if r.Error != nil {
|
for rr, ok := z.Next(); ok; rr, ok = z.Next() {
|
||||||
t.Fatal(r.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if j >= len(test.comments) {
|
if j >= len(test.comments) {
|
||||||
t.Fatalf("too many records for zone %d at %d record, expected %d", i, j+1, len(test.comments))
|
t.Fatalf("too many records for zone %d at %d record, expected %d", i, j+1, len(test.comments))
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Comment != test.comments[j] {
|
if z.Comment() != test.comments[j] {
|
||||||
t.Errorf("invalid comment for record %d:%d %v / %v", i, j, r.RR, r.Error)
|
t.Errorf("invalid comment for record %d:%d %v", i, j, rr)
|
||||||
t.Logf("expected %q", test.comments[j])
|
t.Logf("expected %q", test.comments[j])
|
||||||
t.Logf("got %q", r.Comment)
|
t.Logf("got %q", z.Comment())
|
||||||
}
|
}
|
||||||
|
|
||||||
j++
|
j++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := z.Err(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if j != len(test.comments) {
|
if j != len(test.comments) {
|
||||||
t.Errorf("too few records for zone %d, got %d, expected %d", i, j, len(test.comments))
|
t.Errorf("too few records for zone %d, got %d, expected %d", i, j, len(test.comments))
|
||||||
}
|
}
|
||||||
|
@ -1106,8 +1209,8 @@ func TestTypeXXXX(t *testing.T) {
|
||||||
t.Errorf("this should not work, for TYPE655341")
|
t.Errorf("this should not work, for TYPE655341")
|
||||||
}
|
}
|
||||||
_, err = NewRR("example.com IN TYPE1 \\# 4 0a000001")
|
_, err = NewRR("example.com IN TYPE1 \\# 4 0a000001")
|
||||||
if err == nil {
|
if err != nil {
|
||||||
t.Errorf("this should not work")
|
t.Errorf("failed to parse TYPE1 RR: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1210,9 +1313,8 @@ func TestNewPrivateKey(t *testing.T) {
|
||||||
algorithms := []algorithm{
|
algorithms := []algorithm{
|
||||||
{ECDSAP256SHA256, 256},
|
{ECDSAP256SHA256, 256},
|
||||||
{ECDSAP384SHA384, 384},
|
{ECDSAP384SHA384, 384},
|
||||||
{RSASHA1, 1024},
|
{RSASHA1, 512},
|
||||||
{RSASHA256, 2048},
|
{RSASHA256, 512},
|
||||||
{DSA, 1024},
|
|
||||||
{ED25519, 256},
|
{ED25519, 256},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1413,10 +1515,10 @@ func TestParseSSHFP(t *testing.T) {
|
||||||
|
|
||||||
func TestParseHINFO(t *testing.T) {
|
func TestParseHINFO(t *testing.T) {
|
||||||
dt := map[string]string{
|
dt := map[string]string{
|
||||||
"example.net. HINFO A B": "example.net. 3600 IN HINFO \"A\" \"B\"",
|
"example.net. HINFO A B": "example.net. 3600 IN HINFO \"A\" \"B\"",
|
||||||
"example.net. HINFO \"A\" \"B\"": "example.net. 3600 IN HINFO \"A\" \"B\"",
|
"example.net. HINFO \"A\" \"B\"": "example.net. 3600 IN HINFO \"A\" \"B\"",
|
||||||
"example.net. HINFO A B C D E F": "example.net. 3600 IN HINFO \"A\" \"B C D E F\"",
|
"example.net. HINFO A B C D E F": "example.net. 3600 IN HINFO \"A\" \"B C D E F\"",
|
||||||
"example.net. HINFO AB": "example.net. 3600 IN HINFO \"AB\" \"\"",
|
"example.net. HINFO AB": "example.net. 3600 IN HINFO \"AB\" \"\"",
|
||||||
// "example.net. HINFO PC-Intel-700mhz \"Redhat Linux 7.1\"": "example.net. 3600 IN HINFO \"PC-Intel-700mhz\" \"Redhat Linux 7.1\"",
|
// "example.net. HINFO PC-Intel-700mhz \"Redhat Linux 7.1\"": "example.net. 3600 IN HINFO \"PC-Intel-700mhz\" \"Redhat Linux 7.1\"",
|
||||||
// This one is recommended in Pro Bind book http://www.zytrax.com/books/dns/ch8/hinfo.html
|
// This one is recommended in Pro Bind book http://www.zytrax.com/books/dns/ch8/hinfo.html
|
||||||
// but effectively, even Bind would replace it to correctly formed text when you AXFR
|
// but effectively, even Bind would replace it to correctly formed text when you AXFR
|
||||||
|
@ -1436,9 +1538,9 @@ func TestParseHINFO(t *testing.T) {
|
||||||
|
|
||||||
func TestParseCAA(t *testing.T) {
|
func TestParseCAA(t *testing.T) {
|
||||||
lt := map[string]string{
|
lt := map[string]string{
|
||||||
"example.net. CAA 0 issue \"symantec.com\"": "example.net.\t3600\tIN\tCAA\t0 issue \"symantec.com\"",
|
"example.net. CAA 0 issue \"symantec.com\"": "example.net.\t3600\tIN\tCAA\t0 issue \"symantec.com\"",
|
||||||
"example.net. CAA 0 issuewild \"symantec.com; stuff\"": "example.net.\t3600\tIN\tCAA\t0 issuewild \"symantec.com; stuff\"",
|
"example.net. CAA 0 issuewild \"symantec.com; stuff\"": "example.net.\t3600\tIN\tCAA\t0 issuewild \"symantec.com; stuff\"",
|
||||||
"example.net. CAA 128 tbs \"critical\"": "example.net.\t3600\tIN\tCAA\t128 tbs \"critical\"",
|
"example.net. CAA 128 tbs \"critical\"": "example.net.\t3600\tIN\tCAA\t128 tbs \"critical\"",
|
||||||
"example.net. CAA 2 auth \"0>09\\006\\010+\\006\\001\\004\\001\\214y\\002\\003\\001\\006\\009`\\134H\\001e\\003\\004\\002\\001\\004 y\\209\\012\\221r\\220\\156Q\\218\\150\\150{\\166\\245:\\231\\182%\\157:\\133\\179}\\1923r\\238\\151\\255\\128q\\145\\002\\001\\000\"": "example.net.\t3600\tIN\tCAA\t2 auth \"0>09\\006\\010+\\006\\001\\004\\001\\214y\\002\\003\\001\\006\\009`\\134H\\001e\\003\\004\\002\\001\\004 y\\209\\012\\221r\\220\\156Q\\218\\150\\150{\\166\\245:\\231\\182%\\157:\\133\\179}\\1923r\\238\\151\\255\\128q\\145\\002\\001\\000\"",
|
"example.net. CAA 2 auth \"0>09\\006\\010+\\006\\001\\004\\001\\214y\\002\\003\\001\\006\\009`\\134H\\001e\\003\\004\\002\\001\\004 y\\209\\012\\221r\\220\\156Q\\218\\150\\150{\\166\\245:\\231\\182%\\157:\\133\\179}\\1923r\\238\\151\\255\\128q\\145\\002\\001\\000\"": "example.net.\t3600\tIN\tCAA\t2 auth \"0>09\\006\\010+\\006\\001\\004\\001\\214y\\002\\003\\001\\006\\009`\\134H\\001e\\003\\004\\002\\001\\004 y\\209\\012\\221r\\220\\156Q\\218\\150\\150{\\166\\245:\\231\\182%\\157:\\133\\179}\\1923r\\238\\151\\255\\128q\\145\\002\\001\\000\"",
|
||||||
"example.net. TYPE257 0 issue \"symantec.com\"": "example.net.\t3600\tIN\tCAA\t0 issue \"symantec.com\"",
|
"example.net. TYPE257 0 issue \"symantec.com\"": "example.net.\t3600\tIN\tCAA\t0 issue \"symantec.com\"",
|
||||||
}
|
}
|
||||||
|
@ -1532,6 +1634,91 @@ func TestParseCSYNC(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSVCB(t *testing.T) {
|
||||||
|
svcbs := map[string]string{
|
||||||
|
`example.com. 3600 IN SVCB 0 cloudflare.com.`: `example.com. 3600 IN SVCB 0 cloudflare.com.`,
|
||||||
|
`example.com. 3600 IN SVCB 65000 cloudflare.com. alpn=h2 ipv4hint=3.4.3.2`: `example.com. 3600 IN SVCB 65000 cloudflare.com. alpn="h2" ipv4hint="3.4.3.2"`,
|
||||||
|
`example.com. 3600 IN SVCB 65000 cloudflare.com. key65000=4\ 3 key65001="\" " key65002 key65003= key65004="" key65005== key65006==\"\" key65007=\254 key65008=\032`: `example.com. 3600 IN SVCB 65000 cloudflare.com. key65000="4\ 3" key65001="\"\ " key65002="" key65003="" key65004="" key65005="=" key65006="=\"\"" key65007="\254" key65008="\ "`,
|
||||||
|
// Explained in svcb.go "In AliasMode, records SHOULD NOT include any SvcParams,"
|
||||||
|
`example.com. 3600 IN SVCB 0 no-default-alpn`: `example.com. 3600 IN SVCB 0 no-default-alpn.`,
|
||||||
|
// From the specification
|
||||||
|
`example.com. HTTPS 0 foo.example.com.`: `example.com. 3600 IN HTTPS 0 foo.example.com.`,
|
||||||
|
`example.com. SVCB 1 .`: `example.com. 3600 IN SVCB 1 .`,
|
||||||
|
`example.com. SVCB 16 foo.example.com. port=53`: `example.com. 3600 IN SVCB 16 foo.example.com. port="53"`,
|
||||||
|
`example.com. SVCB 1 foo.example.com. key667=hello`: `example.com. 3600 IN SVCB 1 foo.example.com. key667="hello"`,
|
||||||
|
`example.com. SVCB 1 foo.example.com. key667="hello\210qoo"`: `example.com. 3600 IN SVCB 1 foo.example.com. key667="hello\210qoo"`,
|
||||||
|
`example.com. SVCB 1 foo.example.com. ipv6hint="2001:db8::1,2001:db8::53:1"`: `example.com. 3600 IN SVCB 1 foo.example.com. ipv6hint="2001:db8::1,2001:db8::53:1"`,
|
||||||
|
`example.com. SVCB 1 example.com. ipv6hint="2001:db8::198.51.100.100"`: `example.com. 3600 IN SVCB 1 example.com. ipv6hint="2001:db8::c633:6464"`,
|
||||||
|
`example.com. SVCB 16 foo.example.org. alpn=h2,h3-19 mandatory=ipv4hint,alpn ipv4hint=192.0.2.1`: `example.com. 3600 IN SVCB 16 foo.example.org. alpn="h2,h3-19" mandatory="ipv4hint,alpn" ipv4hint="192.0.2.1"`,
|
||||||
|
`example.com. SVCB 16 foo.example.org. alpn="f\\\\oo\\,bar,h2"`: `example.com. 3600 IN SVCB 16 foo.example.org. alpn="f\\\092oo\\\044bar,h2"`,
|
||||||
|
`example.com. SVCB 16 foo.example.org. alpn=f\\\092oo\092,bar,h2`: `example.com. 3600 IN SVCB 16 foo.example.org. alpn="f\\\092oo\\\044bar,h2"`,
|
||||||
|
// From draft-ietf-add-ddr-06
|
||||||
|
`_dns.example.net. SVCB 1 example.net. alpn=h2 dohpath=/dns-query{?dns}`: `_dns.example.net. 3600 IN SVCB 1 example.net. alpn="h2" dohpath="/dns-query{?dns}"`,
|
||||||
|
`_dns.example.net. SVCB 1 example.net. alpn=h2 dohpath=/dns\045query{\?dns}`: `_dns.example.net. 3600 IN SVCB 1 example.net. alpn="h2" dohpath="/dns-query{?dns}"`,
|
||||||
|
}
|
||||||
|
for s, o := range svcbs {
|
||||||
|
rr, err := NewRR(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to parse RR: ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rr.String() != o {
|
||||||
|
t.Errorf("`%s' should be equal to\n`%s', but is `%s'", s, o, rr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBadSVCB(t *testing.T) {
|
||||||
|
header := `example.com. 3600 IN HTTPS `
|
||||||
|
evils := []string{
|
||||||
|
`65536 . no-default-alpn`, // bad priority
|
||||||
|
`1 ..`, // bad domain
|
||||||
|
`1 . no-default-alpn=1`, // value illegal
|
||||||
|
`1 . key`, // invalid key
|
||||||
|
`1 . key=`, // invalid key
|
||||||
|
`1 . =`, // invalid key
|
||||||
|
`1 . ==`, // invalid key
|
||||||
|
`1 . =a`, // invalid key
|
||||||
|
`1 . ""`, // invalid key
|
||||||
|
`1 . ""=`, // invalid key
|
||||||
|
`1 . "a"`, // invalid key
|
||||||
|
`1 . "a"=`, // invalid key
|
||||||
|
`1 . key1=`, // we know that key
|
||||||
|
`1 . key65535`, // key reserved
|
||||||
|
`1 . key065534`, // key can't be padded
|
||||||
|
`1 . key65534="f`, // unterminated value
|
||||||
|
`1 . key65534="`, // unterminated value
|
||||||
|
`1 . key65534=\2`, // invalid numeric escape
|
||||||
|
`1 . key65534=\24`, // invalid numeric escape
|
||||||
|
`1 . key65534=\256`, // invalid numeric escape
|
||||||
|
`1 . key65534=\`, // invalid numeric escape
|
||||||
|
`1 . key65534=""alpn`, // zQuote ending needs whitespace
|
||||||
|
`1 . key65534="a"alpn`, // zQuote ending needs whitespace
|
||||||
|
`1 . ipv6hint=1.1.1.1`, // not ipv6
|
||||||
|
`1 . ipv6hint=1:1:1:1`, // not ipv6
|
||||||
|
`1 . ipv6hint=a`, // not ipv6
|
||||||
|
`1 . ipv6hint=`, // empty ipv6
|
||||||
|
`1 . ipv4hint=1.1.1.1.1`, // not ipv4
|
||||||
|
`1 . ipv4hint=::fc`, // not ipv4
|
||||||
|
`1 . ipv4hint=..11`, // not ipv4
|
||||||
|
`1 . ipv4hint=a`, // not ipv4
|
||||||
|
`1 . ipv4hint=`, // empty ipv4
|
||||||
|
`1 . port=`, // empty port
|
||||||
|
`1 . echconfig=YUd`, // bad base64
|
||||||
|
`1 . alpn=h\`, // unterminated escape
|
||||||
|
`1 . alpn=h2\\.h3`, // comma-separated list with bad character
|
||||||
|
`1 . alpn=h2,,h3`, // empty protocol identifier
|
||||||
|
`1 . alpn=h3,`, // final protocol identifier empty
|
||||||
|
}
|
||||||
|
for _, o := range evils {
|
||||||
|
_, err := NewRR(header + o)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("failed to reject invalid RR: ", header+o)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseBadNAPTR(t *testing.T) {
|
func TestParseBadNAPTR(t *testing.T) {
|
||||||
// Should look like: mplus.ims.vodafone.com. 3600 IN NAPTR 10 100 "S" "SIP+D2U" "" _sip._udp.mplus.ims.vodafone.com.
|
// Should look like: mplus.ims.vodafone.com. 3600 IN NAPTR 10 100 "S" "SIP+D2U" "" _sip._udp.mplus.ims.vodafone.com.
|
||||||
naptr := `mplus.ims.vodafone.com. 3600 IN NAPTR 10 100 S SIP+D2U _sip._udp.mplus.ims.vodafone.com.`
|
naptr := `mplus.ims.vodafone.com. 3600 IN NAPTR 10 100 S SIP+D2U _sip._udp.mplus.ims.vodafone.com.`
|
||||||
|
@ -1585,3 +1772,246 @@ func TestNULLRecord(t *testing.T) {
|
||||||
t.Fatalf("Expected packet to contain NULL record")
|
t.Fatalf("Expected packet to contain NULL record")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseAPL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"v4",
|
||||||
|
". APL 1:192.0.2.0/24",
|
||||||
|
".\t3600\tIN\tAPL\t1:192.0.2.0/24",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v6",
|
||||||
|
". APL 2:2001:db8::/32",
|
||||||
|
".\t3600\tIN\tAPL\t2:2001:db8::/32",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"null v6",
|
||||||
|
". APL 2:::/0",
|
||||||
|
".\t3600\tIN\tAPL\t2:::/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"null v4",
|
||||||
|
". APL 1:0.0.0.0/0",
|
||||||
|
".\t3600\tIN\tAPL\t1:0.0.0.0/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"full v6",
|
||||||
|
". APL 2:::/0",
|
||||||
|
".\t3600\tIN\tAPL\t2:::/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"full v4",
|
||||||
|
". APL 1:192.0.2.1/32",
|
||||||
|
".\t3600\tIN\tAPL\t1:192.0.2.1/32",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"full v6",
|
||||||
|
". APL 2:2001:0db8:d2b4:b6ba:50db:49cc:a8d1:5bb1/128",
|
||||||
|
".\t3600\tIN\tAPL\t2:2001:db8:d2b4:b6ba:50db:49cc:a8d1:5bb1/128",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v4in6",
|
||||||
|
". APL 2:::ffff:192.0.2.0/120",
|
||||||
|
".\t3600\tIN\tAPL\t2:::ffff:192.0.2.0/120",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v4in6 v6 syntax",
|
||||||
|
". APL 2:::ffff:c000:0200/120",
|
||||||
|
".\t3600\tIN\tAPL\t2:::ffff:192.0.2.0/120",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"negate",
|
||||||
|
". APL !1:192.0.2.0/24",
|
||||||
|
".\t3600\tIN\tAPL\t!1:192.0.2.0/24",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multiple",
|
||||||
|
". APL 1:192.0.2.0/24 !1:192.0.2.1/32 2:2001:db8::/32 !2:2001:db8:1::0/48",
|
||||||
|
".\t3600\tIN\tAPL\t1:192.0.2.0/24 !1:192.0.2.1/32 2:2001:db8::/32 !2:2001:db8:1::/48",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no address",
|
||||||
|
". APL",
|
||||||
|
".\t3600\tIN\tAPL\t",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
rr, err := NewRR(tc.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse RR: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := rr.String()
|
||||||
|
if got != tc.expect {
|
||||||
|
t.Errorf("expected %q, got %q", tc.expect, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseAPLErrors(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"unexpected",
|
||||||
|
`. APL ""`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unrecognized family",
|
||||||
|
". APL 3:0.0.0.0/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"malformed family",
|
||||||
|
". APL foo:0.0.0.0/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"malformed address",
|
||||||
|
". APL 1:192.0.2/16",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"extra bits",
|
||||||
|
". APL 2:2001:db8::/0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address mismatch v2",
|
||||||
|
". APL 1:2001:db8::/64",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address mismatch v6",
|
||||||
|
". APL 2:192.0.2.1/32",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no prefix",
|
||||||
|
". APL 1:192.0.2.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no family",
|
||||||
|
". APL 0.0.0.0/0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, err := NewRR(tc.in)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnpackRRWithHeaderInvalidLengths(t *testing.T) {
|
||||||
|
rr, err := NewRR("test.example.org. 300 IN SSHFP 1 2 BC6533CDC95A79078A39A56EA7635984ED655318ADA9B6159E30723665DA95BB")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse SSHFP record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, Len(rr))
|
||||||
|
headerEnd, end, err := packRR(rr, buf, 0, compressionMap{}, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to pack A record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr.Header().Rdlength = uint16(end - headerEnd)
|
||||||
|
for _, off := range []int{
|
||||||
|
-1,
|
||||||
|
end + 1,
|
||||||
|
1<<16 - 1,
|
||||||
|
} {
|
||||||
|
_, _, err := UnpackRRWithHeader(*rr.Header(), buf, off)
|
||||||
|
if de, ok := err.(*Error); !ok || de.err != "bad off" {
|
||||||
|
t.Errorf("UnpackRRWithHeader with bad offset (%d) returned wrong or no error: %v", off, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rdlength := range []uint16{
|
||||||
|
uint16(end - headerEnd + 1),
|
||||||
|
uint16(end),
|
||||||
|
1<<16 - 1,
|
||||||
|
} {
|
||||||
|
rr.Header().Rdlength = rdlength
|
||||||
|
|
||||||
|
_, _, err := UnpackRRWithHeader(*rr.Header(), buf, headerEnd)
|
||||||
|
if de, ok := err.(*Error); !ok || de.err != "bad rdlength" {
|
||||||
|
t.Errorf("UnpackRRWithHeader with bad rdlength (%d) returned wrong or no error: %v", rdlength, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseZONEMD(t *testing.T) {
|
||||||
|
// Uses examples from https://tools.ietf.org/html/rfc8976
|
||||||
|
dt := map[string]string{
|
||||||
|
// Simple Zone
|
||||||
|
`example. 86400 IN ZONEMD 2018031900 1 1 (
|
||||||
|
c68090d90a7aed71
|
||||||
|
6bc459f9340e3d7c
|
||||||
|
1370d4d24b7e2fc3
|
||||||
|
a1ddc0b9a87153b9
|
||||||
|
a9713b3c9ae5cc27
|
||||||
|
777f98b8e730044c )
|
||||||
|
`: "example.\t86400\tIN\tZONEMD\t2018031900 1 1 c68090d90a7aed716bc459f9340e3d7c1370d4d24b7e2fc3a1ddc0b9a87153b9a9713b3c9ae5cc27777f98b8e730044c",
|
||||||
|
// Complex Zone
|
||||||
|
`example. 86400 IN ZONEMD 2018031900 1 1 (
|
||||||
|
a3b69bad980a3504
|
||||||
|
e1cffcb0fd6397f9
|
||||||
|
3848071c93151f55
|
||||||
|
2ae2f6b1711d4bd2
|
||||||
|
d8b39808226d7b9d
|
||||||
|
b71e34b72077f8fe )
|
||||||
|
`: "example.\t86400\tIN\tZONEMD\t2018031900 1 1 a3b69bad980a3504e1cffcb0fd6397f93848071c93151f552ae2f6b1711d4bd2d8b39808226d7b9db71e34b72077f8fe",
|
||||||
|
// Multiple Digests Zone
|
||||||
|
`example. 86400 IN ZONEMD 2018031900 1 1 (
|
||||||
|
62e6cf51b02e54b9
|
||||||
|
b5f967d547ce4313
|
||||||
|
6792901f9f88e637
|
||||||
|
493daaf401c92c27
|
||||||
|
9dd10f0edb1c56f8
|
||||||
|
080211f8480ee306 )
|
||||||
|
`: "example.\t86400\tIN\tZONEMD\t2018031900 1 1 62e6cf51b02e54b9b5f967d547ce43136792901f9f88e637493daaf401c92c279dd10f0edb1c56f8080211f8480ee306",
|
||||||
|
`example. 86400 IN ZONEMD 2018031900 1 2 (
|
||||||
|
08cfa1115c7b948c
|
||||||
|
4163a901270395ea
|
||||||
|
226a930cd2cbcf2f
|
||||||
|
a9a5e6eb85f37c8a
|
||||||
|
4e114d884e66f176
|
||||||
|
eab121cb02db7d65
|
||||||
|
2e0cc4827e7a3204
|
||||||
|
f166b47e5613fd27 )
|
||||||
|
`: "example.\t86400\tIN\tZONEMD\t2018031900 1 2 08cfa1115c7b948c4163a901270395ea226a930cd2cbcf2fa9a5e6eb85f37c8a4e114d884e66f176eab121cb02db7d652e0cc4827e7a3204f166b47e5613fd27",
|
||||||
|
`example. 86400 IN ZONEMD 2018031900 1 240 (
|
||||||
|
e2d523f654b9422a
|
||||||
|
96c5a8f44607bbee )
|
||||||
|
`: "example. 86400 IN ZONEMD 2018031900 1 240 e2d523f654b9422a96c5a8f44607bbee",
|
||||||
|
`example. 86400 IN ZONEMD 2018031900 241 1 (
|
||||||
|
e1846540e33a9e41
|
||||||
|
89792d18d5d131f6
|
||||||
|
05fc283e )
|
||||||
|
`: "example. 86400 IN ZONEMD 2018031900 241 1 e1846540e33a9e4189792d18d5d131f605fc283e",
|
||||||
|
// URI.ARPA zone
|
||||||
|
`uri.arpa. 3600 IN ZONEMD 2018100702 1 1 (
|
||||||
|
0dbc3c4dbfd75777c12ca19c337854b1577799901307c482e9d91d5d15
|
||||||
|
cd934d16319d98e30c4201cf25a1d5a0254960 )`: "uri.arpa.\t3600\tIN\tZONEMD\t2018100702 1 1 0dbc3c4dbfd75777c12ca19c337854b1577799901307c482e9d91d5d15cd934d16319d98e30c4201cf25a1d5a0254960",
|
||||||
|
// ROOT-SERVERS.NET Zone
|
||||||
|
`root-servers.net. 3600000 IN ZONEMD 2018091100 1 1 (
|
||||||
|
f1ca0ccd91bd5573d9f431c00ee0101b2545c97602be0a97
|
||||||
|
8a3b11dbfc1c776d5b3e86ae3d973d6b5349ba7f04340f79 )
|
||||||
|
`: "root-servers.net.\t3600000\tIN\tZONEMD\t2018091100 1 1 f1ca0ccd91bd5573d9f431c00ee0101b2545c97602be0a978a3b11dbfc1c776d5b3e86ae3d973d6b5349ba7f04340f79",
|
||||||
|
}
|
||||||
|
for i, o := range dt {
|
||||||
|
rr, err := NewRR(i)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to parse RR: ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rr.String() != o {
|
||||||
|
t.Errorf("`%s' should be equal to\n`%s', but is `%s'", i, o, rr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
43
privaterr.go
43
privaterr.go
|
@ -1,24 +1,20 @@
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import "strings"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PrivateRdata is an interface used for implementing "Private Use" RR types, see
|
// PrivateRdata is an interface used for implementing "Private Use" RR types, see
|
||||||
// RFC 6895. This allows one to experiment with new RR types, without requesting an
|
// RFC 6895. This allows one to experiment with new RR types, without requesting an
|
||||||
// official type code. Also see dns.PrivateHandle and dns.PrivateHandleRemove.
|
// official type code. Also see dns.PrivateHandle and dns.PrivateHandleRemove.
|
||||||
type PrivateRdata interface {
|
type PrivateRdata interface {
|
||||||
// String returns the text presentaton of the Rdata of the Private RR.
|
// String returns the text presentation of the Rdata of the Private RR.
|
||||||
String() string
|
String() string
|
||||||
// Parse parses the Rdata of the private RR.
|
// Parse parses the Rdata of the private RR.
|
||||||
Parse([]string) error
|
Parse([]string) error
|
||||||
// Pack is used when packing a private RR into a buffer.
|
// Pack is used when packing a private RR into a buffer.
|
||||||
Pack([]byte) (int, error)
|
Pack([]byte) (int, error)
|
||||||
// Unpack is used when unpacking a private RR from a buffer.
|
// Unpack is used when unpacking a private RR from a buffer.
|
||||||
// TODO(miek): diff. signature than Pack, see edns0.go for instance.
|
|
||||||
Unpack([]byte) (int, error)
|
Unpack([]byte) (int, error)
|
||||||
// Copy copies the Rdata.
|
// Copy copies the Rdata into the PrivateRdata argument.
|
||||||
Copy(PrivateRdata) error
|
Copy(PrivateRdata) error
|
||||||
// Len returns the length in octets of the Rdata.
|
// Len returns the length in octets of the Rdata.
|
||||||
Len() int
|
Len() int
|
||||||
|
@ -29,22 +25,8 @@ type PrivateRdata interface {
|
||||||
type PrivateRR struct {
|
type PrivateRR struct {
|
||||||
Hdr RR_Header
|
Hdr RR_Header
|
||||||
Data PrivateRdata
|
Data PrivateRdata
|
||||||
}
|
|
||||||
|
|
||||||
func mkPrivateRR(rrtype uint16) *PrivateRR {
|
generator func() PrivateRdata // for copy
|
||||||
// Panics if RR is not an instance of PrivateRR.
|
|
||||||
rrfunc, ok := TypeToRR[rrtype]
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("dns: invalid operation with Private RR type %d", rrtype))
|
|
||||||
}
|
|
||||||
|
|
||||||
anyrr := rrfunc()
|
|
||||||
rr, ok := anyrr.(*PrivateRR)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("dns: RR is not a PrivateRR, TypeToRR[%d] generator returned %T", rrtype, anyrr))
|
|
||||||
}
|
|
||||||
|
|
||||||
return rr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header return the RR header of r.
|
// Header return the RR header of r.
|
||||||
|
@ -61,13 +43,12 @@ func (r *PrivateRR) len(off int, compression map[string]struct{}) int {
|
||||||
|
|
||||||
func (r *PrivateRR) copy() RR {
|
func (r *PrivateRR) copy() RR {
|
||||||
// make new RR like this:
|
// make new RR like this:
|
||||||
rr := mkPrivateRR(r.Hdr.Rrtype)
|
rr := &PrivateRR{r.Hdr, r.generator(), r.generator}
|
||||||
rr.Hdr = r.Hdr
|
|
||||||
|
|
||||||
err := r.Data.Copy(rr.Data)
|
if err := r.Data.Copy(rr.Data); err != nil {
|
||||||
if err != nil {
|
panic("dns: got value that could not be used to copy Private rdata: " + err.Error())
|
||||||
panic("dns: got value that could not be used to copy Private rdata")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rr
|
return rr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +67,7 @@ func (r *PrivateRR) unpack(msg []byte, off int) (int, error) {
|
||||||
return off, err
|
return off, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PrivateRR) parse(c *zlexer, origin, file string) *ParseError {
|
func (r *PrivateRR) parse(c *zlexer, origin string) *ParseError {
|
||||||
var l lex
|
var l lex
|
||||||
text := make([]string, 0, 2) // could be 0..N elements, median is probably 1
|
text := make([]string, 0, 2) // could be 0..N elements, median is probably 1
|
||||||
Fetch:
|
Fetch:
|
||||||
|
@ -103,20 +84,20 @@ Fetch:
|
||||||
|
|
||||||
err := r.Data.Parse(text)
|
err := r.Data.Parse(text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ParseError{file, err.Error(), l}
|
return &ParseError{"", err.Error(), l}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r1 *PrivateRR) isDuplicate(r2 RR) bool { return false }
|
func (r *PrivateRR) isDuplicate(r2 RR) bool { return false }
|
||||||
|
|
||||||
// PrivateHandle registers a private resource record type. It requires
|
// PrivateHandle registers a private resource record type. It requires
|
||||||
// string and numeric representation of private RR type and generator function as argument.
|
// string and numeric representation of private RR type and generator function as argument.
|
||||||
func PrivateHandle(rtypestr string, rtype uint16, generator func() PrivateRdata) {
|
func PrivateHandle(rtypestr string, rtype uint16, generator func() PrivateRdata) {
|
||||||
rtypestr = strings.ToUpper(rtypestr)
|
rtypestr = strings.ToUpper(rtypestr)
|
||||||
|
|
||||||
TypeToRR[rtype] = func() RR { return &PrivateRR{RR_Header{}, generator()} }
|
TypeToRR[rtype] = func() RR { return &PrivateRR{RR_Header{}, generator(), generator} }
|
||||||
TypeToString[rtype] = rtypestr
|
TypeToString[rtype] = rtypestr
|
||||||
StringToType[rtypestr] = rtype
|
StringToType[rtypestr] = rtype
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,9 +158,11 @@ func TestPrivateZoneParser(t *testing.T) {
|
||||||
defer dns.PrivateHandleRemove(TypeVERSION)
|
defer dns.PrivateHandleRemove(TypeVERSION)
|
||||||
|
|
||||||
r := strings.NewReader(smallzone)
|
r := strings.NewReader(smallzone)
|
||||||
for x := range dns.ParseZone(r, ".", "") {
|
z := dns.NewZoneParser(r, ".", "")
|
||||||
if err := x.Error; err != nil {
|
|
||||||
t.Fatal(err)
|
for _, ok := z.Next(); ok; _, ok = z.Next() {
|
||||||
}
|
}
|
||||||
|
if err := z.Err(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
package dns
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
const LinodeAddr = "176.58.119.54:53"
|
|
||||||
|
|
||||||
func TestClientRemote(t *testing.T) {
|
|
||||||
m := new(Msg)
|
|
||||||
m.SetQuestion("go.dns.miek.nl.", TypeTXT)
|
|
||||||
|
|
||||||
c := new(Client)
|
|
||||||
r, _, err := c.Exchange(m, LinodeAddr)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed to exchange: %v", err)
|
|
||||||
}
|
|
||||||
if r != nil && r.Rcode != RcodeSuccess {
|
|
||||||
t.Errorf("failed to get an valid answer\n%v", r)
|
|
||||||
}
|
|
||||||
}
|
|
251
scan.go
251
scan.go
|
@ -87,31 +87,18 @@ type lex struct {
|
||||||
column int // column in the file
|
column int // column in the file
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token holds the token that are returned when a zone file is parsed.
|
|
||||||
type Token struct {
|
|
||||||
// The scanned resource record when error is not nil.
|
|
||||||
RR
|
|
||||||
// When an error occurred, this has the error specifics.
|
|
||||||
Error *ParseError
|
|
||||||
// A potential comment positioned after the RR and on the same line.
|
|
||||||
Comment string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ttlState describes the state necessary to fill in an omitted RR TTL
|
// ttlState describes the state necessary to fill in an omitted RR TTL
|
||||||
type ttlState struct {
|
type ttlState struct {
|
||||||
ttl uint32 // ttl is the current default TTL
|
ttl uint32 // ttl is the current default TTL
|
||||||
isByDirective bool // isByDirective indicates whether ttl was set by a $TTL directive
|
isByDirective bool // isByDirective indicates whether ttl was set by a $TTL directive
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRR reads the RR contained in the string s. Only the first RR is
|
// NewRR reads the RR contained in the string s. Only the first RR is returned.
|
||||||
// returned. If s contains no records, NewRR will return nil with no
|
// If s contains no records, NewRR will return nil with no error.
|
||||||
// error.
|
|
||||||
//
|
//
|
||||||
// The class defaults to IN and TTL defaults to 3600. The full zone
|
// The class defaults to IN and TTL defaults to 3600. The full zone file syntax
|
||||||
// file syntax like $TTL, $ORIGIN, etc. is supported.
|
// like $TTL, $ORIGIN, etc. is supported. All fields of the returned RR are
|
||||||
//
|
// set, except RR.Header().Rdlength which is set to 0.
|
||||||
// All fields of the returned RR are set, except RR.Header().Rdlength
|
|
||||||
// which is set to 0.
|
|
||||||
func NewRR(s string) (RR, error) {
|
func NewRR(s string) (RR, error) {
|
||||||
if len(s) > 0 && s[len(s)-1] != '\n' { // We need a closing newline
|
if len(s) > 0 && s[len(s)-1] != '\n' { // We need a closing newline
|
||||||
return ReadRR(strings.NewReader(s+"\n"), "")
|
return ReadRR(strings.NewReader(s+"\n"), "")
|
||||||
|
@ -133,69 +120,6 @@ func ReadRR(r io.Reader, file string) (RR, error) {
|
||||||
return rr, zp.Err()
|
return rr, zp.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseZone reads a RFC 1035 style zonefile from r. It returns
|
|
||||||
// *Tokens on the returned channel, each consisting of either a
|
|
||||||
// parsed RR and optional comment or a nil RR and an error. The
|
|
||||||
// channel is closed by ParseZone when the end of r is reached.
|
|
||||||
//
|
|
||||||
// The string file is used in error reporting and to resolve relative
|
|
||||||
// $INCLUDE directives. The string origin is used as the initial
|
|
||||||
// origin, as if the file would start with an $ORIGIN directive.
|
|
||||||
//
|
|
||||||
// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are all
|
|
||||||
// supported.
|
|
||||||
//
|
|
||||||
// Basic usage pattern when reading from a string (z) containing the
|
|
||||||
// zone data:
|
|
||||||
//
|
|
||||||
// for x := range dns.ParseZone(strings.NewReader(z), "", "") {
|
|
||||||
// if x.Error != nil {
|
|
||||||
// // log.Println(x.Error)
|
|
||||||
// } else {
|
|
||||||
// // Do something with x.RR
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Comments specified after an RR (and on the same line!) are
|
|
||||||
// returned too:
|
|
||||||
//
|
|
||||||
// foo. IN A 10.0.0.1 ; this is a comment
|
|
||||||
//
|
|
||||||
// The text "; this is comment" is returned in Token.Comment.
|
|
||||||
// Comments inside the RR are returned concatenated along with the
|
|
||||||
// RR. Comments on a line by themselves are discarded.
|
|
||||||
//
|
|
||||||
// To prevent memory leaks it is important to always fully drain the
|
|
||||||
// returned channel. If an error occurs, it will always be the last
|
|
||||||
// Token sent on the channel.
|
|
||||||
//
|
|
||||||
// Deprecated: New users should prefer the ZoneParser API.
|
|
||||||
func ParseZone(r io.Reader, origin, file string) chan *Token {
|
|
||||||
t := make(chan *Token, 10000)
|
|
||||||
go parseZone(r, origin, file, t)
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseZone(r io.Reader, origin, file string, t chan *Token) {
|
|
||||||
defer close(t)
|
|
||||||
|
|
||||||
zp := NewZoneParser(r, origin, file)
|
|
||||||
zp.SetIncludeAllowed(true)
|
|
||||||
|
|
||||||
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
|
||||||
t <- &Token{RR: rr, Comment: zp.Comment()}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := zp.Err(); err != nil {
|
|
||||||
pe, ok := err.(*ParseError)
|
|
||||||
if !ok {
|
|
||||||
pe = &ParseError{file: file, err: err.Error()}
|
|
||||||
}
|
|
||||||
|
|
||||||
t <- &Token{Error: pe}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZoneParser is a parser for an RFC 1035 style zonefile.
|
// ZoneParser is a parser for an RFC 1035 style zonefile.
|
||||||
//
|
//
|
||||||
// Each parsed RR in the zone is returned sequentially from Next. An
|
// Each parsed RR in the zone is returned sequentially from Next. An
|
||||||
|
@ -203,6 +127,7 @@ func parseZone(r io.Reader, origin, file string, t chan *Token) {
|
||||||
//
|
//
|
||||||
// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are all
|
// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are all
|
||||||
// supported. Although $INCLUDE is disabled by default.
|
// supported. Although $INCLUDE is disabled by default.
|
||||||
|
// Note that $GENERATE's range support up to a maximum of 65535 steps.
|
||||||
//
|
//
|
||||||
// Basic usage pattern when reading from a string (z) containing the
|
// Basic usage pattern when reading from a string (z) containing the
|
||||||
// zone data:
|
// zone data:
|
||||||
|
@ -225,6 +150,9 @@ func parseZone(r io.Reader, origin, file string, t chan *Token) {
|
||||||
// The text "; this is comment" is returned from Comment. Comments inside
|
// The text "; this is comment" is returned from Comment. Comments inside
|
||||||
// the RR are returned concatenated along with the RR. Comments on a line
|
// the RR are returned concatenated along with the RR. Comments on a line
|
||||||
// by themselves are discarded.
|
// by themselves are discarded.
|
||||||
|
//
|
||||||
|
// Callers should not assume all returned data in an Resource Record is
|
||||||
|
// syntactically correct, e.g. illegal base64 in RRSIGs will be returned as-is.
|
||||||
type ZoneParser struct {
|
type ZoneParser struct {
|
||||||
c *zlexer
|
c *zlexer
|
||||||
|
|
||||||
|
@ -245,7 +173,8 @@ type ZoneParser struct {
|
||||||
|
|
||||||
includeDepth uint8
|
includeDepth uint8
|
||||||
|
|
||||||
includeAllowed bool
|
includeAllowed bool
|
||||||
|
generateDisallowed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewZoneParser returns an RFC 1035 style zonefile parser that reads
|
// NewZoneParser returns an RFC 1035 style zonefile parser that reads
|
||||||
|
@ -503,9 +432,8 @@ func (zp *ZoneParser) Next() (RR, bool) {
|
||||||
return zp.setParseError("expecting $TTL value, not this...", l)
|
return zp.setParseError("expecting $TTL value, not this...", l)
|
||||||
}
|
}
|
||||||
|
|
||||||
if e := slurpRemainder(zp.c, zp.file); e != nil {
|
if err := slurpRemainder(zp.c); err != nil {
|
||||||
zp.parseErr = e
|
return zp.setParseError(err.err, err.lex)
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ttl, ok := stringToTTL(l.token)
|
ttl, ok := stringToTTL(l.token)
|
||||||
|
@ -527,9 +455,8 @@ func (zp *ZoneParser) Next() (RR, bool) {
|
||||||
return zp.setParseError("expecting $ORIGIN value, not this...", l)
|
return zp.setParseError("expecting $ORIGIN value, not this...", l)
|
||||||
}
|
}
|
||||||
|
|
||||||
if e := slurpRemainder(zp.c, zp.file); e != nil {
|
if err := slurpRemainder(zp.c); err != nil {
|
||||||
zp.parseErr = e
|
return zp.setParseError(err.err, err.lex)
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
name, ok := toAbsoluteName(l.token, zp.origin)
|
name, ok := toAbsoluteName(l.token, zp.origin)
|
||||||
|
@ -547,6 +474,9 @@ func (zp *ZoneParser) Next() (RR, bool) {
|
||||||
|
|
||||||
st = zExpectDirGenerate
|
st = zExpectDirGenerate
|
||||||
case zExpectDirGenerate:
|
case zExpectDirGenerate:
|
||||||
|
if zp.generateDisallowed {
|
||||||
|
return zp.setParseError("nested $GENERATE directive not allowed", l)
|
||||||
|
}
|
||||||
if l.value != zString {
|
if l.value != zString {
|
||||||
return zp.setParseError("expecting $GENERATE value, not this...", l)
|
return zp.setParseError("expecting $GENERATE value, not this...", l)
|
||||||
}
|
}
|
||||||
|
@ -650,19 +580,69 @@ func (zp *ZoneParser) Next() (RR, bool) {
|
||||||
|
|
||||||
st = zExpectRdata
|
st = zExpectRdata
|
||||||
case zExpectRdata:
|
case zExpectRdata:
|
||||||
r, e := setRR(*h, zp.c, zp.origin, zp.file)
|
var (
|
||||||
if e != nil {
|
rr RR
|
||||||
// If e.lex is nil than we have encounter a unknown RR type
|
parseAsRFC3597 bool
|
||||||
// in that case we substitute our current lex token
|
)
|
||||||
if e.lex.token == "" && e.lex.value == 0 {
|
if newFn, ok := TypeToRR[h.Rrtype]; ok {
|
||||||
e.lex = l // Uh, dirty
|
rr = newFn()
|
||||||
}
|
*rr.Header() = *h
|
||||||
|
|
||||||
zp.parseErr = e
|
// We may be parsing a known RR type using the RFC3597 format.
|
||||||
return nil, false
|
// If so, we handle that here in a generic way.
|
||||||
|
//
|
||||||
|
// This is also true for PrivateRR types which will have the
|
||||||
|
// RFC3597 parsing done for them and the Unpack method called
|
||||||
|
// to populate the RR instead of simply deferring to Parse.
|
||||||
|
if zp.c.Peek().token == "\\#" {
|
||||||
|
parseAsRFC3597 = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rr = &RFC3597{Hdr: *h}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, true
|
_, isPrivate := rr.(*PrivateRR)
|
||||||
|
if !isPrivate && zp.c.Peek().token == "" {
|
||||||
|
// This is a dynamic update rr.
|
||||||
|
|
||||||
|
// TODO(tmthrgd): Previously slurpRemainder was only called
|
||||||
|
// for certain RR types, which may have been important.
|
||||||
|
if err := slurpRemainder(zp.c); err != nil {
|
||||||
|
return zp.setParseError(err.err, err.lex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rr, true
|
||||||
|
} else if l.value == zNewline {
|
||||||
|
return zp.setParseError("unexpected newline", l)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseAsRR := rr
|
||||||
|
if parseAsRFC3597 {
|
||||||
|
parseAsRR = &RFC3597{Hdr: *h}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := parseAsRR.parse(zp.c, zp.origin); err != nil {
|
||||||
|
// err is a concrete *ParseError without the file field set.
|
||||||
|
// The setParseError call below will construct a new
|
||||||
|
// *ParseError with file set to zp.file.
|
||||||
|
|
||||||
|
// err.lex may be nil in which case we substitute our current
|
||||||
|
// lex token.
|
||||||
|
if err.lex == (lex{}) {
|
||||||
|
return zp.setParseError(err.err, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
return zp.setParseError(err.err, err.lex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parseAsRFC3597 {
|
||||||
|
err := parseAsRR.(*RFC3597).fromRFC3597(rr)
|
||||||
|
if err != nil {
|
||||||
|
return zp.setParseError(err.Error(), l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rr, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -682,7 +662,8 @@ type zlexer struct {
|
||||||
comBuf string
|
comBuf string
|
||||||
comment string
|
comment string
|
||||||
|
|
||||||
l lex
|
l lex
|
||||||
|
cachedL *lex
|
||||||
|
|
||||||
brace int
|
brace int
|
||||||
quote bool
|
quote bool
|
||||||
|
@ -748,13 +729,37 @@ func (zl *zlexer) readByte() (byte, bool) {
|
||||||
return c, true
|
return c, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (zl *zlexer) Peek() lex {
|
||||||
|
if zl.nextL {
|
||||||
|
return zl.l
|
||||||
|
}
|
||||||
|
|
||||||
|
l, ok := zl.Next()
|
||||||
|
if !ok {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
if zl.nextL {
|
||||||
|
// Cache l. Next returns zl.cachedL then zl.l.
|
||||||
|
zl.cachedL = &l
|
||||||
|
} else {
|
||||||
|
// In this case l == zl.l, so we just tell Next to return zl.l.
|
||||||
|
zl.nextL = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
func (zl *zlexer) Next() (lex, bool) {
|
func (zl *zlexer) Next() (lex, bool) {
|
||||||
l := &zl.l
|
l := &zl.l
|
||||||
if zl.nextL {
|
switch {
|
||||||
|
case zl.cachedL != nil:
|
||||||
|
l, zl.cachedL = zl.cachedL, nil
|
||||||
|
return *l, true
|
||||||
|
case zl.nextL:
|
||||||
zl.nextL = false
|
zl.nextL = false
|
||||||
return *l, true
|
return *l, true
|
||||||
}
|
case l.err:
|
||||||
if l.err {
|
|
||||||
// Parsing errors should be sticky.
|
// Parsing errors should be sticky.
|
||||||
return lex{value: zEOF}, false
|
return lex{value: zEOF}, false
|
||||||
}
|
}
|
||||||
|
@ -908,6 +913,11 @@ func (zl *zlexer) Next() (lex, bool) {
|
||||||
// was inside braces and we delayed adding it until now.
|
// was inside braces and we delayed adding it until now.
|
||||||
com[comi] = ' ' // convert newline to space
|
com[comi] = ' ' // convert newline to space
|
||||||
comi++
|
comi++
|
||||||
|
if comi >= len(com) {
|
||||||
|
l.token = "comment length insufficient for parsing"
|
||||||
|
l.err = true
|
||||||
|
return *l, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
com[comi] = ';'
|
com[comi] = ';'
|
||||||
|
@ -1216,11 +1226,29 @@ func stringToCm(token string) (e, m uint8, ok bool) {
|
||||||
if cmeters, err = strconv.Atoi(s[1]); err != nil {
|
if cmeters, err = strconv.Atoi(s[1]); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// There's no point in having more than 2 digits in this part, and would rather make the implementation complicated ('123' should be treated as '12').
|
||||||
|
// So we simply reject it.
|
||||||
|
// We also make sure the first character is a digit to reject '+-' signs.
|
||||||
|
if len(s[1]) > 2 || s[1][0] < '0' || s[1][0] > '9' {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(s[1]) == 1 {
|
||||||
|
// 'nn.1' must be treated as 'nn-meters and 10cm, not 1cm.
|
||||||
|
cmeters *= 10
|
||||||
|
}
|
||||||
|
if s[0] == "" {
|
||||||
|
// This will allow omitting the 'meter' part, like .01 (meaning 0.01m = 1cm).
|
||||||
|
break
|
||||||
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case 1:
|
case 1:
|
||||||
if meters, err = strconv.Atoi(s[0]); err != nil {
|
if meters, err = strconv.Atoi(s[0]); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// RFC1876 states the max value is 90000000.00. The latter two conditions enforce it.
|
||||||
|
if s[0][0] < '0' || s[0][0] > '9' || meters > 90000000 || (meters == 90000000 && cmeters != 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
case 0:
|
case 0:
|
||||||
// huh?
|
// huh?
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
|
@ -1233,13 +1261,10 @@ func stringToCm(token string) (e, m uint8, ok bool) {
|
||||||
e = 0
|
e = 0
|
||||||
val = cmeters
|
val = cmeters
|
||||||
}
|
}
|
||||||
for val > 10 {
|
for val >= 10 {
|
||||||
e++
|
e++
|
||||||
val /= 10
|
val /= 10
|
||||||
}
|
}
|
||||||
if e > 9 {
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
m = uint8(val)
|
m = uint8(val)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1281,6 +1306,9 @@ func appendOrigin(name, origin string) string {
|
||||||
|
|
||||||
// LOC record helper function
|
// LOC record helper function
|
||||||
func locCheckNorth(token string, latitude uint32) (uint32, bool) {
|
func locCheckNorth(token string, latitude uint32) (uint32, bool) {
|
||||||
|
if latitude > 90*1000*60*60 {
|
||||||
|
return latitude, false
|
||||||
|
}
|
||||||
switch token {
|
switch token {
|
||||||
case "n", "N":
|
case "n", "N":
|
||||||
return LOC_EQUATOR + latitude, true
|
return LOC_EQUATOR + latitude, true
|
||||||
|
@ -1292,6 +1320,9 @@ func locCheckNorth(token string, latitude uint32) (uint32, bool) {
|
||||||
|
|
||||||
// LOC record helper function
|
// LOC record helper function
|
||||||
func locCheckEast(token string, longitude uint32) (uint32, bool) {
|
func locCheckEast(token string, longitude uint32) (uint32, bool) {
|
||||||
|
if longitude > 180*1000*60*60 {
|
||||||
|
return longitude, false
|
||||||
|
}
|
||||||
switch token {
|
switch token {
|
||||||
case "e", "E":
|
case "e", "E":
|
||||||
return LOC_EQUATOR + longitude, true
|
return LOC_EQUATOR + longitude, true
|
||||||
|
@ -1302,18 +1333,18 @@ func locCheckEast(token string, longitude uint32) (uint32, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Eat" the rest of the "line"
|
// "Eat" the rest of the "line"
|
||||||
func slurpRemainder(c *zlexer, f string) *ParseError {
|
func slurpRemainder(c *zlexer) *ParseError {
|
||||||
l, _ := c.Next()
|
l, _ := c.Next()
|
||||||
switch l.value {
|
switch l.value {
|
||||||
case zBlank:
|
case zBlank:
|
||||||
l, _ = c.Next()
|
l, _ = c.Next()
|
||||||
if l.value != zNewline && l.value != zEOF {
|
if l.value != zNewline && l.value != zEOF {
|
||||||
return &ParseError{f, "garbage after rdata", l}
|
return &ParseError{"", "garbage after rdata", l}
|
||||||
}
|
}
|
||||||
case zNewline:
|
case zNewline:
|
||||||
case zEOF:
|
case zEOF:
|
||||||
default:
|
default:
|
||||||
return &ParseError{f, "garbage after rdata", l}
|
return &ParseError{"", "garbage after rdata", l}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1324,7 +1355,7 @@ func stringToNodeID(l lex) (uint64, *ParseError) {
|
||||||
if len(l.token) < 19 {
|
if len(l.token) < 19 {
|
||||||
return 0, &ParseError{l.token, "bad NID/L64 NodeID/Locator64", l}
|
return 0, &ParseError{l.token, "bad NID/L64 NodeID/Locator64", l}
|
||||||
}
|
}
|
||||||
// There must be three colons at fixes postitions, if not its a parse error
|
// There must be three colons at fixes positions, if not its a parse error
|
||||||
if l.token[4] != ':' && l.token[9] != ':' && l.token[14] != ':' {
|
if l.token[4] != ':' && l.token[9] != ':' && l.token[14] != ':' {
|
||||||
return 0, &ParseError{l.token, "bad NID/L64 NodeID/Locator64", l}
|
return 0, &ParseError{l.token, "bad NID/L64 NodeID/Locator64", l}
|
||||||
}
|
}
|
||||||
|
|
1181
scan_rr.go
1181
scan_rr.go
File diff suppressed because it is too large
Load Diff
168
scan_test.go
168
scan_test.go
|
@ -9,7 +9,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseZoneGenerate(t *testing.T) {
|
func TestZoneParserGenerate(t *testing.T) {
|
||||||
zone := "$ORIGIN example.org.\n$GENERATE 10-12 foo${2,3,d} IN A 127.0.0.$"
|
zone := "$ORIGIN example.org.\n$GENERATE 10-12 foo${2,3,d} IN A 127.0.0.$"
|
||||||
|
|
||||||
wantRRs := []RR{
|
wantRRs := []RR{
|
||||||
|
@ -17,22 +17,21 @@ func TestParseZoneGenerate(t *testing.T) {
|
||||||
&A{Hdr: RR_Header{Name: "foo013.example.org."}, A: net.ParseIP("127.0.0.11")},
|
&A{Hdr: RR_Header{Name: "foo013.example.org."}, A: net.ParseIP("127.0.0.11")},
|
||||||
&A{Hdr: RR_Header{Name: "foo014.example.org."}, A: net.ParseIP("127.0.0.12")},
|
&A{Hdr: RR_Header{Name: "foo014.example.org."}, A: net.ParseIP("127.0.0.12")},
|
||||||
}
|
}
|
||||||
|
|
||||||
wantIdx := 0
|
wantIdx := 0
|
||||||
|
|
||||||
tok := ParseZone(strings.NewReader(zone), "", "")
|
z := NewZoneParser(strings.NewReader(zone), "", "")
|
||||||
for x := range tok {
|
|
||||||
|
for rr, ok := z.Next(); ok; rr, ok = z.Next() {
|
||||||
if wantIdx >= len(wantRRs) {
|
if wantIdx >= len(wantRRs) {
|
||||||
t.Fatalf("expected %d RRs, but got more", len(wantRRs))
|
t.Fatalf("expected %d RRs, but got more", len(wantRRs))
|
||||||
}
|
}
|
||||||
if x.Error != nil {
|
if got, want := rr.Header().Name, wantRRs[wantIdx].Header().Name; got != want {
|
||||||
t.Fatalf("expected no error, but got %s", x.Error)
|
|
||||||
}
|
|
||||||
if got, want := x.RR.Header().Name, wantRRs[wantIdx].Header().Name; got != want {
|
|
||||||
t.Fatalf("expected name %s, but got %s", want, got)
|
t.Fatalf("expected name %s, but got %s", want, got)
|
||||||
}
|
}
|
||||||
a, ok := x.RR.(*A)
|
a, okA := rr.(*A)
|
||||||
if !ok {
|
if !okA {
|
||||||
t.Fatalf("expected *A RR, but got %T", x.RR)
|
t.Fatalf("expected *A RR, but got %T", rr)
|
||||||
}
|
}
|
||||||
if got, want := a.A, wantRRs[wantIdx].(*A).A; !got.Equal(want) {
|
if got, want := a.A, wantRRs[wantIdx].(*A).A; !got.Equal(want) {
|
||||||
t.Fatalf("expected A with IP %v, but got %v", got, want)
|
t.Fatalf("expected A with IP %v, but got %v", got, want)
|
||||||
|
@ -40,12 +39,16 @@ func TestParseZoneGenerate(t *testing.T) {
|
||||||
wantIdx++
|
wantIdx++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := z.Err(); err != nil {
|
||||||
|
t.Fatalf("expected no error, but got %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
if wantIdx != len(wantRRs) {
|
if wantIdx != len(wantRRs) {
|
||||||
t.Errorf("too few records, expected %d, got %d", len(wantRRs), wantIdx)
|
t.Errorf("too few records, expected %d, got %d", len(wantRRs), wantIdx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseZoneInclude(t *testing.T) {
|
func TestZoneParserInclude(t *testing.T) {
|
||||||
|
|
||||||
tmpfile, err := ioutil.TempFile("", "dns")
|
tmpfile, err := ioutil.TempFile("", "dns")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -63,18 +66,19 @@ func TestParseZoneInclude(t *testing.T) {
|
||||||
zone := "$ORIGIN example.org.\n$INCLUDE " + tmpfile.Name() + "\nbar\tIN\tA\t127.0.0.2"
|
zone := "$ORIGIN example.org.\n$INCLUDE " + tmpfile.Name() + "\nbar\tIN\tA\t127.0.0.2"
|
||||||
|
|
||||||
var got int
|
var got int
|
||||||
tok := ParseZone(strings.NewReader(zone), "", "")
|
z := NewZoneParser(strings.NewReader(zone), "", "")
|
||||||
for x := range tok {
|
z.SetIncludeAllowed(true)
|
||||||
if x.Error != nil {
|
for rr, ok := z.Next(); ok; _, ok = z.Next() {
|
||||||
t.Fatalf("expected no error, but got %s", x.Error)
|
switch rr.Header().Name {
|
||||||
}
|
|
||||||
switch x.RR.Header().Name {
|
|
||||||
case "foo.example.org.", "bar.example.org.":
|
case "foo.example.org.", "bar.example.org.":
|
||||||
default:
|
default:
|
||||||
t.Fatalf("expected foo.example.org. or bar.example.org., but got %s", x.RR.Header().Name)
|
t.Fatalf("expected foo.example.org. or bar.example.org., but got %s", rr.Header().Name)
|
||||||
}
|
}
|
||||||
got++
|
got++
|
||||||
}
|
}
|
||||||
|
if err := z.Err(); err != nil {
|
||||||
|
t.Fatalf("expected no error, but got %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
if expected := 2; got != expected {
|
if expected := 2; got != expected {
|
||||||
t.Errorf("failed to parse zone after include, expected %d records, got %d", expected, got)
|
t.Errorf("failed to parse zone after include, expected %d records, got %d", expected, got)
|
||||||
|
@ -82,17 +86,15 @@ func TestParseZoneInclude(t *testing.T) {
|
||||||
|
|
||||||
os.Remove(tmpfile.Name())
|
os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
tok = ParseZone(strings.NewReader(zone), "", "")
|
z = NewZoneParser(strings.NewReader(zone), "", "")
|
||||||
for x := range tok {
|
z.SetIncludeAllowed(true)
|
||||||
if x.Error == nil {
|
z.Next()
|
||||||
t.Fatalf("expected first token to contain an error but it didn't")
|
if err := z.Err(); err == nil ||
|
||||||
}
|
!strings.Contains(err.Error(), "failed to open") ||
|
||||||
if !strings.Contains(x.Error.Error(), "failed to open") ||
|
!strings.Contains(err.Error(), tmpfile.Name()) ||
|
||||||
!strings.Contains(x.Error.Error(), tmpfile.Name()) ||
|
!strings.Contains(err.Error(), "no such file or directory") {
|
||||||
!strings.Contains(x.Error.Error(), "no such file or directory") {
|
t.Fatalf(`expected error to contain: "failed to open", %q and "no such file or directory" but got: %s`,
|
||||||
t.Fatalf(`expected error to contain: "failed to open", %q and "no such file or directory" but got: %s`,
|
tmpfile.Name(), err)
|
||||||
tmpfile.Name(), x.Error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,10 +145,10 @@ func TestZoneParserAddressAAAA(t *testing.T) {
|
||||||
}
|
}
|
||||||
aaaa, ok := got.(*AAAA)
|
aaaa, ok := got.(*AAAA)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("expected *AAAA RR, but got %T", aaaa)
|
t.Fatalf("expected *AAAA RR, but got %T", got)
|
||||||
}
|
}
|
||||||
if g, w := aaaa.AAAA, tc.want.AAAA; !g.Equal(w) {
|
if !aaaa.AAAA.Equal(tc.want.AAAA) {
|
||||||
t.Fatalf("expected AAAA with IP %v, but got %v", g, w)
|
t.Fatalf("expected AAAA with IP %v, but got %v", tc.want.AAAA, aaaa.AAAA)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,6 +194,98 @@ func TestParseZoneReadError(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnexpectedNewline(t *testing.T) {
|
||||||
|
zone := `
|
||||||
|
example.com. 60 PX
|
||||||
|
1000 TXT 1K
|
||||||
|
`
|
||||||
|
zp := NewZoneParser(strings.NewReader(zone), "example.com.", "")
|
||||||
|
for _, ok := zp.Next(); ok; _, ok = zp.Next() {
|
||||||
|
}
|
||||||
|
|
||||||
|
const expect = `dns: unexpected newline: "\n" at line: 2:18`
|
||||||
|
if err := zp.Err(); err == nil || err.Error() != expect {
|
||||||
|
t.Errorf("expected error to contain %q, got %v", expect, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that newlines inside braces still work.
|
||||||
|
zone = `
|
||||||
|
example.com. 60 PX (
|
||||||
|
1000 TXT 1K )
|
||||||
|
`
|
||||||
|
zp = NewZoneParser(strings.NewReader(zone), "example.com.", "")
|
||||||
|
|
||||||
|
var count int
|
||||||
|
for _, ok := zp.Next(); ok; _, ok = zp.Next() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if count != 1 {
|
||||||
|
t.Errorf("expected 1 record, got %d", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := zp.Err(); err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRFC3597InvalidLength(t *testing.T) {
|
||||||
|
// We need to space separate the 00s otherwise it will exceed the maximum token size
|
||||||
|
// of the zone lexer.
|
||||||
|
_, err := NewRR("example. 3600 CLASS1 TYPE1 \\# 65536 " + strings.Repeat("00 ", 65536))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("should not have parsed excessively long RFC3579 record")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseKnownRRAsRFC3597(t *testing.T) {
|
||||||
|
t.Run("with RDATA", func(t *testing.T) {
|
||||||
|
// This was found by oss-fuzz.
|
||||||
|
_, err := NewRR("example. 3600 tYpe44 \\# 03 75 0100")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to parse RFC3579 format: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr, err := NewRR("example. 3600 CLASS1 TYPE1 \\# 4 7f000001")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse RFC3579 format: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rr.Header().Rrtype != TypeA {
|
||||||
|
t.Errorf("expected TypeA (1) Rrtype, but got %v", rr.Header().Rrtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, ok := rr.(*A)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected *A RR, but got %T", rr)
|
||||||
|
}
|
||||||
|
|
||||||
|
localhost := net.IPv4(127, 0, 0, 1)
|
||||||
|
if !a.A.Equal(localhost) {
|
||||||
|
t.Errorf("expected A with IP %v, but got %v", localhost, a.A)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("without RDATA", func(t *testing.T) {
|
||||||
|
rr, err := NewRR("example. 3600 CLASS1 TYPE1 \\# 0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse RFC3579 format: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rr.Header().Rrtype != TypeA {
|
||||||
|
t.Errorf("expected TypeA (1) Rrtype, but got %v", rr.Header().Rrtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, ok := rr.(*A)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected *A RR, but got %T", rr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a.A) != 0 {
|
||||||
|
t.Errorf("expected A with empty IP, but got %v", a.A)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkNewRR(b *testing.B) {
|
func BenchmarkNewRR(b *testing.B) {
|
||||||
const name1 = "12345678901234567890123456789012345.12345678.123."
|
const name1 = "12345678901234567890123456789012345.12345678.123."
|
||||||
const s = name1 + " 3600 IN MX 10 " + name1
|
const s = name1 + " 3600 IN MX 10 " + name1
|
||||||
|
@ -239,16 +333,6 @@ foo. IN NSEC miek.nl. TXT RRSIG NSEC; this is comment 7
|
||||||
foo. IN TXT "THIS IS TEXT MAN"; this is comment 8
|
foo. IN TXT "THIS IS TEXT MAN"; this is comment 8
|
||||||
`
|
`
|
||||||
|
|
||||||
func BenchmarkParseZone(b *testing.B) {
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
for tok := range ParseZone(strings.NewReader(benchZone), "example.org.", "") {
|
|
||||||
if tok.Error != nil {
|
|
||||||
b.Fatal(tok.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkZoneParser(b *testing.B) {
|
func BenchmarkZoneParser(b *testing.B) {
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
zp := NewZoneParser(strings.NewReader(benchZone), "example.org.", "")
|
zp := NewZoneParser(strings.NewReader(benchZone), "example.org.", "")
|
||||||
|
|
37
serve_mux.go
37
serve_mux.go
|
@ -1,7 +1,6 @@
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,33 +35,9 @@ func (mux *ServeMux) match(q string, t uint16) Handler {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
q = CanonicalName(q)
|
||||||
|
|
||||||
var handler Handler
|
var handler Handler
|
||||||
|
|
||||||
// TODO(tmthrgd): Once https://go-review.googlesource.com/c/go/+/137575
|
|
||||||
// lands in a go release, replace the following with strings.ToLower.
|
|
||||||
var sb strings.Builder
|
|
||||||
for i := 0; i < len(q); i++ {
|
|
||||||
c := q[i]
|
|
||||||
if !(c >= 'A' && c <= 'Z') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.Grow(len(q))
|
|
||||||
sb.WriteString(q[:i])
|
|
||||||
|
|
||||||
for ; i < len(q); i++ {
|
|
||||||
c := q[i]
|
|
||||||
if c >= 'A' && c <= 'Z' {
|
|
||||||
c += 'a' - 'A'
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteByte(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
q = sb.String()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
for off, end := 0, false; !end; off, end = NextLabel(q, off) {
|
for off, end := 0, false; !end; off, end = NextLabel(q, off) {
|
||||||
if h, ok := mux.z[q[off:]]; ok {
|
if h, ok := mux.z[q[off:]]; ok {
|
||||||
if t != TypeDS {
|
if t != TypeDS {
|
||||||
|
@ -90,7 +65,7 @@ func (mux *ServeMux) Handle(pattern string, handler Handler) {
|
||||||
if mux.z == nil {
|
if mux.z == nil {
|
||||||
mux.z = make(map[string]Handler)
|
mux.z = make(map[string]Handler)
|
||||||
}
|
}
|
||||||
mux.z[Fqdn(pattern)] = handler
|
mux.z[CanonicalName(pattern)] = handler
|
||||||
mux.m.Unlock()
|
mux.m.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +80,7 @@ func (mux *ServeMux) HandleRemove(pattern string) {
|
||||||
panic("dns: invalid pattern " + pattern)
|
panic("dns: invalid pattern " + pattern)
|
||||||
}
|
}
|
||||||
mux.m.Lock()
|
mux.m.Lock()
|
||||||
delete(mux.z, Fqdn(pattern))
|
delete(mux.z, CanonicalName(pattern))
|
||||||
mux.m.Unlock()
|
mux.m.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +91,7 @@ func (mux *ServeMux) HandleRemove(pattern string) {
|
||||||
// are redirected to the parent zone (if that is also registered),
|
// are redirected to the parent zone (if that is also registered),
|
||||||
// otherwise the child gets the query.
|
// otherwise the child gets the query.
|
||||||
//
|
//
|
||||||
// If no handler is found, or there is no question, a standard SERVFAIL
|
// If no handler is found, or there is no question, a standard REFUSED
|
||||||
// message is returned
|
// message is returned
|
||||||
func (mux *ServeMux) ServeDNS(w ResponseWriter, req *Msg) {
|
func (mux *ServeMux) ServeDNS(w ResponseWriter, req *Msg) {
|
||||||
var h Handler
|
var h Handler
|
||||||
|
@ -127,7 +102,7 @@ func (mux *ServeMux) ServeDNS(w ResponseWriter, req *Msg) {
|
||||||
if h != nil {
|
if h != nil {
|
||||||
h.ServeDNS(w, req)
|
h.ServeDNS(w, req)
|
||||||
} else {
|
} else {
|
||||||
HandleFailed(w, req)
|
handleRefused(w, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
176
server.go
176
server.go
|
@ -18,7 +18,7 @@ import (
|
||||||
const maxTCPQueries = 128
|
const maxTCPQueries = 128
|
||||||
|
|
||||||
// aLongTimeAgo is a non-zero time, far in the past, used for
|
// aLongTimeAgo is a non-zero time, far in the past, used for
|
||||||
// immediate cancelation of network operations.
|
// immediate cancellation of network operations.
|
||||||
var aLongTimeAgo = time.Unix(1, 0)
|
var aLongTimeAgo = time.Unix(1, 0)
|
||||||
|
|
||||||
// Handler is implemented by any value that implements ServeDNS.
|
// Handler is implemented by any value that implements ServeDNS.
|
||||||
|
@ -71,14 +71,23 @@ type response struct {
|
||||||
tsigTimersOnly bool
|
tsigTimersOnly bool
|
||||||
tsigStatus error
|
tsigStatus error
|
||||||
tsigRequestMAC string
|
tsigRequestMAC string
|
||||||
tsigSecret map[string]string // the tsig secrets
|
tsigProvider TsigProvider
|
||||||
udp *net.UDPConn // i/o connection if UDP was used
|
udp net.PacketConn // i/o connection if UDP was used
|
||||||
tcp net.Conn // i/o connection if TCP was used
|
tcp net.Conn // i/o connection if TCP was used
|
||||||
udpSession *SessionUDP // oob data to get egress interface right
|
udpSession *SessionUDP // oob data to get egress interface right
|
||||||
writer Writer // writer to output the raw DNS bits
|
pcSession net.Addr // address to use when writing to a generic net.PacketConn
|
||||||
|
writer Writer // writer to output the raw DNS bits
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRefused returns a HandlerFunc that returns REFUSED for every request it gets.
|
||||||
|
func handleRefused(w ResponseWriter, r *Msg) {
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetRcode(r, RcodeRefused)
|
||||||
|
w.WriteMsg(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleFailed returns a HandlerFunc that returns SERVFAIL for every request it gets.
|
// HandleFailed returns a HandlerFunc that returns SERVFAIL for every request it gets.
|
||||||
|
// Deprecated: This function is going away.
|
||||||
func HandleFailed(w ResponseWriter, r *Msg) {
|
func HandleFailed(w ResponseWriter, r *Msg) {
|
||||||
m := new(Msg)
|
m := new(Msg)
|
||||||
m.SetRcode(r, RcodeServerFailure)
|
m.SetRcode(r, RcodeServerFailure)
|
||||||
|
@ -139,12 +148,24 @@ type Reader interface {
|
||||||
ReadUDP(conn *net.UDPConn, timeout time.Duration) ([]byte, *SessionUDP, error)
|
ReadUDP(conn *net.UDPConn, timeout time.Duration) ([]byte, *SessionUDP, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultReader is an adapter for the Server struct that implements the Reader interface
|
// PacketConnReader is an optional interface that Readers can implement to support using generic net.PacketConns.
|
||||||
// using the readTCP and readUDP func of the embedded Server.
|
type PacketConnReader interface {
|
||||||
|
Reader
|
||||||
|
|
||||||
|
// ReadPacketConn reads a raw message from a generic net.PacketConn UDP connection. Implementations may
|
||||||
|
// alter connection properties, for example the read-deadline.
|
||||||
|
ReadPacketConn(conn net.PacketConn, timeout time.Duration) ([]byte, net.Addr, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultReader is an adapter for the Server struct that implements the Reader and
|
||||||
|
// PacketConnReader interfaces using the readTCP, readUDP and readPacketConn funcs
|
||||||
|
// of the embedded Server.
|
||||||
type defaultReader struct {
|
type defaultReader struct {
|
||||||
*Server
|
*Server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ PacketConnReader = defaultReader{}
|
||||||
|
|
||||||
func (dr defaultReader) ReadTCP(conn net.Conn, timeout time.Duration) ([]byte, error) {
|
func (dr defaultReader) ReadTCP(conn net.Conn, timeout time.Duration) ([]byte, error) {
|
||||||
return dr.readTCP(conn, timeout)
|
return dr.readTCP(conn, timeout)
|
||||||
}
|
}
|
||||||
|
@ -153,8 +174,14 @@ func (dr defaultReader) ReadUDP(conn *net.UDPConn, timeout time.Duration) ([]byt
|
||||||
return dr.readUDP(conn, timeout)
|
return dr.readUDP(conn, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dr defaultReader) ReadPacketConn(conn net.PacketConn, timeout time.Duration) ([]byte, net.Addr, error) {
|
||||||
|
return dr.readPacketConn(conn, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
// DecorateReader is a decorator hook for extending or supplanting the functionality of a Reader.
|
// DecorateReader is a decorator hook for extending or supplanting the functionality of a Reader.
|
||||||
// Implementations should never return a nil Reader.
|
// Implementations should never return a nil Reader.
|
||||||
|
// Readers should also implement the optional PacketConnReader interface.
|
||||||
|
// PacketConnReader is required to use a generic net.PacketConn.
|
||||||
type DecorateReader func(Reader) Reader
|
type DecorateReader func(Reader) Reader
|
||||||
|
|
||||||
// DecorateWriter is a decorator hook for extending or supplanting the functionality of a Writer.
|
// DecorateWriter is a decorator hook for extending or supplanting the functionality of a Writer.
|
||||||
|
@ -184,6 +211,8 @@ type Server struct {
|
||||||
WriteTimeout time.Duration
|
WriteTimeout time.Duration
|
||||||
// TCP idle timeout for multiple queries, if nil, defaults to 8 * time.Second (RFC 5966).
|
// TCP idle timeout for multiple queries, if nil, defaults to 8 * time.Second (RFC 5966).
|
||||||
IdleTimeout func() time.Duration
|
IdleTimeout func() time.Duration
|
||||||
|
// An implementation of the TsigProvider interface. If defined it replaces TsigSecret and is used for all TSIG operations.
|
||||||
|
TsigProvider TsigProvider
|
||||||
// Secret(s) for Tsig map[<zonename>]<base64 secret>. The zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2).
|
// Secret(s) for Tsig map[<zonename>]<base64 secret>. The zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2).
|
||||||
TsigSecret map[string]string
|
TsigSecret map[string]string
|
||||||
// If NotifyStartedFunc is set it is called once the server has started listening.
|
// If NotifyStartedFunc is set it is called once the server has started listening.
|
||||||
|
@ -211,6 +240,16 @@ type Server struct {
|
||||||
udpPool sync.Pool
|
udpPool sync.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *Server) tsigProvider() TsigProvider {
|
||||||
|
if srv.TsigProvider != nil {
|
||||||
|
return srv.TsigProvider
|
||||||
|
}
|
||||||
|
if srv.TsigSecret != nil {
|
||||||
|
return tsigSecretProvider(srv.TsigSecret)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (srv *Server) isStarted() bool {
|
func (srv *Server) isStarted() bool {
|
||||||
srv.lock.RLock()
|
srv.lock.RLock()
|
||||||
started := srv.started
|
started := srv.started
|
||||||
|
@ -294,6 +333,7 @@ func (srv *Server) ListenAndServe() error {
|
||||||
}
|
}
|
||||||
u := l.(*net.UDPConn)
|
u := l.(*net.UDPConn)
|
||||||
if e := setUDPSocketOptions(u); e != nil {
|
if e := setUDPSocketOptions(u); e != nil {
|
||||||
|
u.Close()
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
srv.PacketConn = l
|
srv.PacketConn = l
|
||||||
|
@ -317,24 +357,22 @@ func (srv *Server) ActivateAndServe() error {
|
||||||
|
|
||||||
srv.init()
|
srv.init()
|
||||||
|
|
||||||
pConn := srv.PacketConn
|
if srv.PacketConn != nil {
|
||||||
l := srv.Listener
|
|
||||||
if pConn != nil {
|
|
||||||
// Check PacketConn interface's type is valid and value
|
// Check PacketConn interface's type is valid and value
|
||||||
// is not nil
|
// is not nil
|
||||||
if t, ok := pConn.(*net.UDPConn); ok && t != nil {
|
if t, ok := srv.PacketConn.(*net.UDPConn); ok && t != nil {
|
||||||
if e := setUDPSocketOptions(t); e != nil {
|
if e := setUDPSocketOptions(t); e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
srv.started = true
|
|
||||||
unlock()
|
|
||||||
return srv.serveUDP(t)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if l != nil {
|
|
||||||
srv.started = true
|
srv.started = true
|
||||||
unlock()
|
unlock()
|
||||||
return srv.serveTCP(l)
|
return srv.serveUDP(srv.PacketConn)
|
||||||
|
}
|
||||||
|
if srv.Listener != nil {
|
||||||
|
srv.started = true
|
||||||
|
unlock()
|
||||||
|
return srv.serveTCP(srv.Listener)
|
||||||
}
|
}
|
||||||
return &Error{err: "bad listeners"}
|
return &Error{err: "bad listeners"}
|
||||||
}
|
}
|
||||||
|
@ -438,18 +476,24 @@ func (srv *Server) serveTCP(l net.Listener) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveUDP starts a UDP listener for the server.
|
// serveUDP starts a UDP listener for the server.
|
||||||
func (srv *Server) serveUDP(l *net.UDPConn) error {
|
func (srv *Server) serveUDP(l net.PacketConn) error {
|
||||||
defer l.Close()
|
defer l.Close()
|
||||||
|
|
||||||
if srv.NotifyStartedFunc != nil {
|
|
||||||
srv.NotifyStartedFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := Reader(defaultReader{srv})
|
reader := Reader(defaultReader{srv})
|
||||||
if srv.DecorateReader != nil {
|
if srv.DecorateReader != nil {
|
||||||
reader = srv.DecorateReader(reader)
|
reader = srv.DecorateReader(reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lUDP, isUDP := l.(*net.UDPConn)
|
||||||
|
readerPC, canPacketConn := reader.(PacketConnReader)
|
||||||
|
if !isUDP && !canPacketConn {
|
||||||
|
return &Error{err: "PacketConnReader was not implemented on Reader returned from DecorateReader but is required for net.PacketConn"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv.NotifyStartedFunc != nil {
|
||||||
|
srv.NotifyStartedFunc()
|
||||||
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
defer func() {
|
defer func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
@ -459,7 +503,17 @@ func (srv *Server) serveUDP(l *net.UDPConn) error {
|
||||||
rtimeout := srv.getReadTimeout()
|
rtimeout := srv.getReadTimeout()
|
||||||
// deadline is not used here
|
// deadline is not used here
|
||||||
for srv.isStarted() {
|
for srv.isStarted() {
|
||||||
m, s, err := reader.ReadUDP(l, rtimeout)
|
var (
|
||||||
|
m []byte
|
||||||
|
sPC net.Addr
|
||||||
|
sUDP *SessionUDP
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if isUDP {
|
||||||
|
m, sUDP, err = reader.ReadUDP(lUDP, rtimeout)
|
||||||
|
} else {
|
||||||
|
m, sPC, err = readerPC.ReadPacketConn(l, rtimeout)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !srv.isStarted() {
|
if !srv.isStarted() {
|
||||||
return nil
|
return nil
|
||||||
|
@ -476,7 +530,7 @@ func (srv *Server) serveUDP(l *net.UDPConn) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go srv.serveUDPPacket(&wg, m, l, s)
|
go srv.serveUDPPacket(&wg, m, l, sUDP, sPC)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -484,7 +538,7 @@ func (srv *Server) serveUDP(l *net.UDPConn) error {
|
||||||
|
|
||||||
// Serve a new TCP connection.
|
// Serve a new TCP connection.
|
||||||
func (srv *Server) serveTCPConn(wg *sync.WaitGroup, rw net.Conn) {
|
func (srv *Server) serveTCPConn(wg *sync.WaitGroup, rw net.Conn) {
|
||||||
w := &response{tsigSecret: srv.TsigSecret, tcp: rw}
|
w := &response{tsigProvider: srv.tsigProvider(), tcp: rw}
|
||||||
if srv.DecorateWriter != nil {
|
if srv.DecorateWriter != nil {
|
||||||
w.writer = srv.DecorateWriter(w)
|
w.writer = srv.DecorateWriter(w)
|
||||||
} else {
|
} else {
|
||||||
|
@ -538,8 +592,8 @@ func (srv *Server) serveTCPConn(wg *sync.WaitGroup, rw net.Conn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve a new UDP request.
|
// Serve a new UDP request.
|
||||||
func (srv *Server) serveUDPPacket(wg *sync.WaitGroup, m []byte, u *net.UDPConn, s *SessionUDP) {
|
func (srv *Server) serveUDPPacket(wg *sync.WaitGroup, m []byte, u net.PacketConn, udpSession *SessionUDP, pcSession net.Addr) {
|
||||||
w := &response{tsigSecret: srv.TsigSecret, udp: u, udpSession: s}
|
w := &response{tsigProvider: srv.tsigProvider(), udp: u, udpSession: udpSession, pcSession: pcSession}
|
||||||
if srv.DecorateWriter != nil {
|
if srv.DecorateWriter != nil {
|
||||||
w.writer = srv.DecorateWriter(w)
|
w.writer = srv.DecorateWriter(w)
|
||||||
} else {
|
} else {
|
||||||
|
@ -560,39 +614,41 @@ func (srv *Server) serveDNS(m []byte, w *response) {
|
||||||
req := new(Msg)
|
req := new(Msg)
|
||||||
req.setHdr(dh)
|
req.setHdr(dh)
|
||||||
|
|
||||||
switch srv.MsgAcceptFunc(dh) {
|
switch action := srv.MsgAcceptFunc(dh); action {
|
||||||
case MsgAccept:
|
case MsgAccept:
|
||||||
if req.unpack(dh, m, off) == nil {
|
if req.unpack(dh, m, off) == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
fallthrough
|
fallthrough
|
||||||
case MsgReject:
|
case MsgReject, MsgRejectNotImplemented:
|
||||||
|
opcode := req.Opcode
|
||||||
req.SetRcodeFormatError(req)
|
req.SetRcodeFormatError(req)
|
||||||
|
req.Zero = false
|
||||||
|
if action == MsgRejectNotImplemented {
|
||||||
|
req.Opcode = opcode
|
||||||
|
req.Rcode = RcodeNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
// Are we allowed to delete any OPT records here?
|
// Are we allowed to delete any OPT records here?
|
||||||
req.Ns, req.Answer, req.Extra = nil, nil, nil
|
req.Ns, req.Answer, req.Extra = nil, nil, nil
|
||||||
|
|
||||||
w.WriteMsg(req)
|
w.WriteMsg(req)
|
||||||
|
fallthrough
|
||||||
|
case MsgIgnore:
|
||||||
if w.udp != nil && cap(m) == srv.UDPSize {
|
if w.udp != nil && cap(m) == srv.UDPSize {
|
||||||
srv.udpPool.Put(m[:srv.UDPSize])
|
srv.udpPool.Put(m[:srv.UDPSize])
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
case MsgIgnore:
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.tsigStatus = nil
|
w.tsigStatus = nil
|
||||||
if w.tsigSecret != nil {
|
if w.tsigProvider != nil {
|
||||||
if t := req.IsTsig(); t != nil {
|
if t := req.IsTsig(); t != nil {
|
||||||
if secret, ok := w.tsigSecret[t.Hdr.Name]; ok {
|
w.tsigStatus = TsigVerifyWithProvider(m, w.tsigProvider, "", false)
|
||||||
w.tsigStatus = TsigVerify(m, secret, "", false)
|
|
||||||
} else {
|
|
||||||
w.tsigStatus = ErrSecret
|
|
||||||
}
|
|
||||||
w.tsigTimersOnly = false
|
w.tsigTimersOnly = false
|
||||||
w.tsigRequestMAC = req.Extra[len(req.Extra)-1].(*TSIG).MAC
|
w.tsigRequestMAC = t.MAC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -645,6 +701,24 @@ func (srv *Server) readUDP(conn *net.UDPConn, timeout time.Duration) ([]byte, *S
|
||||||
return m, s, nil
|
return m, s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *Server) readPacketConn(conn net.PacketConn, timeout time.Duration) ([]byte, net.Addr, error) {
|
||||||
|
srv.lock.RLock()
|
||||||
|
if srv.started {
|
||||||
|
// See the comment in readTCP above.
|
||||||
|
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
}
|
||||||
|
srv.lock.RUnlock()
|
||||||
|
|
||||||
|
m := srv.udpPool.Get().([]byte)
|
||||||
|
n, addr, err := conn.ReadFrom(m)
|
||||||
|
if err != nil {
|
||||||
|
srv.udpPool.Put(m)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
m = m[:n]
|
||||||
|
return m, addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
// WriteMsg implements the ResponseWriter.WriteMsg method.
|
// WriteMsg implements the ResponseWriter.WriteMsg method.
|
||||||
func (w *response) WriteMsg(m *Msg) (err error) {
|
func (w *response) WriteMsg(m *Msg) (err error) {
|
||||||
if w.closed {
|
if w.closed {
|
||||||
|
@ -652,9 +726,9 @@ func (w *response) WriteMsg(m *Msg) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []byte
|
var data []byte
|
||||||
if w.tsigSecret != nil { // if no secrets, dont check for the tsig (which is a longer check)
|
if w.tsigProvider != nil { // if no provider, dont check for the tsig (which is a longer check)
|
||||||
if t := m.IsTsig(); t != nil {
|
if t := m.IsTsig(); t != nil {
|
||||||
data, w.tsigRequestMAC, err = TsigGenerate(m, w.tsigSecret[t.Hdr.Name], w.tsigRequestMAC, w.tsigTimersOnly)
|
data, w.tsigRequestMAC, err = TsigGenerateWithProvider(m, w.tsigProvider, w.tsigRequestMAC, w.tsigTimersOnly)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -678,17 +752,19 @@ func (w *response) Write(m []byte) (int, error) {
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case w.udp != nil:
|
case w.udp != nil:
|
||||||
return WriteToSessionUDP(w.udp, m, w.udpSession)
|
if u, ok := w.udp.(*net.UDPConn); ok {
|
||||||
|
return WriteToSessionUDP(u, m, w.udpSession)
|
||||||
|
}
|
||||||
|
return w.udp.WriteTo(m, w.pcSession)
|
||||||
case w.tcp != nil:
|
case w.tcp != nil:
|
||||||
if len(m) > MaxMsgSize {
|
if len(m) > MaxMsgSize {
|
||||||
return 0, &Error{err: "message too large"}
|
return 0, &Error{err: "message too large"}
|
||||||
}
|
}
|
||||||
|
|
||||||
l := make([]byte, 2)
|
msg := make([]byte, 2+len(m))
|
||||||
binary.BigEndian.PutUint16(l, uint16(len(m)))
|
binary.BigEndian.PutUint16(msg, uint16(len(m)))
|
||||||
|
copy(msg[2:], m)
|
||||||
n, err := (&net.Buffers{l, m}).WriteTo(w.tcp)
|
return w.tcp.Write(msg)
|
||||||
return int(n), err
|
|
||||||
default:
|
default:
|
||||||
panic("dns: internal error: udp and tcp both nil")
|
panic("dns: internal error: udp and tcp both nil")
|
||||||
}
|
}
|
||||||
|
@ -711,10 +787,12 @@ func (w *response) RemoteAddr() net.Addr {
|
||||||
switch {
|
switch {
|
||||||
case w.udpSession != nil:
|
case w.udpSession != nil:
|
||||||
return w.udpSession.RemoteAddr()
|
return w.udpSession.RemoteAddr()
|
||||||
|
case w.pcSession != nil:
|
||||||
|
return w.pcSession
|
||||||
case w.tcp != nil:
|
case w.tcp != nil:
|
||||||
return w.tcp.RemoteAddr()
|
return w.tcp.RemoteAddr()
|
||||||
default:
|
default:
|
||||||
panic("dns: internal error: udpSession and tcp both nil")
|
panic("dns: internal error: udpSession, pcSession and tcp are all nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
381
server_test.go
381
server_test.go
|
@ -35,6 +35,19 @@ func HelloServerBadID(w ResponseWriter, req *Msg) {
|
||||||
w.WriteMsg(m)
|
w.WriteMsg(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HelloServerBadThenGoodID(w ResponseWriter, req *Msg) {
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetReply(req)
|
||||||
|
m.Id++
|
||||||
|
|
||||||
|
m.Extra = make([]RR, 1)
|
||||||
|
m.Extra[0] = &TXT{Hdr: RR_Header{Name: m.Question[0].Name, Rrtype: TypeTXT, Class: ClassINET, Ttl: 0}, Txt: []string{"Hello world"}}
|
||||||
|
w.WriteMsg(m)
|
||||||
|
|
||||||
|
m.Id--
|
||||||
|
w.WriteMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
func HelloServerEchoAddrPort(w ResponseWriter, req *Msg) {
|
func HelloServerEchoAddrPort(w ResponseWriter, req *Msg) {
|
||||||
m := new(Msg)
|
m := new(Msg)
|
||||||
m.SetReply(req)
|
m.SetReply(req)
|
||||||
|
@ -54,100 +67,178 @@ func AnotherHelloServer(w ResponseWriter, req *Msg) {
|
||||||
w.WriteMsg(m)
|
w.WriteMsg(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunLocalUDPServer(laddr string) (*Server, string, error) {
|
func RunLocalServer(pc net.PacketConn, l net.Listener, opts ...func(*Server)) (*Server, string, chan error, error) {
|
||||||
server, l, _, err := RunLocalUDPServerWithFinChan(laddr)
|
server := &Server{
|
||||||
|
PacketConn: pc,
|
||||||
|
Listener: l,
|
||||||
|
|
||||||
return server, l, err
|
ReadTimeout: time.Hour,
|
||||||
}
|
WriteTimeout: time.Hour,
|
||||||
|
|
||||||
func RunLocalUDPServerWithFinChan(laddr string, opts ...func(*Server)) (*Server, string, chan error, error) {
|
|
||||||
pc, err := net.ListenPacket("udp", laddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", nil, err
|
|
||||||
}
|
}
|
||||||
server := &Server{PacketConn: pc, ReadTimeout: time.Hour, WriteTimeout: time.Hour}
|
|
||||||
|
|
||||||
waitLock := sync.Mutex{}
|
waitLock := sync.Mutex{}
|
||||||
waitLock.Lock()
|
waitLock.Lock()
|
||||||
server.NotifyStartedFunc = waitLock.Unlock
|
server.NotifyStartedFunc = waitLock.Unlock
|
||||||
|
|
||||||
// fin must be buffered so the goroutine below won't block
|
|
||||||
// forever if fin is never read from. This always happens
|
|
||||||
// in RunLocalUDPServer and can happen in TestShutdownUDP.
|
|
||||||
fin := make(chan error, 1)
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(server)
|
opt(server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
addr string
|
||||||
|
closer io.Closer
|
||||||
|
)
|
||||||
|
if l != nil {
|
||||||
|
addr = l.Addr().String()
|
||||||
|
closer = l
|
||||||
|
} else {
|
||||||
|
addr = pc.LocalAddr().String()
|
||||||
|
closer = pc
|
||||||
|
}
|
||||||
|
|
||||||
|
// fin must be buffered so the goroutine below won't block
|
||||||
|
// forever if fin is never read from. This always happens
|
||||||
|
// if the channel is discarded and can happen in TestShutdownUDP.
|
||||||
|
fin := make(chan error, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
fin <- server.ActivateAndServe()
|
fin <- server.ActivateAndServe()
|
||||||
pc.Close()
|
closer.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
waitLock.Lock()
|
waitLock.Lock()
|
||||||
return server, pc.LocalAddr().String(), fin, nil
|
return server, addr, fin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunLocalTCPServer(laddr string) (*Server, string, error) {
|
func RunLocalUDPServer(laddr string, opts ...func(*Server)) (*Server, string, chan error, error) {
|
||||||
server, l, _, err := RunLocalTCPServerWithFinChan(laddr)
|
pc, err := net.ListenPacket("udp", laddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return server, l, err
|
return RunLocalServer(pc, nil, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunLocalTCPServerWithFinChan(laddr string) (*Server, string, chan error, error) {
|
func RunLocalPacketConnServer(laddr string, opts ...func(*Server)) (*Server, string, chan error, error) {
|
||||||
|
return RunLocalUDPServer(laddr, append(opts, func(srv *Server) {
|
||||||
|
// Make srv.PacketConn opaque to trigger the generic code paths.
|
||||||
|
srv.PacketConn = struct{ net.PacketConn }{srv.PacketConn}
|
||||||
|
})...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunLocalTCPServer(laddr string, opts ...func(*Server)) (*Server, string, chan error, error) {
|
||||||
l, err := net.Listen("tcp", laddr)
|
l, err := net.Listen("tcp", laddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", nil, err
|
return nil, "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
server := &Server{Listener: l, ReadTimeout: time.Hour, WriteTimeout: time.Hour}
|
return RunLocalServer(nil, l, opts...)
|
||||||
|
|
||||||
waitLock := sync.Mutex{}
|
|
||||||
waitLock.Lock()
|
|
||||||
server.NotifyStartedFunc = waitLock.Unlock
|
|
||||||
|
|
||||||
// See the comment in RunLocalUDPServerWithFinChan as to
|
|
||||||
// why fin must be buffered.
|
|
||||||
fin := make(chan error, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
fin <- server.ActivateAndServe()
|
|
||||||
l.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
waitLock.Lock()
|
|
||||||
return server, l.Addr().String(), fin, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunLocalTLSServer(laddr string, config *tls.Config) (*Server, string, error) {
|
func RunLocalTLSServer(laddr string, config *tls.Config) (*Server, string, chan error, error) {
|
||||||
l, err := tls.Listen("tcp", laddr, config)
|
return RunLocalTCPServer(laddr, func(srv *Server) {
|
||||||
|
srv.Listener = tls.NewListener(srv.Listener, config)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunLocalUnixServer(laddr string, opts ...func(*Server)) (*Server, string, chan error, error) {
|
||||||
|
l, err := net.Listen("unix", laddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return RunLocalServer(nil, l, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunLocalUnixGramServer(laddr string, opts ...func(*Server)) (*Server, string, chan error, error) {
|
||||||
|
pc, err := net.ListenPacket("unixgram", laddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return RunLocalServer(pc, nil, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunLocalUnixSeqPacketServer(laddr string) (chan interface{}, string, error) {
|
||||||
|
pc, err := net.Listen("unixpacket", laddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
server := &Server{Listener: l, ReadTimeout: time.Hour, WriteTimeout: time.Hour}
|
shutdownChan := make(chan interface{})
|
||||||
|
|
||||||
waitLock := sync.Mutex{}
|
|
||||||
waitLock.Lock()
|
|
||||||
server.NotifyStartedFunc = waitLock.Unlock
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
server.ActivateAndServe()
|
pc.Accept()
|
||||||
l.Close()
|
<-shutdownChan
|
||||||
}()
|
}()
|
||||||
|
|
||||||
waitLock.Lock()
|
return shutdownChan, pc.Addr().String(), nil
|
||||||
return server, l.Addr().String(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServing(t *testing.T) {
|
func TestServing(t *testing.T) {
|
||||||
HandleFunc("miek.nl.", HelloServer)
|
for _, tc := range []struct {
|
||||||
HandleFunc("example.com.", AnotherHelloServer)
|
name string
|
||||||
defer HandleRemove("miek.nl.")
|
network string
|
||||||
defer HandleRemove("example.com.")
|
runServer func(laddr string, opts ...func(*Server)) (*Server, string, chan error, error)
|
||||||
|
}{
|
||||||
|
{"udp", "udp", RunLocalUDPServer},
|
||||||
|
{"tcp", "tcp", RunLocalTCPServer},
|
||||||
|
{"PacketConn", "udp", RunLocalPacketConnServer},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
HandleFunc("miek.nl.", HelloServer)
|
||||||
|
HandleFunc("example.com.", AnotherHelloServer)
|
||||||
|
defer HandleRemove("miek.nl.")
|
||||||
|
defer HandleRemove("example.com.")
|
||||||
|
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := tc.runServer(":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
c := &Client{
|
||||||
|
Net: tc.network,
|
||||||
|
}
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("miek.nl.", TypeTXT)
|
||||||
|
r, _, err := c.Exchange(m, addrstr)
|
||||||
|
if err != nil || len(r.Extra) == 0 {
|
||||||
|
t.Fatal("failed to exchange miek.nl", err)
|
||||||
|
}
|
||||||
|
txt := r.Extra[0].(*TXT).Txt[0]
|
||||||
|
if txt != "Hello world" {
|
||||||
|
t.Error("unexpected result for miek.nl", txt, "!= Hello world")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetQuestion("example.com.", TypeTXT)
|
||||||
|
r, _, err = c.Exchange(m, addrstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to exchange example.com", err)
|
||||||
|
}
|
||||||
|
txt = r.Extra[0].(*TXT).Txt[0]
|
||||||
|
if txt != "Hello example" {
|
||||||
|
t.Error("unexpected result for example.com", txt, "!= Hello example")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Mixes cased as noticed by Ask.
|
||||||
|
m.SetQuestion("eXaMplE.cOm.", TypeTXT)
|
||||||
|
r, _, err = c.Exchange(m, addrstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to exchange eXaMplE.cOm", err)
|
||||||
|
}
|
||||||
|
txt = r.Extra[0].(*TXT).Txt[0]
|
||||||
|
if txt != "Hello example" {
|
||||||
|
t.Error("unexpected result for example.com", txt, "!= Hello example")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the server responds to a query with Z flag on, ignoring the flag, and does not echoes it back
|
||||||
|
func TestServeIgnoresZFlag(t *testing.T) {
|
||||||
|
HandleFunc("example.com.", AnotherHelloServer)
|
||||||
|
|
||||||
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -155,35 +246,48 @@ func TestServing(t *testing.T) {
|
||||||
|
|
||||||
c := new(Client)
|
c := new(Client)
|
||||||
m := new(Msg)
|
m := new(Msg)
|
||||||
m.SetQuestion("miek.nl.", TypeTXT)
|
|
||||||
r, _, err := c.Exchange(m, addrstr)
|
|
||||||
if err != nil || len(r.Extra) == 0 {
|
|
||||||
t.Fatal("failed to exchange miek.nl", err)
|
|
||||||
}
|
|
||||||
txt := r.Extra[0].(*TXT).Txt[0]
|
|
||||||
if txt != "Hello world" {
|
|
||||||
t.Error("unexpected result for miek.nl", txt, "!= Hello world")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Test the Z flag is not echoed
|
||||||
m.SetQuestion("example.com.", TypeTXT)
|
m.SetQuestion("example.com.", TypeTXT)
|
||||||
r, _, err = c.Exchange(m, addrstr)
|
m.Zero = true
|
||||||
|
r, _, err := c.Exchange(m, addrstr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("failed to exchange example.com", err)
|
t.Fatal("failed to exchange example.com with +zflag", err)
|
||||||
}
|
}
|
||||||
txt = r.Extra[0].(*TXT).Txt[0]
|
if r.Zero {
|
||||||
if txt != "Hello example" {
|
t.Error("the response should not have Z flag set - even for a query which does")
|
||||||
t.Error("unexpected result for example.com", txt, "!= Hello example")
|
|
||||||
}
|
}
|
||||||
|
if r.Rcode != RcodeSuccess {
|
||||||
|
t.Errorf("expected rcode %v, got %v", RcodeSuccess, r.Rcode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test Mixes cased as noticed by Ask.
|
// Verify that the server responds to a query with unsupported Opcode with a NotImplemented error and that Opcode is unchanged.
|
||||||
m.SetQuestion("eXaMplE.cOm.", TypeTXT)
|
func TestServeNotImplemented(t *testing.T) {
|
||||||
r, _, err = c.Exchange(m, addrstr)
|
HandleFunc("example.com.", AnotherHelloServer)
|
||||||
|
opcode := 15
|
||||||
|
|
||||||
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("failed to exchange eXaMplE.cOm", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
txt = r.Extra[0].(*TXT).Txt[0]
|
defer s.Shutdown()
|
||||||
if txt != "Hello example" {
|
|
||||||
t.Error("unexpected result for example.com", txt, "!= Hello example")
|
c := new(Client)
|
||||||
|
m := new(Msg)
|
||||||
|
|
||||||
|
// Test that Opcode is like the unchanged from request Opcode and that Rcode is set to NotImplemented
|
||||||
|
m.SetQuestion("example.com.", TypeTXT)
|
||||||
|
m.Opcode = opcode
|
||||||
|
r, _, err := c.Exchange(m, addrstr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to exchange example.com with +zflag", err)
|
||||||
|
}
|
||||||
|
if r.Opcode != opcode {
|
||||||
|
t.Errorf("expected opcode %v, got %v", opcode, r.Opcode)
|
||||||
|
}
|
||||||
|
if r.Rcode != RcodeNotImplemented {
|
||||||
|
t.Errorf("expected rcode %v, got %v", RcodeNotImplemented, r.Rcode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +306,7 @@ func TestServingTLS(t *testing.T) {
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
}
|
}
|
||||||
|
|
||||||
s, addrstr, err := RunLocalTLSServer(":0", &config)
|
s, addrstr, _, err := RunLocalTLSServer(":0", &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -288,7 +392,7 @@ func TestServingTLSConnectionState(t *testing.T) {
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
}
|
}
|
||||||
|
|
||||||
s, addrstr, err := RunLocalTLSServer(":0", &config)
|
s, addrstr, _, err := RunLocalTLSServer(":0", &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -311,7 +415,7 @@ func TestServingTLSConnectionState(t *testing.T) {
|
||||||
// UDP DNS Server
|
// UDP DNS Server
|
||||||
HandleFunc(".", tlsHandlerTLS(false))
|
HandleFunc(".", tlsHandlerTLS(false))
|
||||||
defer HandleRemove(".")
|
defer HandleRemove(".")
|
||||||
s, addrstr, err = RunLocalUDPServer(":0")
|
s, addrstr, _, err = RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -325,7 +429,7 @@ func TestServingTLSConnectionState(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TCP DNS Server
|
// TCP DNS Server
|
||||||
s, addrstr, err = RunLocalTCPServer(":0")
|
s, addrstr, _, err = RunLocalTCPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -409,7 +513,7 @@ func BenchmarkServe(b *testing.B) {
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
a := runtime.GOMAXPROCS(4)
|
a := runtime.GOMAXPROCS(4)
|
||||||
|
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("unable to run test server: %v", err)
|
b.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -434,7 +538,7 @@ func BenchmarkServe6(b *testing.B) {
|
||||||
HandleFunc("miek.nl.", HelloServer)
|
HandleFunc("miek.nl.", HelloServer)
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
a := runtime.GOMAXPROCS(4)
|
a := runtime.GOMAXPROCS(4)
|
||||||
s, addrstr, err := RunLocalUDPServer("[::1]:0")
|
s, addrstr, _, err := RunLocalUDPServer("[::1]:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "bind: cannot assign requested address") {
|
if strings.Contains(err.Error(), "bind: cannot assign requested address") {
|
||||||
b.Skip("missing IPv6 support")
|
b.Skip("missing IPv6 support")
|
||||||
|
@ -471,7 +575,7 @@ func BenchmarkServeCompress(b *testing.B) {
|
||||||
HandleFunc("miek.nl.", HelloServerCompress)
|
HandleFunc("miek.nl.", HelloServerCompress)
|
||||||
defer HandleRemove("miek.nl.")
|
defer HandleRemove("miek.nl.")
|
||||||
a := runtime.GOMAXPROCS(4)
|
a := runtime.GOMAXPROCS(4)
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("unable to run test server: %v", err)
|
b.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -524,7 +628,7 @@ func TestServingLargeResponses(t *testing.T) {
|
||||||
HandleFunc("example.", HelloServerLargeResponse)
|
HandleFunc("example.", HelloServerLargeResponse)
|
||||||
defer HandleRemove("example.")
|
defer HandleRemove("example.")
|
||||||
|
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -564,7 +668,7 @@ func TestServingResponse(t *testing.T) {
|
||||||
t.Skip("skipping test in short mode.")
|
t.Skip("skipping test in short mode.")
|
||||||
}
|
}
|
||||||
HandleFunc("miek.nl.", HelloServer)
|
HandleFunc("miek.nl.", HelloServer)
|
||||||
s, addrstr, err := RunLocalUDPServer(":0")
|
s, addrstr, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -578,7 +682,8 @@ func TestServingResponse(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("failed to exchange", err)
|
t.Fatal("failed to exchange", err)
|
||||||
}
|
}
|
||||||
m.Response = true
|
m.Response = true // this holds up the reply, set short read time out to avoid waiting too long
|
||||||
|
c.ReadTimeout = 100 * time.Millisecond
|
||||||
_, _, err = c.Exchange(m, addrstr)
|
_, _, err = c.Exchange(m, addrstr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("exchanged response message")
|
t.Fatal("exchanged response message")
|
||||||
|
@ -586,7 +691,7 @@ func TestServingResponse(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShutdownTCP(t *testing.T) {
|
func TestShutdownTCP(t *testing.T) {
|
||||||
s, _, fin, err := RunLocalTCPServerWithFinChan(":0")
|
s, _, fin, err := RunLocalTCPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -611,7 +716,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkInProgressQueriesAtShutdownServer(t *testing.T, srv *Server, addr string, client *Client) {
|
func checkInProgressQueriesAtShutdownServer(t *testing.T, srv *Server, addr string, client *Client) {
|
||||||
const requests = 100
|
const requests = 15 // enough to make this interesting? TODO: find a proper value
|
||||||
|
|
||||||
var errOnce sync.Once
|
var errOnce sync.Once
|
||||||
// t.Fail will panic if it's called after the test function has
|
// t.Fail will panic if it's called after the test function has
|
||||||
|
@ -641,7 +746,7 @@ func checkInProgressQueriesAtShutdownServer(t *testing.T, srv *Server, addr stri
|
||||||
})
|
})
|
||||||
defer HandleRemove("example.com.")
|
defer HandleRemove("example.com.")
|
||||||
|
|
||||||
client.Timeout = 10 * time.Second
|
client.Timeout = 1 * time.Second
|
||||||
|
|
||||||
conns := make([]*Conn, requests)
|
conns := make([]*Conn, requests)
|
||||||
eg := new(errgroup.Group)
|
eg := new(errgroup.Group)
|
||||||
|
@ -717,7 +822,7 @@ func checkInProgressQueriesAtShutdownServer(t *testing.T, srv *Server, addr stri
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInProgressQueriesAtShutdownTCP(t *testing.T) {
|
func TestInProgressQueriesAtShutdownTCP(t *testing.T) {
|
||||||
s, addr, _, err := RunLocalTCPServerWithFinChan(":0")
|
s, addr, _, err := RunLocalTCPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -736,7 +841,7 @@ func TestShutdownTLS(t *testing.T) {
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
}
|
}
|
||||||
|
|
||||||
s, _, err := RunLocalTLSServer(":0", &config)
|
s, _, _, err := RunLocalTLSServer(":0", &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -756,7 +861,7 @@ func TestInProgressQueriesAtShutdownTLS(t *testing.T) {
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
}
|
}
|
||||||
|
|
||||||
s, addr, err := RunLocalTLSServer(":0", &config)
|
s, addr, _, err := RunLocalTLSServer(":0", &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -771,7 +876,6 @@ func TestInProgressQueriesAtShutdownTLS(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandlerCloseTCP(t *testing.T) {
|
func TestHandlerCloseTCP(t *testing.T) {
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", ":0")
|
ln, err := net.Listen("tcp", ":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -816,7 +920,26 @@ func TestHandlerCloseTCP(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShutdownUDP(t *testing.T) {
|
func TestShutdownUDP(t *testing.T) {
|
||||||
s, _, fin, err := RunLocalUDPServerWithFinChan(":0")
|
s, _, fin, err := RunLocalUDPServer(":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
err = s.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not shutdown test UDP server, %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err := <-fin:
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error returned from ActivateAndServe, %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Error("could not shutdown test UDP server. Gave up waiting")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShutdownPacketConn(t *testing.T) {
|
||||||
|
s, _, fin, err := RunLocalPacketConnServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -835,7 +958,17 @@ func TestShutdownUDP(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInProgressQueriesAtShutdownUDP(t *testing.T) {
|
func TestInProgressQueriesAtShutdownUDP(t *testing.T) {
|
||||||
s, addr, _, err := RunLocalUDPServerWithFinChan(":0")
|
s, addr, _, err := RunLocalUDPServer(":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Client{Net: "udp"}
|
||||||
|
checkInProgressQueriesAtShutdownServer(t, s, addr, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInProgressQueriesAtShutdownPacketConn(t *testing.T) {
|
||||||
|
s, addr, _, err := RunLocalPacketConnServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -848,7 +981,7 @@ func TestServerStartStopRace(t *testing.T) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
s, _, _, err := RunLocalUDPServerWithFinChan(":0")
|
s, _, _, err := RunLocalUDPServer(":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not start server: %s", err)
|
t.Fatalf("could not start server: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -911,30 +1044,39 @@ func TestServerReuseport(t *testing.T) {
|
||||||
func TestServerRoundtripTsig(t *testing.T) {
|
func TestServerRoundtripTsig(t *testing.T) {
|
||||||
secret := map[string]string{"test.": "so6ZGir4GPAqINNh9U5c3A=="}
|
secret := map[string]string{"test.": "so6ZGir4GPAqINNh9U5c3A=="}
|
||||||
|
|
||||||
s, addrstr, _, err := RunLocalUDPServerWithFinChan(":0", func(srv *Server) {
|
s, addrstr, _, err := RunLocalUDPServer(":0", func(srv *Server) {
|
||||||
srv.TsigSecret = secret
|
srv.TsigSecret = secret
|
||||||
|
srv.MsgAcceptFunc = func(dh Header) MsgAcceptAction {
|
||||||
|
// defaultMsgAcceptFunc does reject UPDATE queries
|
||||||
|
return MsgAccept
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to run test server: %v", err)
|
t.Fatalf("unable to run test server: %v", err)
|
||||||
}
|
}
|
||||||
defer s.Shutdown()
|
defer s.Shutdown()
|
||||||
|
|
||||||
|
handlerFired := make(chan struct{})
|
||||||
HandleFunc("example.com.", func(w ResponseWriter, r *Msg) {
|
HandleFunc("example.com.", func(w ResponseWriter, r *Msg) {
|
||||||
|
close(handlerFired)
|
||||||
|
|
||||||
m := new(Msg)
|
m := new(Msg)
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
if r.IsTsig() != nil {
|
if r.IsTsig() != nil {
|
||||||
status := w.TsigStatus()
|
status := w.TsigStatus()
|
||||||
if status == nil {
|
if status == nil {
|
||||||
// *Msg r has an TSIG record and it was validated
|
// *Msg r has an TSIG record and it was validated
|
||||||
m.SetTsig("test.", HmacMD5, 300, time.Now().Unix())
|
m.SetTsig("test.", HmacSHA256, 300, time.Now().Unix())
|
||||||
} else {
|
} else {
|
||||||
// *Msg r has an TSIG records and it was not valided
|
// *Msg r has an TSIG records and it was not validated
|
||||||
t.Errorf("invalid TSIG: %v", status)
|
t.Errorf("invalid TSIG: %v", status)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.Error("missing TSIG")
|
t.Error("missing TSIG")
|
||||||
}
|
}
|
||||||
w.WriteMsg(m)
|
if err := w.WriteMsg(m); err != nil {
|
||||||
|
t.Error("writemsg failed", err)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
c := new(Client)
|
c := new(Client)
|
||||||
|
@ -951,11 +1093,17 @@ func TestServerRoundtripTsig(t *testing.T) {
|
||||||
Target: "bar.example.com.",
|
Target: "bar.example.com.",
|
||||||
}}
|
}}
|
||||||
c.TsigSecret = secret
|
c.TsigSecret = secret
|
||||||
m.SetTsig("test.", HmacMD5, 300, time.Now().Unix())
|
m.SetTsig("test.", HmacSHA256, 300, time.Now().Unix())
|
||||||
_, _, err = c.Exchange(m, addrstr)
|
_, _, err = c.Exchange(m, addrstr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("failed to exchange", err)
|
t.Fatal("failed to exchange", err)
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
|
case <-handlerFired:
|
||||||
|
// ok, handler was actually called
|
||||||
|
default:
|
||||||
|
t.Error("handler was not called")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResponseAfterClose(t *testing.T) {
|
func TestResponseAfterClose(t *testing.T) {
|
||||||
|
@ -989,6 +1137,37 @@ func TestResponseDoubleClose(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type countingConn struct {
|
||||||
|
net.Conn
|
||||||
|
writes int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *countingConn) Write(p []byte) (int, error) {
|
||||||
|
c.writes++
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponseWriteSinglePacket(t *testing.T) {
|
||||||
|
c := &countingConn{}
|
||||||
|
rw := &response{
|
||||||
|
tcp: c,
|
||||||
|
}
|
||||||
|
rw.writer = rw
|
||||||
|
|
||||||
|
m := new(Msg)
|
||||||
|
m.SetQuestion("miek.nl.", TypeTXT)
|
||||||
|
m.Response = true
|
||||||
|
err := rw.WriteMsg(m)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.writes != 1 {
|
||||||
|
t.Fatalf("incorrect number of Write calls")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ExampleFrameLengthWriter struct {
|
type ExampleFrameLengthWriter struct {
|
||||||
Writer
|
Writer
|
||||||
}
|
}
|
||||||
|
|
67
sig0.go
67
sig0.go
|
@ -2,8 +2,8 @@ package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/dsa"
|
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
@ -18,7 +18,7 @@ func (rr *SIG) Sign(k crypto.Signer, m *Msg) ([]byte, error) {
|
||||||
if k == nil {
|
if k == nil {
|
||||||
return nil, ErrPrivKey
|
return nil, ErrPrivKey
|
||||||
}
|
}
|
||||||
if rr.KeyTag == 0 || len(rr.SignerName) == 0 || rr.Algorithm == 0 {
|
if rr.KeyTag == 0 || rr.SignerName == "" || rr.Algorithm == 0 {
|
||||||
return nil, ErrKey
|
return nil, ErrKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,18 +39,17 @@ func (rr *SIG) Sign(k crypto.Signer, m *Msg) ([]byte, error) {
|
||||||
}
|
}
|
||||||
buf = buf[:off:cap(buf)]
|
buf = buf[:off:cap(buf)]
|
||||||
|
|
||||||
hash, ok := AlgorithmToHash[rr.Algorithm]
|
h, cryptohash, err := hashFromAlgorithm(rr.Algorithm)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return nil, ErrAlg
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
hasher := hash.New()
|
|
||||||
// Write SIG rdata
|
// Write SIG rdata
|
||||||
hasher.Write(buf[len(mbuf)+1+2+2+4+2:])
|
h.Write(buf[len(mbuf)+1+2+2+4+2:])
|
||||||
// Write message
|
// Write message
|
||||||
hasher.Write(buf[:len(mbuf)])
|
h.Write(buf[:len(mbuf)])
|
||||||
|
|
||||||
signature, err := sign(k, hasher.Sum(nil), hash, rr.Algorithm)
|
signature, err := sign(k, h.Sum(nil), cryptohash, rr.Algorithm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -79,24 +78,14 @@ func (rr *SIG) Verify(k *KEY, buf []byte) error {
|
||||||
if k == nil {
|
if k == nil {
|
||||||
return ErrKey
|
return ErrKey
|
||||||
}
|
}
|
||||||
if rr.KeyTag == 0 || len(rr.SignerName) == 0 || rr.Algorithm == 0 {
|
if rr.KeyTag == 0 || rr.SignerName == "" || rr.Algorithm == 0 {
|
||||||
return ErrKey
|
return ErrKey
|
||||||
}
|
}
|
||||||
|
|
||||||
var hash crypto.Hash
|
h, cryptohash, err := hashFromAlgorithm(rr.Algorithm)
|
||||||
switch rr.Algorithm {
|
if err != nil {
|
||||||
case DSA, RSASHA1:
|
return err
|
||||||
hash = crypto.SHA1
|
|
||||||
case RSASHA256, ECDSAP256SHA256:
|
|
||||||
hash = crypto.SHA256
|
|
||||||
case ECDSAP384SHA384:
|
|
||||||
hash = crypto.SHA384
|
|
||||||
case RSASHA512:
|
|
||||||
hash = crypto.SHA512
|
|
||||||
default:
|
|
||||||
return ErrAlg
|
|
||||||
}
|
}
|
||||||
hasher := hash.New()
|
|
||||||
|
|
||||||
buflen := len(buf)
|
buflen := len(buf)
|
||||||
qdc := binary.BigEndian.Uint16(buf[4:])
|
qdc := binary.BigEndian.Uint16(buf[4:])
|
||||||
|
@ -104,7 +93,6 @@ func (rr *SIG) Verify(k *KEY, buf []byte) error {
|
||||||
auc := binary.BigEndian.Uint16(buf[8:])
|
auc := binary.BigEndian.Uint16(buf[8:])
|
||||||
adc := binary.BigEndian.Uint16(buf[10:])
|
adc := binary.BigEndian.Uint16(buf[10:])
|
||||||
offset := headerSize
|
offset := headerSize
|
||||||
var err error
|
|
||||||
for i := uint16(0); i < qdc && offset < buflen; i++ {
|
for i := uint16(0); i < qdc && offset < buflen; i++ {
|
||||||
_, offset, err = UnpackDomainName(buf, offset)
|
_, offset, err = UnpackDomainName(buf, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -167,32 +155,21 @@ func (rr *SIG) Verify(k *KEY, buf []byte) error {
|
||||||
return &Error{err: "signer name doesn't match key name"}
|
return &Error{err: "signer name doesn't match key name"}
|
||||||
}
|
}
|
||||||
sigend := offset
|
sigend := offset
|
||||||
hasher.Write(buf[sigstart:sigend])
|
h.Write(buf[sigstart:sigend])
|
||||||
hasher.Write(buf[:10])
|
h.Write(buf[:10])
|
||||||
hasher.Write([]byte{
|
h.Write([]byte{
|
||||||
byte((adc - 1) << 8),
|
byte((adc - 1) << 8),
|
||||||
byte(adc - 1),
|
byte(adc - 1),
|
||||||
})
|
})
|
||||||
hasher.Write(buf[12:bodyend])
|
h.Write(buf[12:bodyend])
|
||||||
|
|
||||||
hashed := hasher.Sum(nil)
|
hashed := h.Sum(nil)
|
||||||
sig := buf[sigend:]
|
sig := buf[sigend:]
|
||||||
switch k.Algorithm {
|
switch k.Algorithm {
|
||||||
case DSA:
|
|
||||||
pk := k.publicKeyDSA()
|
|
||||||
sig = sig[1:]
|
|
||||||
r := new(big.Int).SetBytes(sig[:len(sig)/2])
|
|
||||||
s := new(big.Int).SetBytes(sig[len(sig)/2:])
|
|
||||||
if pk != nil {
|
|
||||||
if dsa.Verify(pk, hashed, r, s) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ErrSig
|
|
||||||
}
|
|
||||||
case RSASHA1, RSASHA256, RSASHA512:
|
case RSASHA1, RSASHA256, RSASHA512:
|
||||||
pk := k.publicKeyRSA()
|
pk := k.publicKeyRSA()
|
||||||
if pk != nil {
|
if pk != nil {
|
||||||
return rsa.VerifyPKCS1v15(pk, hash, hashed, sig)
|
return rsa.VerifyPKCS1v15(pk, cryptohash, hashed, sig)
|
||||||
}
|
}
|
||||||
case ECDSAP256SHA256, ECDSAP384SHA384:
|
case ECDSAP256SHA256, ECDSAP384SHA384:
|
||||||
pk := k.publicKeyECDSA()
|
pk := k.publicKeyECDSA()
|
||||||
|
@ -204,6 +181,14 @@ func (rr *SIG) Verify(k *KEY, buf []byte) error {
|
||||||
}
|
}
|
||||||
return ErrSig
|
return ErrSig
|
||||||
}
|
}
|
||||||
|
case ED25519:
|
||||||
|
pk := k.publicKeyED25519()
|
||||||
|
if pk != nil {
|
||||||
|
if ed25519.Verify(pk, hashed, sig) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ErrSig
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ErrKeyAlg
|
return ErrKeyAlg
|
||||||
}
|
}
|
||||||
|
|
22
sig0_test.go
22
sig0_test.go
|
@ -12,23 +12,25 @@ func TestSIG0(t *testing.T) {
|
||||||
}
|
}
|
||||||
m := new(Msg)
|
m := new(Msg)
|
||||||
m.SetQuestion("example.org.", TypeSOA)
|
m.SetQuestion("example.org.", TypeSOA)
|
||||||
for _, alg := range []uint8{ECDSAP256SHA256, ECDSAP384SHA384, RSASHA1, RSASHA256, RSASHA512} {
|
for _, alg := range []uint8{ECDSAP256SHA256, ECDSAP384SHA384, RSASHA1, RSASHA256, RSASHA512, ED25519} {
|
||||||
algstr := AlgorithmToString[alg]
|
algstr := AlgorithmToString[alg]
|
||||||
keyrr := new(KEY)
|
keyrr := new(KEY)
|
||||||
keyrr.Hdr.Name = algstr + "."
|
keyrr.Hdr.Name = algstr + "."
|
||||||
keyrr.Hdr.Rrtype = TypeKEY
|
keyrr.Hdr.Rrtype = TypeKEY
|
||||||
keyrr.Hdr.Class = ClassINET
|
keyrr.Hdr.Class = ClassINET
|
||||||
keyrr.Algorithm = alg
|
keyrr.Algorithm = alg
|
||||||
keysize := 1024
|
keysize := 512
|
||||||
switch alg {
|
switch alg {
|
||||||
case ECDSAP256SHA256:
|
case ECDSAP256SHA256, ED25519:
|
||||||
keysize = 256
|
keysize = 256
|
||||||
case ECDSAP384SHA384:
|
case ECDSAP384SHA384:
|
||||||
keysize = 384
|
keysize = 384
|
||||||
|
case RSASHA512:
|
||||||
|
keysize = 1024
|
||||||
}
|
}
|
||||||
pk, err := keyrr.Generate(keysize)
|
pk, err := keyrr.Generate(keysize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to generate key for “%s”: %v", algstr, err)
|
t.Errorf("failed to generate key for %q: %v", algstr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
now := uint32(time.Now().Unix())
|
now := uint32(time.Now().Unix())
|
||||||
|
@ -43,16 +45,16 @@ func TestSIG0(t *testing.T) {
|
||||||
sigrr.SignerName = keyrr.Hdr.Name
|
sigrr.SignerName = keyrr.Hdr.Name
|
||||||
mb, err := sigrr.Sign(pk.(crypto.Signer), m)
|
mb, err := sigrr.Sign(pk.(crypto.Signer), m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to sign message using “%s”: %v", algstr, err)
|
t.Errorf("failed to sign message using %q: %v", algstr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m := new(Msg)
|
m := new(Msg)
|
||||||
if err := m.Unpack(mb); err != nil {
|
if err := m.Unpack(mb); err != nil {
|
||||||
t.Errorf("failed to unpack message signed using “%s”: %v", algstr, err)
|
t.Errorf("failed to unpack message signed using %q: %v", algstr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(m.Extra) != 1 {
|
if len(m.Extra) != 1 {
|
||||||
t.Errorf("missing SIG for message signed using “%s”", algstr)
|
t.Errorf("missing SIG for message signed using %q", algstr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var sigrrwire *SIG
|
var sigrrwire *SIG
|
||||||
|
@ -69,20 +71,20 @@ func TestSIG0(t *testing.T) {
|
||||||
id = "sigrrwire"
|
id = "sigrrwire"
|
||||||
}
|
}
|
||||||
if err := rr.Verify(keyrr, mb); err != nil {
|
if err := rr.Verify(keyrr, mb); err != nil {
|
||||||
t.Errorf("failed to verify “%s” signed SIG(%s): %v", algstr, id, err)
|
t.Errorf("failed to verify %q signed SIG(%s): %v", algstr, id, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mb[13]++
|
mb[13]++
|
||||||
if err := sigrr.Verify(keyrr, mb); err == nil {
|
if err := sigrr.Verify(keyrr, mb); err == nil {
|
||||||
t.Errorf("verify succeeded on an altered message using “%s”", algstr)
|
t.Errorf("verify succeeded on an altered message using %q", algstr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sigrr.Expiration = 2
|
sigrr.Expiration = 2
|
||||||
sigrr.Inception = 1
|
sigrr.Inception = 1
|
||||||
mb, _ = sigrr.Sign(pk.(crypto.Signer), m)
|
mb, _ = sigrr.Sign(pk.(crypto.Signer), m)
|
||||||
if err := sigrr.Verify(keyrr, mb); err == nil {
|
if err := sigrr.Verify(keyrr, mb); err == nil {
|
||||||
t.Errorf("verify succeeded on an expired message using “%s”", algstr)
|
t.Errorf("verify succeeded on an expired message using %q", algstr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,935 @@
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SVCBKey is the type of the keys used in the SVCB RR.
|
||||||
|
type SVCBKey uint16
|
||||||
|
|
||||||
|
// Keys defined in draft-ietf-dnsop-svcb-https-08 Section 14.3.2.
|
||||||
|
const (
|
||||||
|
SVCB_MANDATORY SVCBKey = iota
|
||||||
|
SVCB_ALPN
|
||||||
|
SVCB_NO_DEFAULT_ALPN
|
||||||
|
SVCB_PORT
|
||||||
|
SVCB_IPV4HINT
|
||||||
|
SVCB_ECHCONFIG
|
||||||
|
SVCB_IPV6HINT
|
||||||
|
SVCB_DOHPATH // draft-ietf-add-svcb-dns-02 Section 9
|
||||||
|
|
||||||
|
svcb_RESERVED SVCBKey = 65535
|
||||||
|
)
|
||||||
|
|
||||||
|
var svcbKeyToStringMap = map[SVCBKey]string{
|
||||||
|
SVCB_MANDATORY: "mandatory",
|
||||||
|
SVCB_ALPN: "alpn",
|
||||||
|
SVCB_NO_DEFAULT_ALPN: "no-default-alpn",
|
||||||
|
SVCB_PORT: "port",
|
||||||
|
SVCB_IPV4HINT: "ipv4hint",
|
||||||
|
SVCB_ECHCONFIG: "ech",
|
||||||
|
SVCB_IPV6HINT: "ipv6hint",
|
||||||
|
SVCB_DOHPATH: "dohpath",
|
||||||
|
}
|
||||||
|
|
||||||
|
var svcbStringToKeyMap = reverseSVCBKeyMap(svcbKeyToStringMap)
|
||||||
|
|
||||||
|
func reverseSVCBKeyMap(m map[SVCBKey]string) map[string]SVCBKey {
|
||||||
|
n := make(map[string]SVCBKey, len(m))
|
||||||
|
for u, s := range m {
|
||||||
|
n[s] = u
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// String takes the numerical code of an SVCB key and returns its name.
|
||||||
|
// Returns an empty string for reserved keys.
|
||||||
|
// Accepts unassigned keys as well as experimental/private keys.
|
||||||
|
func (key SVCBKey) String() string {
|
||||||
|
if x := svcbKeyToStringMap[key]; x != "" {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
if key == svcb_RESERVED {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "key" + strconv.FormatUint(uint64(key), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// svcbStringToKey returns the numerical code of an SVCB key.
|
||||||
|
// Returns svcb_RESERVED for reserved/invalid keys.
|
||||||
|
// Accepts unassigned keys as well as experimental/private keys.
|
||||||
|
func svcbStringToKey(s string) SVCBKey {
|
||||||
|
if strings.HasPrefix(s, "key") {
|
||||||
|
a, err := strconv.ParseUint(s[3:], 10, 16)
|
||||||
|
// no leading zeros
|
||||||
|
// key shouldn't be registered
|
||||||
|
if err != nil || a == 65535 || s[3] == '0' || svcbKeyToStringMap[SVCBKey(a)] != "" {
|
||||||
|
return svcb_RESERVED
|
||||||
|
}
|
||||||
|
return SVCBKey(a)
|
||||||
|
}
|
||||||
|
if key, ok := svcbStringToKeyMap[s]; ok {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return svcb_RESERVED
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *SVCB) parse(c *zlexer, o string) *ParseError {
|
||||||
|
l, _ := c.Next()
|
||||||
|
i, e := strconv.ParseUint(l.token, 10, 16)
|
||||||
|
if e != nil || l.err {
|
||||||
|
return &ParseError{l.token, "bad SVCB priority", l}
|
||||||
|
}
|
||||||
|
rr.Priority = uint16(i)
|
||||||
|
|
||||||
|
c.Next() // zBlank
|
||||||
|
l, _ = c.Next() // zString
|
||||||
|
rr.Target = l.token
|
||||||
|
|
||||||
|
name, nameOk := toAbsoluteName(l.token, o)
|
||||||
|
if l.err || !nameOk {
|
||||||
|
return &ParseError{l.token, "bad SVCB Target", l}
|
||||||
|
}
|
||||||
|
rr.Target = name
|
||||||
|
|
||||||
|
// Values (if any)
|
||||||
|
l, _ = c.Next()
|
||||||
|
var xs []SVCBKeyValue
|
||||||
|
// Helps require whitespace between pairs.
|
||||||
|
// Prevents key1000="a"key1001=...
|
||||||
|
canHaveNextKey := true
|
||||||
|
for l.value != zNewline && l.value != zEOF {
|
||||||
|
switch l.value {
|
||||||
|
case zString:
|
||||||
|
if !canHaveNextKey {
|
||||||
|
// The key we can now read was probably meant to be
|
||||||
|
// a part of the last value.
|
||||||
|
return &ParseError{l.token, "bad SVCB value quotation", l}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In key=value pairs, value does not have to be quoted unless value
|
||||||
|
// contains whitespace. And keys don't need to have values.
|
||||||
|
// Similarly, keys with an equality signs after them don't need values.
|
||||||
|
// l.token includes at least up to the first equality sign.
|
||||||
|
idx := strings.IndexByte(l.token, '=')
|
||||||
|
var key, value string
|
||||||
|
if idx < 0 {
|
||||||
|
// Key with no value and no equality sign
|
||||||
|
key = l.token
|
||||||
|
} else if idx == 0 {
|
||||||
|
return &ParseError{l.token, "bad SVCB key", l}
|
||||||
|
} else {
|
||||||
|
key, value = l.token[:idx], l.token[idx+1:]
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
// We have a key and an equality sign. Maybe we have nothing
|
||||||
|
// after "=" or we have a double quote.
|
||||||
|
l, _ = c.Next()
|
||||||
|
if l.value == zQuote {
|
||||||
|
// Only needed when value ends with double quotes.
|
||||||
|
// Any value starting with zQuote ends with it.
|
||||||
|
canHaveNextKey = false
|
||||||
|
|
||||||
|
l, _ = c.Next()
|
||||||
|
switch l.value {
|
||||||
|
case zString:
|
||||||
|
// We have a value in double quotes.
|
||||||
|
value = l.token
|
||||||
|
l, _ = c.Next()
|
||||||
|
if l.value != zQuote {
|
||||||
|
return &ParseError{l.token, "SVCB unterminated value", l}
|
||||||
|
}
|
||||||
|
case zQuote:
|
||||||
|
// There's nothing in double quotes.
|
||||||
|
default:
|
||||||
|
return &ParseError{l.token, "bad SVCB value", l}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kv := makeSVCBKeyValue(svcbStringToKey(key))
|
||||||
|
if kv == nil {
|
||||||
|
return &ParseError{l.token, "bad SVCB key", l}
|
||||||
|
}
|
||||||
|
if err := kv.parse(value); err != nil {
|
||||||
|
return &ParseError{l.token, err.Error(), l}
|
||||||
|
}
|
||||||
|
xs = append(xs, kv)
|
||||||
|
case zQuote:
|
||||||
|
return &ParseError{l.token, "SVCB key can't contain double quotes", l}
|
||||||
|
case zBlank:
|
||||||
|
canHaveNextKey = true
|
||||||
|
default:
|
||||||
|
return &ParseError{l.token, "bad SVCB values", l}
|
||||||
|
}
|
||||||
|
l, _ = c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// "In AliasMode, records SHOULD NOT include any SvcParams, and recipients MUST
|
||||||
|
// ignore any SvcParams that are present."
|
||||||
|
// However, we don't check rr.Priority == 0 && len(xs) > 0 here
|
||||||
|
// It is the responsibility of the user of the library to check this.
|
||||||
|
// This is to encourage the fixing of the source of this error.
|
||||||
|
|
||||||
|
rr.Value = xs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeSVCBKeyValue returns an SVCBKeyValue struct with the key or nil for reserved keys.
|
||||||
|
func makeSVCBKeyValue(key SVCBKey) SVCBKeyValue {
|
||||||
|
switch key {
|
||||||
|
case SVCB_MANDATORY:
|
||||||
|
return new(SVCBMandatory)
|
||||||
|
case SVCB_ALPN:
|
||||||
|
return new(SVCBAlpn)
|
||||||
|
case SVCB_NO_DEFAULT_ALPN:
|
||||||
|
return new(SVCBNoDefaultAlpn)
|
||||||
|
case SVCB_PORT:
|
||||||
|
return new(SVCBPort)
|
||||||
|
case SVCB_IPV4HINT:
|
||||||
|
return new(SVCBIPv4Hint)
|
||||||
|
case SVCB_ECHCONFIG:
|
||||||
|
return new(SVCBECHConfig)
|
||||||
|
case SVCB_IPV6HINT:
|
||||||
|
return new(SVCBIPv6Hint)
|
||||||
|
case SVCB_DOHPATH:
|
||||||
|
return new(SVCBDoHPath)
|
||||||
|
case svcb_RESERVED:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
e := new(SVCBLocal)
|
||||||
|
e.KeyCode = key
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCB RR. See RFC xxxx (https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-08).
|
||||||
|
//
|
||||||
|
// NOTE: The HTTPS/SVCB RFCs are in the draft stage.
|
||||||
|
// The API, including constants and types related to SVCBKeyValues, may
|
||||||
|
// change in future versions in accordance with the latest drafts.
|
||||||
|
type SVCB struct {
|
||||||
|
Hdr RR_Header
|
||||||
|
Priority uint16 // If zero, Value must be empty or discarded by the user of this library
|
||||||
|
Target string `dns:"domain-name"`
|
||||||
|
Value []SVCBKeyValue `dns:"pairs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPS RR. Everything valid for SVCB applies to HTTPS as well.
|
||||||
|
// Except that the HTTPS record is intended for use with the HTTP and HTTPS protocols.
|
||||||
|
//
|
||||||
|
// NOTE: The HTTPS/SVCB RFCs are in the draft stage.
|
||||||
|
// The API, including constants and types related to SVCBKeyValues, may
|
||||||
|
// change in future versions in accordance with the latest drafts.
|
||||||
|
type HTTPS struct {
|
||||||
|
SVCB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *HTTPS) String() string {
|
||||||
|
return rr.SVCB.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *HTTPS) parse(c *zlexer, o string) *ParseError {
|
||||||
|
return rr.SVCB.parse(c, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCBKeyValue defines a key=value pair for the SVCB RR type.
|
||||||
|
// An SVCB RR can have multiple SVCBKeyValues appended to it.
|
||||||
|
type SVCBKeyValue interface {
|
||||||
|
Key() SVCBKey // Key returns the numerical key code.
|
||||||
|
pack() ([]byte, error) // pack returns the encoded value.
|
||||||
|
unpack([]byte) error // unpack sets the value.
|
||||||
|
String() string // String returns the string representation of the value.
|
||||||
|
parse(string) error // parse sets the value to the given string representation of the value.
|
||||||
|
copy() SVCBKeyValue // copy returns a deep-copy of the pair.
|
||||||
|
len() int // len returns the length of value in the wire format.
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCBMandatory pair adds to required keys that must be interpreted for the RR
|
||||||
|
// to be functional. If ignored, the whole RRSet must be ignored.
|
||||||
|
// "port" and "no-default-alpn" are mandatory by default if present,
|
||||||
|
// so they shouldn't be included here.
|
||||||
|
//
|
||||||
|
// It is incumbent upon the user of this library to reject the RRSet if
|
||||||
|
// or avoid constructing such an RRSet that:
|
||||||
|
// - "mandatory" is included as one of the keys of mandatory
|
||||||
|
// - no key is listed multiple times in mandatory
|
||||||
|
// - all keys listed in mandatory are present
|
||||||
|
// - escape sequences are not used in mandatory
|
||||||
|
// - mandatory, when present, lists at least one key
|
||||||
|
//
|
||||||
|
// Basic use pattern for creating a mandatory option:
|
||||||
|
//
|
||||||
|
// s := &dns.SVCB{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeSVCB, Class: dns.ClassINET}}
|
||||||
|
// e := new(dns.SVCBMandatory)
|
||||||
|
// e.Code = []uint16{dns.SVCB_ALPN}
|
||||||
|
// s.Value = append(s.Value, e)
|
||||||
|
// t := new(dns.SVCBAlpn)
|
||||||
|
// t.Alpn = []string{"xmpp-client"}
|
||||||
|
// s.Value = append(s.Value, t)
|
||||||
|
type SVCBMandatory struct {
|
||||||
|
Code []SVCBKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SVCBMandatory) Key() SVCBKey { return SVCB_MANDATORY }
|
||||||
|
|
||||||
|
func (s *SVCBMandatory) String() string {
|
||||||
|
str := make([]string, len(s.Code))
|
||||||
|
for i, e := range s.Code {
|
||||||
|
str[i] = e.String()
|
||||||
|
}
|
||||||
|
return strings.Join(str, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBMandatory) pack() ([]byte, error) {
|
||||||
|
codes := append([]SVCBKey(nil), s.Code...)
|
||||||
|
sort.Slice(codes, func(i, j int) bool {
|
||||||
|
return codes[i] < codes[j]
|
||||||
|
})
|
||||||
|
b := make([]byte, 2*len(codes))
|
||||||
|
for i, e := range codes {
|
||||||
|
binary.BigEndian.PutUint16(b[2*i:], uint16(e))
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBMandatory) unpack(b []byte) error {
|
||||||
|
if len(b)%2 != 0 {
|
||||||
|
return errors.New("dns: svcbmandatory: value length is not a multiple of 2")
|
||||||
|
}
|
||||||
|
codes := make([]SVCBKey, 0, len(b)/2)
|
||||||
|
for i := 0; i < len(b); i += 2 {
|
||||||
|
// We assume strictly increasing order.
|
||||||
|
codes = append(codes, SVCBKey(binary.BigEndian.Uint16(b[i:])))
|
||||||
|
}
|
||||||
|
s.Code = codes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBMandatory) parse(b string) error {
|
||||||
|
str := strings.Split(b, ",")
|
||||||
|
codes := make([]SVCBKey, 0, len(str))
|
||||||
|
for _, e := range str {
|
||||||
|
codes = append(codes, svcbStringToKey(e))
|
||||||
|
}
|
||||||
|
s.Code = codes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBMandatory) len() int {
|
||||||
|
return 2 * len(s.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBMandatory) copy() SVCBKeyValue {
|
||||||
|
return &SVCBMandatory{
|
||||||
|
append([]SVCBKey(nil), s.Code...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCBAlpn pair is used to list supported connection protocols.
|
||||||
|
// The user of this library must ensure that at least one protocol is listed when alpn is present.
|
||||||
|
// Protocol IDs can be found at:
|
||||||
|
// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
|
||||||
|
// Basic use pattern for creating an alpn option:
|
||||||
|
//
|
||||||
|
// h := new(dns.HTTPS)
|
||||||
|
// h.Hdr = dns.RR_Header{Name: ".", Rrtype: dns.TypeHTTPS, Class: dns.ClassINET}
|
||||||
|
// e := new(dns.SVCBAlpn)
|
||||||
|
// e.Alpn = []string{"h2", "http/1.1"}
|
||||||
|
// h.Value = append(h.Value, e)
|
||||||
|
type SVCBAlpn struct {
|
||||||
|
Alpn []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SVCBAlpn) Key() SVCBKey { return SVCB_ALPN }
|
||||||
|
|
||||||
|
func (s *SVCBAlpn) String() string {
|
||||||
|
// An ALPN value is a comma-separated list of values, each of which can be
|
||||||
|
// an arbitrary binary value. In order to allow parsing, the comma and
|
||||||
|
// backslash characters are themselves escaped.
|
||||||
|
//
|
||||||
|
// However, this escaping is done in addition to the normal escaping which
|
||||||
|
// happens in zone files, meaning that these values must be
|
||||||
|
// double-escaped. This looks terrible, so if you see a never-ending
|
||||||
|
// sequence of backslash in a zone file this may be why.
|
||||||
|
//
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-08#appendix-A.1
|
||||||
|
var str strings.Builder
|
||||||
|
for i, alpn := range s.Alpn {
|
||||||
|
// 4*len(alpn) is the worst case where we escape every character in the alpn as \123, plus 1 byte for the ',' separating the alpn from others
|
||||||
|
str.Grow(4*len(alpn) + 1)
|
||||||
|
if i > 0 {
|
||||||
|
str.WriteByte(',')
|
||||||
|
}
|
||||||
|
for j := 0; j < len(alpn); j++ {
|
||||||
|
e := alpn[j]
|
||||||
|
if ' ' > e || e > '~' {
|
||||||
|
str.WriteString(escapeByte(e))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch e {
|
||||||
|
// We escape a few characters which may confuse humans or parsers.
|
||||||
|
case '"', ';', ' ':
|
||||||
|
str.WriteByte('\\')
|
||||||
|
str.WriteByte(e)
|
||||||
|
// The comma and backslash characters themselves must be
|
||||||
|
// doubly-escaped. We use `\\` for the first backslash and
|
||||||
|
// the escaped numeric value for the other value. We especially
|
||||||
|
// don't want a comma in the output.
|
||||||
|
case ',':
|
||||||
|
str.WriteString(`\\\044`)
|
||||||
|
case '\\':
|
||||||
|
str.WriteString(`\\\092`)
|
||||||
|
default:
|
||||||
|
str.WriteByte(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBAlpn) pack() ([]byte, error) {
|
||||||
|
// Liberally estimate the size of an alpn as 10 octets
|
||||||
|
b := make([]byte, 0, 10*len(s.Alpn))
|
||||||
|
for _, e := range s.Alpn {
|
||||||
|
if e == "" {
|
||||||
|
return nil, errors.New("dns: svcbalpn: empty alpn-id")
|
||||||
|
}
|
||||||
|
if len(e) > 255 {
|
||||||
|
return nil, errors.New("dns: svcbalpn: alpn-id too long")
|
||||||
|
}
|
||||||
|
b = append(b, byte(len(e)))
|
||||||
|
b = append(b, e...)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBAlpn) unpack(b []byte) error {
|
||||||
|
// Estimate the size of the smallest alpn as 4 bytes
|
||||||
|
alpn := make([]string, 0, len(b)/4)
|
||||||
|
for i := 0; i < len(b); {
|
||||||
|
length := int(b[i])
|
||||||
|
i++
|
||||||
|
if i+length > len(b) {
|
||||||
|
return errors.New("dns: svcbalpn: alpn array overflowing")
|
||||||
|
}
|
||||||
|
alpn = append(alpn, string(b[i:i+length]))
|
||||||
|
i += length
|
||||||
|
}
|
||||||
|
s.Alpn = alpn
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBAlpn) parse(b string) error {
|
||||||
|
if len(b) == 0 {
|
||||||
|
s.Alpn = []string{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
alpn := []string{}
|
||||||
|
a := []byte{}
|
||||||
|
for p := 0; p < len(b); {
|
||||||
|
c, q := nextByte(b, p)
|
||||||
|
if q == 0 {
|
||||||
|
return errors.New("dns: svcbalpn: unterminated escape")
|
||||||
|
}
|
||||||
|
p += q
|
||||||
|
// If we find a comma, we have finished reading an alpn.
|
||||||
|
if c == ',' {
|
||||||
|
if len(a) == 0 {
|
||||||
|
return errors.New("dns: svcbalpn: empty protocol identifier")
|
||||||
|
}
|
||||||
|
alpn = append(alpn, string(a))
|
||||||
|
a = []byte{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If it's a backslash, we need to handle a comma-separated list.
|
||||||
|
if c == '\\' {
|
||||||
|
dc, dq := nextByte(b, p)
|
||||||
|
if dq == 0 {
|
||||||
|
return errors.New("dns: svcbalpn: unterminated escape decoding comma-separated list")
|
||||||
|
}
|
||||||
|
if dc != '\\' && dc != ',' {
|
||||||
|
return errors.New("dns: svcbalpn: bad escaped character decoding comma-separated list")
|
||||||
|
}
|
||||||
|
p += dq
|
||||||
|
c = dc
|
||||||
|
}
|
||||||
|
a = append(a, c)
|
||||||
|
}
|
||||||
|
// Add the final alpn.
|
||||||
|
if len(a) == 0 {
|
||||||
|
return errors.New("dns: svcbalpn: last protocol identifier empty")
|
||||||
|
}
|
||||||
|
s.Alpn = append(alpn, string(a))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBAlpn) len() int {
|
||||||
|
var l int
|
||||||
|
for _, e := range s.Alpn {
|
||||||
|
l += 1 + len(e)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBAlpn) copy() SVCBKeyValue {
|
||||||
|
return &SVCBAlpn{
|
||||||
|
append([]string(nil), s.Alpn...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCBNoDefaultAlpn pair signifies no support for default connection protocols.
|
||||||
|
// Should be used in conjunction with alpn.
|
||||||
|
// Basic use pattern for creating a no-default-alpn option:
|
||||||
|
//
|
||||||
|
// s := &dns.SVCB{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeSVCB, Class: dns.ClassINET}}
|
||||||
|
// t := new(dns.SVCBAlpn)
|
||||||
|
// t.Alpn = []string{"xmpp-client"}
|
||||||
|
// s.Value = append(s.Value, t)
|
||||||
|
// e := new(dns.SVCBNoDefaultAlpn)
|
||||||
|
// s.Value = append(s.Value, e)
|
||||||
|
type SVCBNoDefaultAlpn struct{}
|
||||||
|
|
||||||
|
func (*SVCBNoDefaultAlpn) Key() SVCBKey { return SVCB_NO_DEFAULT_ALPN }
|
||||||
|
func (*SVCBNoDefaultAlpn) copy() SVCBKeyValue { return &SVCBNoDefaultAlpn{} }
|
||||||
|
func (*SVCBNoDefaultAlpn) pack() ([]byte, error) { return []byte{}, nil }
|
||||||
|
func (*SVCBNoDefaultAlpn) String() string { return "" }
|
||||||
|
func (*SVCBNoDefaultAlpn) len() int { return 0 }
|
||||||
|
|
||||||
|
func (*SVCBNoDefaultAlpn) unpack(b []byte) error {
|
||||||
|
if len(b) != 0 {
|
||||||
|
return errors.New("dns: svcbnodefaultalpn: no-default-alpn must have no value")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SVCBNoDefaultAlpn) parse(b string) error {
|
||||||
|
if b != "" {
|
||||||
|
return errors.New("dns: svcbnodefaultalpn: no-default-alpn must have no value")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCBPort pair defines the port for connection.
|
||||||
|
// Basic use pattern for creating a port option:
|
||||||
|
//
|
||||||
|
// s := &dns.SVCB{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeSVCB, Class: dns.ClassINET}}
|
||||||
|
// e := new(dns.SVCBPort)
|
||||||
|
// e.Port = 80
|
||||||
|
// s.Value = append(s.Value, e)
|
||||||
|
type SVCBPort struct {
|
||||||
|
Port uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SVCBPort) Key() SVCBKey { return SVCB_PORT }
|
||||||
|
func (*SVCBPort) len() int { return 2 }
|
||||||
|
func (s *SVCBPort) String() string { return strconv.FormatUint(uint64(s.Port), 10) }
|
||||||
|
func (s *SVCBPort) copy() SVCBKeyValue { return &SVCBPort{s.Port} }
|
||||||
|
|
||||||
|
func (s *SVCBPort) unpack(b []byte) error {
|
||||||
|
if len(b) != 2 {
|
||||||
|
return errors.New("dns: svcbport: port length is not exactly 2 octets")
|
||||||
|
}
|
||||||
|
s.Port = binary.BigEndian.Uint16(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBPort) pack() ([]byte, error) {
|
||||||
|
b := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(b, s.Port)
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBPort) parse(b string) error {
|
||||||
|
port, err := strconv.ParseUint(b, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("dns: svcbport: port out of range")
|
||||||
|
}
|
||||||
|
s.Port = uint16(port)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCBIPv4Hint pair suggests an IPv4 address which may be used to open connections
|
||||||
|
// if A and AAAA record responses for SVCB's Target domain haven't been received.
|
||||||
|
// In that case, optionally, A and AAAA requests can be made, after which the connection
|
||||||
|
// to the hinted IP address may be terminated and a new connection may be opened.
|
||||||
|
// Basic use pattern for creating an ipv4hint option:
|
||||||
|
//
|
||||||
|
// h := new(dns.HTTPS)
|
||||||
|
// h.Hdr = dns.RR_Header{Name: ".", Rrtype: dns.TypeHTTPS, Class: dns.ClassINET}
|
||||||
|
// e := new(dns.SVCBIPv4Hint)
|
||||||
|
// e.Hint = []net.IP{net.IPv4(1,1,1,1).To4()}
|
||||||
|
//
|
||||||
|
// Or
|
||||||
|
//
|
||||||
|
// e.Hint = []net.IP{net.ParseIP("1.1.1.1").To4()}
|
||||||
|
// h.Value = append(h.Value, e)
|
||||||
|
type SVCBIPv4Hint struct {
|
||||||
|
Hint []net.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SVCBIPv4Hint) Key() SVCBKey { return SVCB_IPV4HINT }
|
||||||
|
func (s *SVCBIPv4Hint) len() int { return 4 * len(s.Hint) }
|
||||||
|
|
||||||
|
func (s *SVCBIPv4Hint) pack() ([]byte, error) {
|
||||||
|
b := make([]byte, 0, 4*len(s.Hint))
|
||||||
|
for _, e := range s.Hint {
|
||||||
|
x := e.To4()
|
||||||
|
if x == nil {
|
||||||
|
return nil, errors.New("dns: svcbipv4hint: expected ipv4, hint is ipv6")
|
||||||
|
}
|
||||||
|
b = append(b, x...)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBIPv4Hint) unpack(b []byte) error {
|
||||||
|
if len(b) == 0 || len(b)%4 != 0 {
|
||||||
|
return errors.New("dns: svcbipv4hint: ipv4 address byte array length is not a multiple of 4")
|
||||||
|
}
|
||||||
|
x := make([]net.IP, 0, len(b)/4)
|
||||||
|
for i := 0; i < len(b); i += 4 {
|
||||||
|
x = append(x, net.IP(b[i:i+4]))
|
||||||
|
}
|
||||||
|
s.Hint = x
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBIPv4Hint) String() string {
|
||||||
|
str := make([]string, len(s.Hint))
|
||||||
|
for i, e := range s.Hint {
|
||||||
|
x := e.To4()
|
||||||
|
if x == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
str[i] = x.String()
|
||||||
|
}
|
||||||
|
return strings.Join(str, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBIPv4Hint) parse(b string) error {
|
||||||
|
if strings.Contains(b, ":") {
|
||||||
|
return errors.New("dns: svcbipv4hint: expected ipv4, got ipv6")
|
||||||
|
}
|
||||||
|
str := strings.Split(b, ",")
|
||||||
|
dst := make([]net.IP, len(str))
|
||||||
|
for i, e := range str {
|
||||||
|
ip := net.ParseIP(e).To4()
|
||||||
|
if ip == nil {
|
||||||
|
return errors.New("dns: svcbipv4hint: bad ip")
|
||||||
|
}
|
||||||
|
dst[i] = ip
|
||||||
|
}
|
||||||
|
s.Hint = dst
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBIPv4Hint) copy() SVCBKeyValue {
|
||||||
|
hint := make([]net.IP, len(s.Hint))
|
||||||
|
for i, ip := range s.Hint {
|
||||||
|
hint[i] = copyIP(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SVCBIPv4Hint{
|
||||||
|
Hint: hint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCBECHConfig pair contains the ECHConfig structure defined in draft-ietf-tls-esni [RFC xxxx].
|
||||||
|
// Basic use pattern for creating an ech option:
|
||||||
|
//
|
||||||
|
// h := new(dns.HTTPS)
|
||||||
|
// h.Hdr = dns.RR_Header{Name: ".", Rrtype: dns.TypeHTTPS, Class: dns.ClassINET}
|
||||||
|
// e := new(dns.SVCBECHConfig)
|
||||||
|
// e.ECH = []byte{0xfe, 0x08, ...}
|
||||||
|
// h.Value = append(h.Value, e)
|
||||||
|
type SVCBECHConfig struct {
|
||||||
|
ECH []byte // Specifically ECHConfigList including the redundant length prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SVCBECHConfig) Key() SVCBKey { return SVCB_ECHCONFIG }
|
||||||
|
func (s *SVCBECHConfig) String() string { return toBase64(s.ECH) }
|
||||||
|
func (s *SVCBECHConfig) len() int { return len(s.ECH) }
|
||||||
|
|
||||||
|
func (s *SVCBECHConfig) pack() ([]byte, error) {
|
||||||
|
return append([]byte(nil), s.ECH...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBECHConfig) copy() SVCBKeyValue {
|
||||||
|
return &SVCBECHConfig{
|
||||||
|
append([]byte(nil), s.ECH...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBECHConfig) unpack(b []byte) error {
|
||||||
|
s.ECH = append([]byte(nil), b...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *SVCBECHConfig) parse(b string) error {
|
||||||
|
x, err := fromBase64([]byte(b))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("dns: svcbech: bad base64 ech")
|
||||||
|
}
|
||||||
|
s.ECH = x
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCBIPv6Hint pair suggests an IPv6 address which may be used to open connections
|
||||||
|
// if A and AAAA record responses for SVCB's Target domain haven't been received.
|
||||||
|
// In that case, optionally, A and AAAA requests can be made, after which the
|
||||||
|
// connection to the hinted IP address may be terminated and a new connection may be opened.
|
||||||
|
// Basic use pattern for creating an ipv6hint option:
|
||||||
|
//
|
||||||
|
// h := new(dns.HTTPS)
|
||||||
|
// h.Hdr = dns.RR_Header{Name: ".", Rrtype: dns.TypeHTTPS, Class: dns.ClassINET}
|
||||||
|
// e := new(dns.SVCBIPv6Hint)
|
||||||
|
// e.Hint = []net.IP{net.ParseIP("2001:db8::1")}
|
||||||
|
// h.Value = append(h.Value, e)
|
||||||
|
type SVCBIPv6Hint struct {
|
||||||
|
Hint []net.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SVCBIPv6Hint) Key() SVCBKey { return SVCB_IPV6HINT }
|
||||||
|
func (s *SVCBIPv6Hint) len() int { return 16 * len(s.Hint) }
|
||||||
|
|
||||||
|
func (s *SVCBIPv6Hint) pack() ([]byte, error) {
|
||||||
|
b := make([]byte, 0, 16*len(s.Hint))
|
||||||
|
for _, e := range s.Hint {
|
||||||
|
if len(e) != net.IPv6len || e.To4() != nil {
|
||||||
|
return nil, errors.New("dns: svcbipv6hint: expected ipv6, hint is ipv4")
|
||||||
|
}
|
||||||
|
b = append(b, e...)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBIPv6Hint) unpack(b []byte) error {
|
||||||
|
if len(b) == 0 || len(b)%16 != 0 {
|
||||||
|
return errors.New("dns: svcbipv6hint: ipv6 address byte array length not a multiple of 16")
|
||||||
|
}
|
||||||
|
x := make([]net.IP, 0, len(b)/16)
|
||||||
|
for i := 0; i < len(b); i += 16 {
|
||||||
|
ip := net.IP(b[i : i+16])
|
||||||
|
if ip.To4() != nil {
|
||||||
|
return errors.New("dns: svcbipv6hint: expected ipv6, got ipv4")
|
||||||
|
}
|
||||||
|
x = append(x, ip)
|
||||||
|
}
|
||||||
|
s.Hint = x
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBIPv6Hint) String() string {
|
||||||
|
str := make([]string, len(s.Hint))
|
||||||
|
for i, e := range s.Hint {
|
||||||
|
if x := e.To4(); x != nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
str[i] = e.String()
|
||||||
|
}
|
||||||
|
return strings.Join(str, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBIPv6Hint) parse(b string) error {
|
||||||
|
str := strings.Split(b, ",")
|
||||||
|
dst := make([]net.IP, len(str))
|
||||||
|
for i, e := range str {
|
||||||
|
ip := net.ParseIP(e)
|
||||||
|
if ip == nil {
|
||||||
|
return errors.New("dns: svcbipv6hint: bad ip")
|
||||||
|
}
|
||||||
|
if ip.To4() != nil {
|
||||||
|
return errors.New("dns: svcbipv6hint: expected ipv6, got ipv4-mapped-ipv6")
|
||||||
|
}
|
||||||
|
dst[i] = ip
|
||||||
|
}
|
||||||
|
s.Hint = dst
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBIPv6Hint) copy() SVCBKeyValue {
|
||||||
|
hint := make([]net.IP, len(s.Hint))
|
||||||
|
for i, ip := range s.Hint {
|
||||||
|
hint[i] = copyIP(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SVCBIPv6Hint{
|
||||||
|
Hint: hint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCBDoHPath pair is used to indicate the URI template that the
|
||||||
|
// clients may use to construct a DNS over HTTPS URI.
|
||||||
|
//
|
||||||
|
// See RFC xxxx (https://datatracker.ietf.org/doc/html/draft-ietf-add-svcb-dns-02)
|
||||||
|
// and RFC yyyy (https://datatracker.ietf.org/doc/html/draft-ietf-add-ddr-06).
|
||||||
|
//
|
||||||
|
// A basic example of using the dohpath option together with the alpn
|
||||||
|
// option to indicate support for DNS over HTTPS on a certain path:
|
||||||
|
//
|
||||||
|
// s := new(dns.SVCB)
|
||||||
|
// s.Hdr = dns.RR_Header{Name: ".", Rrtype: dns.TypeSVCB, Class: dns.ClassINET}
|
||||||
|
// e := new(dns.SVCBAlpn)
|
||||||
|
// e.Alpn = []string{"h2", "h3"}
|
||||||
|
// p := new(dns.SVCBDoHPath)
|
||||||
|
// p.Template = "/dns-query{?dns}"
|
||||||
|
// s.Value = append(s.Value, e, p)
|
||||||
|
//
|
||||||
|
// The parsing currently doesn't validate that Template is a valid
|
||||||
|
// RFC 6570 URI template.
|
||||||
|
type SVCBDoHPath struct {
|
||||||
|
Template string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SVCBDoHPath) Key() SVCBKey { return SVCB_DOHPATH }
|
||||||
|
func (s *SVCBDoHPath) String() string { return svcbParamToStr([]byte(s.Template)) }
|
||||||
|
func (s *SVCBDoHPath) len() int { return len(s.Template) }
|
||||||
|
func (s *SVCBDoHPath) pack() ([]byte, error) { return []byte(s.Template), nil }
|
||||||
|
|
||||||
|
func (s *SVCBDoHPath) unpack(b []byte) error {
|
||||||
|
s.Template = string(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBDoHPath) parse(b string) error {
|
||||||
|
template, err := svcbParseParam(b)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dns: svcbdohpath: %w", err)
|
||||||
|
}
|
||||||
|
s.Template = string(template)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBDoHPath) copy() SVCBKeyValue {
|
||||||
|
return &SVCBDoHPath{
|
||||||
|
Template: s.Template,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVCBLocal pair is intended for experimental/private use. The key is recommended
|
||||||
|
// to be in the range [SVCB_PRIVATE_LOWER, SVCB_PRIVATE_UPPER].
|
||||||
|
// Basic use pattern for creating a keyNNNNN option:
|
||||||
|
//
|
||||||
|
// h := new(dns.HTTPS)
|
||||||
|
// h.Hdr = dns.RR_Header{Name: ".", Rrtype: dns.TypeHTTPS, Class: dns.ClassINET}
|
||||||
|
// e := new(dns.SVCBLocal)
|
||||||
|
// e.KeyCode = 65400
|
||||||
|
// e.Data = []byte("abc")
|
||||||
|
// h.Value = append(h.Value, e)
|
||||||
|
type SVCBLocal struct {
|
||||||
|
KeyCode SVCBKey // Never 65535 or any assigned keys.
|
||||||
|
Data []byte // All byte sequences are allowed.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBLocal) Key() SVCBKey { return s.KeyCode }
|
||||||
|
func (s *SVCBLocal) String() string { return svcbParamToStr(s.Data) }
|
||||||
|
func (s *SVCBLocal) pack() ([]byte, error) { return append([]byte(nil), s.Data...), nil }
|
||||||
|
func (s *SVCBLocal) len() int { return len(s.Data) }
|
||||||
|
|
||||||
|
func (s *SVCBLocal) unpack(b []byte) error {
|
||||||
|
s.Data = append([]byte(nil), b...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBLocal) parse(b string) error {
|
||||||
|
data, err := svcbParseParam(b)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dns: svcblocal: svcb private/experimental key %w", err)
|
||||||
|
}
|
||||||
|
s.Data = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SVCBLocal) copy() SVCBKeyValue {
|
||||||
|
return &SVCBLocal{s.KeyCode,
|
||||||
|
append([]byte(nil), s.Data...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *SVCB) String() string {
|
||||||
|
s := rr.Hdr.String() +
|
||||||
|
strconv.Itoa(int(rr.Priority)) + " " +
|
||||||
|
sprintName(rr.Target)
|
||||||
|
for _, e := range rr.Value {
|
||||||
|
s += " " + e.Key().String() + "=\"" + e.String() + "\""
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// areSVCBPairArraysEqual checks if SVCBKeyValue arrays are equal after sorting their
|
||||||
|
// copies. arrA and arrB have equal lengths, otherwise zduplicate.go wouldn't call this function.
|
||||||
|
func areSVCBPairArraysEqual(a []SVCBKeyValue, b []SVCBKeyValue) bool {
|
||||||
|
a = append([]SVCBKeyValue(nil), a...)
|
||||||
|
b = append([]SVCBKeyValue(nil), b...)
|
||||||
|
sort.Slice(a, func(i, j int) bool { return a[i].Key() < a[j].Key() })
|
||||||
|
sort.Slice(b, func(i, j int) bool { return b[i].Key() < b[j].Key() })
|
||||||
|
for i, e := range a {
|
||||||
|
if e.Key() != b[i].Key() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b1, err1 := e.pack()
|
||||||
|
b2, err2 := b[i].pack()
|
||||||
|
if err1 != nil || err2 != nil || !bytes.Equal(b1, b2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// svcbParamStr converts the value of an SVCB parameter into a DNS presentation-format string.
|
||||||
|
func svcbParamToStr(s []byte) string {
|
||||||
|
var str strings.Builder
|
||||||
|
str.Grow(4 * len(s))
|
||||||
|
for _, e := range s {
|
||||||
|
if ' ' <= e && e <= '~' {
|
||||||
|
switch e {
|
||||||
|
case '"', ';', ' ', '\\':
|
||||||
|
str.WriteByte('\\')
|
||||||
|
str.WriteByte(e)
|
||||||
|
default:
|
||||||
|
str.WriteByte(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str.WriteString(escapeByte(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// svcbParseParam parses a DNS presentation-format string into an SVCB parameter value.
|
||||||
|
func svcbParseParam(b string) ([]byte, error) {
|
||||||
|
data := make([]byte, 0, len(b))
|
||||||
|
for i := 0; i < len(b); {
|
||||||
|
if b[i] != '\\' {
|
||||||
|
data = append(data, b[i])
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i+1 == len(b) {
|
||||||
|
return nil, errors.New("escape unterminated")
|
||||||
|
}
|
||||||
|
if isDigit(b[i+1]) {
|
||||||
|
if i+3 < len(b) && isDigit(b[i+2]) && isDigit(b[i+3]) {
|
||||||
|
a, err := strconv.ParseUint(b[i+1:i+4], 10, 8)
|
||||||
|
if err == nil {
|
||||||
|
i += 4
|
||||||
|
data = append(data, byte(a))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("bad escaped octet")
|
||||||
|
} else {
|
||||||
|
data = append(data, b[i+1])
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This tests everything valid about SVCB but parsing.
|
||||||
|
// Parsing tests belong to parse_test.go.
|
||||||
|
func TestSVCB(t *testing.T) {
|
||||||
|
svcbs := []struct {
|
||||||
|
key string
|
||||||
|
data string
|
||||||
|
}{
|
||||||
|
{`mandatory`, `alpn,key65000`},
|
||||||
|
{`alpn`, `h2,h2c`},
|
||||||
|
{`port`, `499`},
|
||||||
|
{`ipv4hint`, `3.4.3.2,1.1.1.1`},
|
||||||
|
{`no-default-alpn`, ``},
|
||||||
|
{`ipv6hint`, `1::4:4:4:4,1::3:3:3:3`},
|
||||||
|
{`ech`, `YUdWc2JHOD0=`},
|
||||||
|
{`dohpath`, `/dns-query{?dns}`},
|
||||||
|
{`key65000`, `4\ 3`},
|
||||||
|
{`key65001`, `\"\ `},
|
||||||
|
{`key65002`, ``},
|
||||||
|
{`key65003`, `=\"\"`},
|
||||||
|
{`key65004`, `\254\ \ \030\000`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range svcbs {
|
||||||
|
keyCode := svcbStringToKey(o.key)
|
||||||
|
kv := makeSVCBKeyValue(keyCode)
|
||||||
|
if kv == nil {
|
||||||
|
t.Error("failed to parse svc key: ", o.key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if kv.Key() != keyCode {
|
||||||
|
t.Error("key constant is not in sync: ", keyCode)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := kv.parse(o.data)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to parse svc pair: ", o.key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b, err := kv.pack()
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to pack value of svc pair: ", o.key, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(b) != int(kv.len()) {
|
||||||
|
t.Errorf("expected packed svc value %s to be of length %d but got %d", o.key, int(kv.len()), len(b))
|
||||||
|
}
|
||||||
|
err = kv.unpack(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to unpack value of svc pair: ", o.key, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if str := kv.String(); str != o.data {
|
||||||
|
t.Errorf("`%s' should be equal to\n`%s', but is `%s'", o.key, o.data, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeBadSVCB(t *testing.T) {
|
||||||
|
svcbs := []struct {
|
||||||
|
key SVCBKey
|
||||||
|
data []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
key: SVCB_ALPN,
|
||||||
|
data: []byte{3, 0, 0}, // There aren't three octets after 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SVCB_NO_DEFAULT_ALPN,
|
||||||
|
data: []byte{0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SVCB_PORT,
|
||||||
|
data: []byte{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SVCB_IPV4HINT,
|
||||||
|
data: []byte{0, 0, 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SVCB_IPV6HINT,
|
||||||
|
data: []byte{0, 0, 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, o := range svcbs {
|
||||||
|
err := makeSVCBKeyValue(SVCBKey(o.key)).unpack(o.data)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("accepted invalid svc value with key ", SVCBKey(o.key).String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPresentationSVCBAlpn(t *testing.T) {
|
||||||
|
tests := map[string]string{
|
||||||
|
"h2": "h2",
|
||||||
|
"http": "http",
|
||||||
|
"\xfa": `\250`,
|
||||||
|
"some\"other,chars": `some\"other\\\044chars`,
|
||||||
|
}
|
||||||
|
for input, want := range tests {
|
||||||
|
e := new(SVCBAlpn)
|
||||||
|
e.Alpn = []string{input}
|
||||||
|
if e.String() != want {
|
||||||
|
t.Errorf("improper conversion with String(), wanted %v got %v", want, e.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSVCBAlpn(t *testing.T) {
|
||||||
|
tests := map[string][]string{
|
||||||
|
`. 1 IN SVCB 10 one.test. alpn=h2`: {"h2"},
|
||||||
|
`. 2 IN SVCB 20 two.test. alpn=h2,h3-19`: {"h2", "h3-19"},
|
||||||
|
`. 3 IN SVCB 30 three.test. alpn="f\\\\oo\\,bar,h2"`: {`f\oo,bar`, "h2"},
|
||||||
|
`. 4 IN SVCB 40 four.test. alpn="part1,part2,part3\\,part4\\\\"`: {"part1", "part2", `part3,part4\`},
|
||||||
|
`. 5 IN SVCB 50 five.test. alpn=part1\,\p\a\r\t2\044part3\092,part4\092\\`: {"part1", "part2", `part3,part4\`},
|
||||||
|
}
|
||||||
|
for s, v := range tests {
|
||||||
|
rr, err := NewRR(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to parse RR: ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
alpn := rr.(*SVCB).Value[0].(*SVCBAlpn).Alpn
|
||||||
|
if len(v) != len(alpn) {
|
||||||
|
t.Fatalf("parsing alpn failed, wanted %v got %v", v, alpn)
|
||||||
|
}
|
||||||
|
for i := range v {
|
||||||
|
if v[i] != alpn[i] {
|
||||||
|
t.Fatalf("parsing alpn failed, wanted %v got %v", v, alpn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareSVCB(t *testing.T) {
|
||||||
|
val1 := []SVCBKeyValue{
|
||||||
|
&SVCBPort{
|
||||||
|
Port: 117,
|
||||||
|
},
|
||||||
|
&SVCBAlpn{
|
||||||
|
Alpn: []string{"h2", "h3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
val2 := []SVCBKeyValue{
|
||||||
|
&SVCBAlpn{
|
||||||
|
Alpn: []string{"h2", "h3"},
|
||||||
|
},
|
||||||
|
&SVCBPort{
|
||||||
|
Port: 117,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !areSVCBPairArraysEqual(val1, val2) {
|
||||||
|
t.Error("svcb pairs were compared without sorting")
|
||||||
|
}
|
||||||
|
if val1[0].Key() != SVCB_PORT || val2[0].Key() != SVCB_ALPN {
|
||||||
|
t.Error("original svcb pairs were reordered during comparison")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
//go:build tools
|
||||||
|
// +build tools
|
||||||
|
|
||||||
|
// We include our tool dependencies for `go generate` here to ensure they're
|
||||||
|
// properly tracked by the go tool. See the Go Wiki for the rationale behind this:
|
||||||
|
// https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module.
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import _ "golang.org/x/tools/go/packages"
|
213
tsig.go
213
tsig.go
|
@ -2,7 +2,6 @@ package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/md5"
|
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
|
@ -16,12 +15,83 @@ import (
|
||||||
|
|
||||||
// HMAC hashing codes. These are transmitted as domain names.
|
// HMAC hashing codes. These are transmitted as domain names.
|
||||||
const (
|
const (
|
||||||
HmacMD5 = "hmac-md5.sig-alg.reg.int."
|
|
||||||
HmacSHA1 = "hmac-sha1."
|
HmacSHA1 = "hmac-sha1."
|
||||||
|
HmacSHA224 = "hmac-sha224."
|
||||||
HmacSHA256 = "hmac-sha256."
|
HmacSHA256 = "hmac-sha256."
|
||||||
|
HmacSHA384 = "hmac-sha384."
|
||||||
HmacSHA512 = "hmac-sha512."
|
HmacSHA512 = "hmac-sha512."
|
||||||
|
|
||||||
|
HmacMD5 = "hmac-md5.sig-alg.reg.int." // Deprecated: HmacMD5 is no longer supported.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TsigProvider provides the API to plug-in a custom TSIG implementation.
|
||||||
|
type TsigProvider interface {
|
||||||
|
// Generate is passed the DNS message to be signed and the partial TSIG RR. It returns the signature and nil, otherwise an error.
|
||||||
|
Generate(msg []byte, t *TSIG) ([]byte, error)
|
||||||
|
// Verify is passed the DNS message to be verified and the TSIG RR. If the signature is valid it will return nil, otherwise an error.
|
||||||
|
Verify(msg []byte, t *TSIG) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type tsigHMACProvider string
|
||||||
|
|
||||||
|
func (key tsigHMACProvider) Generate(msg []byte, t *TSIG) ([]byte, error) {
|
||||||
|
// If we barf here, the caller is to blame
|
||||||
|
rawsecret, err := fromBase64([]byte(key))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var h hash.Hash
|
||||||
|
switch CanonicalName(t.Algorithm) {
|
||||||
|
case HmacSHA1:
|
||||||
|
h = hmac.New(sha1.New, rawsecret)
|
||||||
|
case HmacSHA224:
|
||||||
|
h = hmac.New(sha256.New224, rawsecret)
|
||||||
|
case HmacSHA256:
|
||||||
|
h = hmac.New(sha256.New, rawsecret)
|
||||||
|
case HmacSHA384:
|
||||||
|
h = hmac.New(sha512.New384, rawsecret)
|
||||||
|
case HmacSHA512:
|
||||||
|
h = hmac.New(sha512.New, rawsecret)
|
||||||
|
default:
|
||||||
|
return nil, ErrKeyAlg
|
||||||
|
}
|
||||||
|
h.Write(msg)
|
||||||
|
return h.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (key tsigHMACProvider) Verify(msg []byte, t *TSIG) error {
|
||||||
|
b, err := key.Generate(msg, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mac, err := hex.DecodeString(t.MAC)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hmac.Equal(b, mac) {
|
||||||
|
return ErrSig
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type tsigSecretProvider map[string]string
|
||||||
|
|
||||||
|
func (ts tsigSecretProvider) Generate(msg []byte, t *TSIG) ([]byte, error) {
|
||||||
|
key, ok := ts[t.Hdr.Name]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrSecret
|
||||||
|
}
|
||||||
|
return tsigHMACProvider(key).Generate(msg, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts tsigSecretProvider) Verify(msg []byte, t *TSIG) error {
|
||||||
|
key, ok := ts[t.Hdr.Name]
|
||||||
|
if !ok {
|
||||||
|
return ErrSecret
|
||||||
|
}
|
||||||
|
return tsigHMACProvider(key).Verify(msg, t)
|
||||||
|
}
|
||||||
|
|
||||||
// TSIG is the RR the holds the transaction signature of a message.
|
// TSIG is the RR the holds the transaction signature of a message.
|
||||||
// See RFC 2845 and RFC 4635.
|
// See RFC 2845 and RFC 4635.
|
||||||
type TSIG struct {
|
type TSIG struct {
|
||||||
|
@ -40,7 +110,7 @@ type TSIG struct {
|
||||||
// TSIG has no official presentation format, but this will suffice.
|
// TSIG has no official presentation format, but this will suffice.
|
||||||
|
|
||||||
func (rr *TSIG) String() string {
|
func (rr *TSIG) String() string {
|
||||||
s := "\n;; TSIG PSEUDOSECTION:\n"
|
s := "\n;; TSIG PSEUDOSECTION:\n; " // add another semi-colon to signify TSIG does not have a presentation format
|
||||||
s += rr.Hdr.String() +
|
s += rr.Hdr.String() +
|
||||||
" " + rr.Algorithm +
|
" " + rr.Algorithm +
|
||||||
" " + tsigTimeToString(rr.TimeSigned) +
|
" " + tsigTimeToString(rr.TimeSigned) +
|
||||||
|
@ -54,8 +124,8 @@ func (rr *TSIG) String() string {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rr *TSIG) parse(c *zlexer, origin, file string) *ParseError {
|
func (*TSIG) parse(c *zlexer, origin string) *ParseError {
|
||||||
panic("dns: internal error: parse should never be called on TSIG")
|
return &ParseError{err: "TSIG records do not have a presentation format"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The following values must be put in wireformat, so that the MAC can be calculated.
|
// The following values must be put in wireformat, so that the MAC can be calculated.
|
||||||
|
@ -88,22 +158,20 @@ type timerWireFmt struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TsigGenerate fills out the TSIG record attached to the message.
|
// TsigGenerate fills out the TSIG record attached to the message.
|
||||||
// The message should contain
|
// The message should contain a "stub" TSIG RR with the algorithm, key name
|
||||||
// a "stub" TSIG RR with the algorithm, key name (owner name of the RR),
|
// (owner name of the RR), time fudge (defaults to 300 seconds) and the current
|
||||||
// time fudge (defaults to 300 seconds) and the current time
|
// time The TSIG MAC is saved in that Tsig RR. When TsigGenerate is called for
|
||||||
// The TSIG MAC is saved in that Tsig RR.
|
// the first time requestMAC should be set to the empty string and timersOnly to
|
||||||
// When TsigGenerate is called for the first time requestMAC is set to the empty string and
|
// false.
|
||||||
// timersOnly is false.
|
|
||||||
// If something goes wrong an error is returned, otherwise it is nil.
|
|
||||||
func TsigGenerate(m *Msg, secret, requestMAC string, timersOnly bool) ([]byte, string, error) {
|
func TsigGenerate(m *Msg, secret, requestMAC string, timersOnly bool) ([]byte, string, error) {
|
||||||
|
return TsigGenerateWithProvider(m, tsigHMACProvider(secret), requestMAC, timersOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TsigGenerateWithProvider is similar to TsigGenerate, but allows for a custom TsigProvider.
|
||||||
|
func TsigGenerateWithProvider(m *Msg, provider TsigProvider, requestMAC string, timersOnly bool) ([]byte, string, error) {
|
||||||
if m.IsTsig() == nil {
|
if m.IsTsig() == nil {
|
||||||
panic("dns: TSIG not last RR in additional")
|
panic("dns: TSIG not last RR in additional")
|
||||||
}
|
}
|
||||||
// If we barf here, the caller is to blame
|
|
||||||
rawsecret, err := fromBase64([]byte(secret))
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
rr := m.Extra[len(m.Extra)-1].(*TSIG)
|
rr := m.Extra[len(m.Extra)-1].(*TSIG)
|
||||||
m.Extra = m.Extra[0 : len(m.Extra)-1] // kill the TSIG from the msg
|
m.Extra = m.Extra[0 : len(m.Extra)-1] // kill the TSIG from the msg
|
||||||
|
@ -111,31 +179,29 @@ func TsigGenerate(m *Msg, secret, requestMAC string, timersOnly bool) ([]byte, s
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
buf := tsigBuffer(mbuf, rr, requestMAC, timersOnly)
|
|
||||||
|
buf, err := tsigBuffer(mbuf, rr, requestMAC, timersOnly)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
t := new(TSIG)
|
t := new(TSIG)
|
||||||
var h hash.Hash
|
// Copy all TSIG fields except MAC, its size, and time signed which are filled when signing.
|
||||||
switch strings.ToLower(rr.Algorithm) {
|
*t = *rr
|
||||||
case HmacMD5:
|
t.TimeSigned = 0
|
||||||
h = hmac.New(md5.New, rawsecret)
|
t.MAC = ""
|
||||||
case HmacSHA1:
|
t.MACSize = 0
|
||||||
h = hmac.New(sha1.New, rawsecret)
|
|
||||||
case HmacSHA256:
|
|
||||||
h = hmac.New(sha256.New, rawsecret)
|
|
||||||
case HmacSHA512:
|
|
||||||
h = hmac.New(sha512.New, rawsecret)
|
|
||||||
default:
|
|
||||||
return nil, "", ErrKeyAlg
|
|
||||||
}
|
|
||||||
h.Write(buf)
|
|
||||||
t.MAC = hex.EncodeToString(h.Sum(nil))
|
|
||||||
t.MACSize = uint16(len(t.MAC) / 2) // Size is half!
|
|
||||||
|
|
||||||
t.Hdr = RR_Header{Name: rr.Hdr.Name, Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0}
|
// Sign unless there is a key or MAC validation error (RFC 8945 5.3.2)
|
||||||
t.Fudge = rr.Fudge
|
if rr.Error != RcodeBadKey && rr.Error != RcodeBadSig {
|
||||||
t.TimeSigned = rr.TimeSigned
|
mac, err := provider.Generate(buf, rr)
|
||||||
t.Algorithm = rr.Algorithm
|
if err != nil {
|
||||||
t.OrigId = m.Id
|
return nil, "", err
|
||||||
|
}
|
||||||
|
t.TimeSigned = rr.TimeSigned
|
||||||
|
t.MAC = hex.EncodeToString(mac)
|
||||||
|
t.MACSize = uint16(len(t.MAC) / 2) // Size is half!
|
||||||
|
}
|
||||||
|
|
||||||
tbuf := make([]byte, Len(t))
|
tbuf := make([]byte, Len(t))
|
||||||
off, err := PackRR(t, tbuf, 0, nil, false)
|
off, err := PackRR(t, tbuf, 0, nil, false)
|
||||||
|
@ -149,30 +215,39 @@ func TsigGenerate(m *Msg, secret, requestMAC string, timersOnly bool) ([]byte, s
|
||||||
return mbuf, t.MAC, nil
|
return mbuf, t.MAC, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TsigVerify verifies the TSIG on a message.
|
// TsigVerify verifies the TSIG on a message. If the signature does not
|
||||||
// If the signature does not validate err contains the
|
// validate the returned error contains the cause. If the signature is OK, the
|
||||||
// error, otherwise it is nil.
|
// error is nil.
|
||||||
func TsigVerify(msg []byte, secret, requestMAC string, timersOnly bool) error {
|
func TsigVerify(msg []byte, secret, requestMAC string, timersOnly bool) error {
|
||||||
rawsecret, err := fromBase64([]byte(secret))
|
return tsigVerify(msg, tsigHMACProvider(secret), requestMAC, timersOnly, uint64(time.Now().Unix()))
|
||||||
if err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
// TsigVerifyWithProvider is similar to TsigVerify, but allows for a custom TsigProvider.
|
||||||
|
func TsigVerifyWithProvider(msg []byte, provider TsigProvider, requestMAC string, timersOnly bool) error {
|
||||||
|
return tsigVerify(msg, provider, requestMAC, timersOnly, uint64(time.Now().Unix()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// actual implementation of TsigVerify, taking the current time ('now') as a parameter for the convenience of tests.
|
||||||
|
func tsigVerify(msg []byte, provider TsigProvider, requestMAC string, timersOnly bool, now uint64) error {
|
||||||
// Strip the TSIG from the incoming msg
|
// Strip the TSIG from the incoming msg
|
||||||
stripped, tsig, err := stripTsig(msg)
|
stripped, tsig, err := stripTsig(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
msgMAC, err := hex.DecodeString(tsig.MAC)
|
buf, err := tsigBuffer(stripped, tsig, requestMAC, timersOnly)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := tsigBuffer(stripped, tsig, requestMAC, timersOnly)
|
if err := provider.Verify(buf, tsig); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Fudge factor works both ways. A message can arrive before it was signed because
|
// Fudge factor works both ways. A message can arrive before it was signed because
|
||||||
// of clock skew.
|
// of clock skew.
|
||||||
now := uint64(time.Now().Unix())
|
// We check this after verifying the signature, following draft-ietf-dnsop-rfc2845bis
|
||||||
|
// instead of RFC2845, in order to prevent a security vulnerability as reported in CVE-2017-3142/3143.
|
||||||
ti := now - tsig.TimeSigned
|
ti := now - tsig.TimeSigned
|
||||||
if now < tsig.TimeSigned {
|
if now < tsig.TimeSigned {
|
||||||
ti = tsig.TimeSigned - now
|
ti = tsig.TimeSigned - now
|
||||||
|
@ -181,28 +256,11 @@ func TsigVerify(msg []byte, secret, requestMAC string, timersOnly bool) error {
|
||||||
return ErrTime
|
return ErrTime
|
||||||
}
|
}
|
||||||
|
|
||||||
var h hash.Hash
|
|
||||||
switch strings.ToLower(tsig.Algorithm) {
|
|
||||||
case HmacMD5:
|
|
||||||
h = hmac.New(md5.New, rawsecret)
|
|
||||||
case HmacSHA1:
|
|
||||||
h = hmac.New(sha1.New, rawsecret)
|
|
||||||
case HmacSHA256:
|
|
||||||
h = hmac.New(sha256.New, rawsecret)
|
|
||||||
case HmacSHA512:
|
|
||||||
h = hmac.New(sha512.New, rawsecret)
|
|
||||||
default:
|
|
||||||
return ErrKeyAlg
|
|
||||||
}
|
|
||||||
h.Write(buf)
|
|
||||||
if !hmac.Equal(h.Sum(nil), msgMAC) {
|
|
||||||
return ErrSig
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a wiredata buffer for the MAC calculation.
|
// Create a wiredata buffer for the MAC calculation.
|
||||||
func tsigBuffer(msgbuf []byte, rr *TSIG, requestMAC string, timersOnly bool) []byte {
|
func tsigBuffer(msgbuf []byte, rr *TSIG, requestMAC string, timersOnly bool) ([]byte, error) {
|
||||||
var buf []byte
|
var buf []byte
|
||||||
if rr.TimeSigned == 0 {
|
if rr.TimeSigned == 0 {
|
||||||
rr.TimeSigned = uint64(time.Now().Unix())
|
rr.TimeSigned = uint64(time.Now().Unix())
|
||||||
|
@ -219,7 +277,10 @@ func tsigBuffer(msgbuf []byte, rr *TSIG, requestMAC string, timersOnly bool) []b
|
||||||
m.MACSize = uint16(len(requestMAC) / 2)
|
m.MACSize = uint16(len(requestMAC) / 2)
|
||||||
m.MAC = requestMAC
|
m.MAC = requestMAC
|
||||||
buf = make([]byte, len(requestMAC)) // long enough
|
buf = make([]byte, len(requestMAC)) // long enough
|
||||||
n, _ := packMacWire(m, buf)
|
n, err := packMacWire(m, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
buf = buf[:n]
|
buf = buf[:n]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,20 +289,26 @@ func tsigBuffer(msgbuf []byte, rr *TSIG, requestMAC string, timersOnly bool) []b
|
||||||
tsig := new(timerWireFmt)
|
tsig := new(timerWireFmt)
|
||||||
tsig.TimeSigned = rr.TimeSigned
|
tsig.TimeSigned = rr.TimeSigned
|
||||||
tsig.Fudge = rr.Fudge
|
tsig.Fudge = rr.Fudge
|
||||||
n, _ := packTimerWire(tsig, tsigvar)
|
n, err := packTimerWire(tsig, tsigvar)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
tsigvar = tsigvar[:n]
|
tsigvar = tsigvar[:n]
|
||||||
} else {
|
} else {
|
||||||
tsig := new(tsigWireFmt)
|
tsig := new(tsigWireFmt)
|
||||||
tsig.Name = strings.ToLower(rr.Hdr.Name)
|
tsig.Name = CanonicalName(rr.Hdr.Name)
|
||||||
tsig.Class = ClassANY
|
tsig.Class = ClassANY
|
||||||
tsig.Ttl = rr.Hdr.Ttl
|
tsig.Ttl = rr.Hdr.Ttl
|
||||||
tsig.Algorithm = strings.ToLower(rr.Algorithm)
|
tsig.Algorithm = CanonicalName(rr.Algorithm)
|
||||||
tsig.TimeSigned = rr.TimeSigned
|
tsig.TimeSigned = rr.TimeSigned
|
||||||
tsig.Fudge = rr.Fudge
|
tsig.Fudge = rr.Fudge
|
||||||
tsig.Error = rr.Error
|
tsig.Error = rr.Error
|
||||||
tsig.OtherLen = rr.OtherLen
|
tsig.OtherLen = rr.OtherLen
|
||||||
tsig.OtherData = rr.OtherData
|
tsig.OtherData = rr.OtherData
|
||||||
n, _ := packTsigWire(tsig, tsigvar)
|
n, err := packTsigWire(tsig, tsigvar)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
tsigvar = tsigvar[:n]
|
tsigvar = tsigvar[:n]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,7 +318,7 @@ func tsigBuffer(msgbuf []byte, rr *TSIG, requestMAC string, timersOnly bool) []b
|
||||||
} else {
|
} else {
|
||||||
buf = append(msgbuf, tsigvar...)
|
buf = append(msgbuf, tsigvar...)
|
||||||
}
|
}
|
||||||
return buf
|
return buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip the TSIG from the raw message.
|
// Strip the TSIG from the raw message.
|
||||||
|
|
363
tsig_test.go
363
tsig_test.go
|
@ -2,6 +2,10 @@ package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -14,7 +18,7 @@ func newTsig(algo string) *Msg {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTsig(t *testing.T) {
|
func TestTsig(t *testing.T) {
|
||||||
m := newTsig(HmacMD5)
|
m := newTsig(HmacSHA256)
|
||||||
buf, _, err := TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
|
buf, _, err := TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -26,7 +30,7 @@ func TestTsig(t *testing.T) {
|
||||||
|
|
||||||
// TSIG accounts for ID substitution. This means if the message ID is
|
// TSIG accounts for ID substitution. This means if the message ID is
|
||||||
// changed by a forwarder, we should still be able to verify the TSIG.
|
// changed by a forwarder, we should still be able to verify the TSIG.
|
||||||
m = newTsig(HmacMD5)
|
m = newTsig(HmacSHA256)
|
||||||
buf, _, err = TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
|
buf, _, err = TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -40,7 +44,7 @@ func TestTsig(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTsigCase(t *testing.T) {
|
func TestTsigCase(t *testing.T) {
|
||||||
m := newTsig("HmAc-mD5.sig-ALg.rEg.int.") // HmacMD5
|
m := newTsig(strings.ToUpper(HmacSHA256))
|
||||||
buf, _, err := TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
|
buf, _, err := TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -50,3 +54,356 @@ func TestTsigCase(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTsigErrorResponse(t *testing.T) {
|
||||||
|
for _, rcode := range []uint16{RcodeBadSig, RcodeBadKey} {
|
||||||
|
m := newTsig(strings.ToUpper(HmacSHA256))
|
||||||
|
m.IsTsig().Error = rcode
|
||||||
|
buf, _, err := TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.Unpack(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mTsig := m.IsTsig()
|
||||||
|
if mTsig.MAC != "" {
|
||||||
|
t.Error("Expected empty MAC")
|
||||||
|
}
|
||||||
|
if mTsig.MACSize != 0 {
|
||||||
|
t.Error("Expected 0 MACSize")
|
||||||
|
}
|
||||||
|
if mTsig.TimeSigned != 0 {
|
||||||
|
t.Errorf("Expected TimeSigned to be 0, got %v", mTsig.TimeSigned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTsigBadTimeResponse(t *testing.T) {
|
||||||
|
clientTime := uint64(time.Now().Unix()) - 3600
|
||||||
|
m := newTsig(strings.ToUpper(HmacSHA256))
|
||||||
|
m.IsTsig().Error = RcodeBadTime
|
||||||
|
m.IsTsig().TimeSigned = clientTime
|
||||||
|
|
||||||
|
buf, _, err := TsigGenerate(m, "pRZgBrBvI4NAHZYhxmhs/Q==", "", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.Unpack(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mTsig := m.IsTsig()
|
||||||
|
if mTsig.MAC == "" {
|
||||||
|
t.Error("Expected non-empty MAC")
|
||||||
|
}
|
||||||
|
if int(mTsig.MACSize) != len(mTsig.MAC)/2 {
|
||||||
|
t.Errorf("Expected MACSize %v, got %v", len(mTsig.MAC)/2, mTsig.MACSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mTsig.TimeSigned != clientTime {
|
||||||
|
t.Errorf("Expected TimeSigned %v to be retained, got %v", clientTime, mTsig.TimeSigned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// A template wire-format DNS message (in hex form) containing a TSIG RR.
|
||||||
|
// Its time signed field will be filled by tests.
|
||||||
|
wireMsg = "c60028000001000000010001076578616d706c6503636f6d00000600010161c00c0001000100000e100004c0000201077465" +
|
||||||
|
"73746b65790000fa00ff00000000003d0b686d61632d73686132353600" +
|
||||||
|
"%012x" + // placeholder for the "time signed" field
|
||||||
|
"012c00208cf23e0081d915478a182edcea7ff48ad102948e6c7ef8e887536957d1fa5616c60000000000"
|
||||||
|
// A secret (in base64 format) with which the TSIG in wireMsg will be validated
|
||||||
|
testSecret = "NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk="
|
||||||
|
// the 'time signed' field value that would make the TSIG RR valid with testSecret
|
||||||
|
timeSigned uint64 = 1594855491
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTsigErrors(t *testing.T) {
|
||||||
|
// Helper shortcut to build wire-format test message.
|
||||||
|
// TsigVerify can modify the slice, so we need to create a new one for each test case below.
|
||||||
|
buildMsgData := func(tm uint64) []byte {
|
||||||
|
msgData, err := hex.DecodeString(fmt.Sprintf(wireMsg, tm))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return msgData
|
||||||
|
}
|
||||||
|
|
||||||
|
// the signature is valid but 'time signed' is too far from the "current time".
|
||||||
|
if err := tsigVerify(buildMsgData(timeSigned), tsigHMACProvider(testSecret), "", false, timeSigned+301); err != ErrTime {
|
||||||
|
t.Fatalf("expected an error '%v' but got '%v'", ErrTime, err)
|
||||||
|
}
|
||||||
|
if err := tsigVerify(buildMsgData(timeSigned), tsigHMACProvider(testSecret), "", false, timeSigned-301); err != ErrTime {
|
||||||
|
t.Fatalf("expected an error '%v' but got '%v'", ErrTime, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the signature is invalid and 'time signed' is too far.
|
||||||
|
// the signature should be checked first, so we should see ErrSig.
|
||||||
|
if err := tsigVerify(buildMsgData(timeSigned+301), tsigHMACProvider(testSecret), "", false, timeSigned); err != ErrSig {
|
||||||
|
t.Fatalf("expected an error '%v' but got '%v'", ErrSig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tweak the algorithm name in the wire data, resulting in the "unknown algorithm" error.
|
||||||
|
msgData := buildMsgData(timeSigned)
|
||||||
|
copy(msgData[67:], "bogus")
|
||||||
|
if err := tsigVerify(msgData, tsigHMACProvider(testSecret), "", false, timeSigned); err != ErrKeyAlg {
|
||||||
|
t.Fatalf("expected an error '%v' but got '%v'", ErrKeyAlg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// call TsigVerify with a message that doesn't contain a TSIG
|
||||||
|
msgData, tsig, err := stripTsig(buildMsgData(timeSigned))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tsigVerify(msgData, tsigHMACProvider(testSecret), "", false, timeSigned); err != ErrNoSig {
|
||||||
|
t.Fatalf("expected an error '%v' but got '%v'", ErrNoSig, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace the test TSIG with a bogus one with large "other data", which would cause overflow in TsigVerify.
|
||||||
|
// The overflow should be caught without disruption.
|
||||||
|
tsig.OtherData = strings.Repeat("00", 4096)
|
||||||
|
tsig.OtherLen = uint16(len(tsig.OtherData) / 2)
|
||||||
|
msg := new(Msg)
|
||||||
|
if err = msg.Unpack(msgData); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
msg.Extra = append(msg.Extra, tsig)
|
||||||
|
if msgData, err = msg.Pack(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = tsigVerify(msgData, tsigHMACProvider(testSecret), "", false, timeSigned)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "overflow") {
|
||||||
|
t.Errorf("expected error to contain %q, but got %v", "overflow", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test exercises some more corner cases for TsigGenerate.
|
||||||
|
func TestTsigGenerate(t *testing.T) {
|
||||||
|
// This is a template TSIG to be used for signing.
|
||||||
|
tsig := TSIG{
|
||||||
|
Hdr: RR_Header{Name: "testkey.", Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0},
|
||||||
|
Algorithm: HmacSHA256,
|
||||||
|
TimeSigned: timeSigned,
|
||||||
|
Fudge: 300,
|
||||||
|
OrigId: 42,
|
||||||
|
Error: RcodeBadTime, // use a non-0 value to make sure it's indeed used
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
desc string // test description
|
||||||
|
requestMAC string // request MAC to be passed to TsigGenerate (arbitrary choice)
|
||||||
|
otherData string // other data specified in the TSIG (arbitrary choice)
|
||||||
|
expectedMAC string // pre-computed expected (correct) MAC in hex form
|
||||||
|
}{
|
||||||
|
{"with request MAC", "3684c225", "",
|
||||||
|
"c110e3f62694755c10761dc8717462431ee34340b7c9d1eee09449150757c5b1"},
|
||||||
|
{"no request MAC", "", "",
|
||||||
|
"385449a425c6d52b9bf2c65c0726eefa0ad8084cdaf488f24547e686605b9610"},
|
||||||
|
{"with other data", "3684c225", "666f6f",
|
||||||
|
"15b91571ca80b3b410a77e2b44f8cc4f35ace22b26020138439dd94803e23b5d"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
// Build TSIG for signing from the template
|
||||||
|
testTSIG := tsig
|
||||||
|
testTSIG.OtherLen = uint16(len(tc.otherData) / 2)
|
||||||
|
testTSIG.OtherData = tc.otherData
|
||||||
|
req := &Msg{
|
||||||
|
MsgHdr: MsgHdr{Opcode: OpcodeUpdate},
|
||||||
|
Question: []Question{{Name: "example.com.", Qtype: TypeSOA, Qclass: ClassINET}},
|
||||||
|
Extra: []RR{&testTSIG},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call generate, and check the returned MAC against the expected value
|
||||||
|
msgData, mac, err := TsigGenerate(req, testSecret, tc.requestMAC, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if mac != tc.expectedMAC {
|
||||||
|
t.Fatalf("MAC doesn't match: expected '%s', but got '%s'", tc.expectedMAC, mac)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the TSIG to be sent out, confirm the MAC in it
|
||||||
|
_, outTSIG, err := stripTsig(msgData)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if outTSIG.MAC != tc.expectedMAC {
|
||||||
|
t.Fatalf("MAC doesn't match: expected '%s', but got '%s'", tc.expectedMAC, outTSIG.MAC)
|
||||||
|
}
|
||||||
|
// Confirm other fields of MAC.
|
||||||
|
// RDLENGTH should be valid as stripTsig succeeded, so we exclude it from comparison
|
||||||
|
outTSIG.MACSize = 0
|
||||||
|
outTSIG.MAC = ""
|
||||||
|
testTSIG.Hdr.Rdlength = outTSIG.Hdr.Rdlength
|
||||||
|
if *outTSIG != testTSIG {
|
||||||
|
t.Fatalf("TSIG RR doesn't match: expected '%v', but got '%v'", *outTSIG, testTSIG)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTSIGHMAC224And384(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
algorithm string // TSIG algorithm, also used as test description
|
||||||
|
secret string // (arbitrarily chosen) secret suitable for the algorithm in base64 format
|
||||||
|
expectedMAC string // pre-computed expected (correct) MAC in hex form
|
||||||
|
}{
|
||||||
|
{HmacSHA224, "hVEkQuAqnTmBuRrT9KF1Udr91gOMGWPw9LaTtw==",
|
||||||
|
"d6daf9ea189e48bc38f9aed63d6cc4140cdfa38a7a333ee2eefdbd31",
|
||||||
|
},
|
||||||
|
{HmacSHA384, "Qjer2TL2lAdpq9w6Gjs98/ClCQx/L3vtgVHCmrZ8l/oKEPjqUUMFO18gMCRwd5H4",
|
||||||
|
"89a48936d29187870c325cbdba5ad71609bd038d0459d6010c844d659c570e881d3650e4fe7310be53ebe5178d0d1001",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.algorithm, func(t *testing.T) {
|
||||||
|
// Build a DNS message with TSIG for the test scenario
|
||||||
|
tsig := TSIG{
|
||||||
|
Hdr: RR_Header{Name: "testkey.", Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0},
|
||||||
|
Algorithm: tc.algorithm,
|
||||||
|
TimeSigned: timeSigned,
|
||||||
|
Fudge: 300,
|
||||||
|
OrigId: 42,
|
||||||
|
}
|
||||||
|
req := &Msg{
|
||||||
|
MsgHdr: MsgHdr{Opcode: OpcodeUpdate},
|
||||||
|
Question: []Question{{Name: "example.com.", Qtype: TypeSOA, Qclass: ClassINET}},
|
||||||
|
Extra: []RR{&tsig},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm both Generate and Verify recognize the algorithm and handle it correctly
|
||||||
|
msgData, mac, err := TsigGenerate(req, tc.secret, "", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if mac != tc.expectedMAC {
|
||||||
|
t.Fatalf("MAC doesn't match: expected '%s' but got '%s'", tc.expectedMAC, mac)
|
||||||
|
}
|
||||||
|
if err = tsigVerify(msgData, tsigHMACProvider(tc.secret), "", false, timeSigned); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testGoodKeyName = "goodkey."
|
||||||
|
|
||||||
|
var (
|
||||||
|
errBadKey = errors.New("this is an intentional error")
|
||||||
|
testGoodMAC = []byte{0, 1, 2, 3}
|
||||||
|
)
|
||||||
|
|
||||||
|
// testProvider always generates the same MAC and only accepts the one signature
|
||||||
|
type testProvider struct {
|
||||||
|
GenerateAllKeys bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *testProvider) Generate(_ []byte, t *TSIG) ([]byte, error) {
|
||||||
|
if t.Hdr.Name == testGoodKeyName || provider.GenerateAllKeys {
|
||||||
|
return testGoodMAC, nil
|
||||||
|
}
|
||||||
|
return nil, errBadKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*testProvider) Verify(_ []byte, t *TSIG) error {
|
||||||
|
if t.Hdr.Name == testGoodKeyName {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errBadKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTsigGenerateProvider(t *testing.T) {
|
||||||
|
tables := []struct {
|
||||||
|
keyname string
|
||||||
|
mac []byte
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
testGoodKeyName,
|
||||||
|
testGoodMAC,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"badkey.",
|
||||||
|
nil,
|
||||||
|
errBadKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
t.Run(table.keyname, func(t *testing.T) {
|
||||||
|
tsig := TSIG{
|
||||||
|
Hdr: RR_Header{Name: table.keyname, Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0},
|
||||||
|
Algorithm: HmacSHA1,
|
||||||
|
TimeSigned: timeSigned,
|
||||||
|
Fudge: 300,
|
||||||
|
OrigId: 42,
|
||||||
|
}
|
||||||
|
req := &Msg{
|
||||||
|
MsgHdr: MsgHdr{Opcode: OpcodeUpdate},
|
||||||
|
Question: []Question{{Name: "example.com.", Qtype: TypeSOA, Qclass: ClassINET}},
|
||||||
|
Extra: []RR{&tsig},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, mac, err := TsigGenerateWithProvider(req, new(testProvider), "", false)
|
||||||
|
if err != table.err {
|
||||||
|
t.Fatalf("error doesn't match: expected '%s' but got '%s'", table.err, err)
|
||||||
|
}
|
||||||
|
expectedMAC := hex.EncodeToString(table.mac)
|
||||||
|
if mac != expectedMAC {
|
||||||
|
t.Fatalf("MAC doesn't match: expected '%s' but got '%s'", table.mac, expectedMAC)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTsigVerifyProvider(t *testing.T) {
|
||||||
|
tables := []struct {
|
||||||
|
keyname string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
testGoodKeyName,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"badkey.",
|
||||||
|
errBadKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
t.Run(table.keyname, func(t *testing.T) {
|
||||||
|
tsig := TSIG{
|
||||||
|
Hdr: RR_Header{Name: table.keyname, Rrtype: TypeTSIG, Class: ClassANY, Ttl: 0},
|
||||||
|
Algorithm: HmacSHA1,
|
||||||
|
TimeSigned: timeSigned,
|
||||||
|
Fudge: 300,
|
||||||
|
OrigId: 42,
|
||||||
|
}
|
||||||
|
req := &Msg{
|
||||||
|
MsgHdr: MsgHdr{Opcode: OpcodeUpdate},
|
||||||
|
Question: []Question{{Name: "example.com.", Qtype: TypeSOA, Qclass: ClassINET}},
|
||||||
|
Extra: []RR{&tsig},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := &testProvider{true}
|
||||||
|
msgData, _, err := TsigGenerateWithProvider(req, provider, "", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if err = tsigVerify(msgData, provider, "", false, timeSigned); err != table.err {
|
||||||
|
t.Fatalf("error doesn't match: expected '%s' but got '%s'", table.err, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
255
types.go
255
types.go
|
@ -1,6 +1,7 @@
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -61,6 +62,7 @@ const (
|
||||||
TypeCERT uint16 = 37
|
TypeCERT uint16 = 37
|
||||||
TypeDNAME uint16 = 39
|
TypeDNAME uint16 = 39
|
||||||
TypeOPT uint16 = 41 // EDNS
|
TypeOPT uint16 = 41 // EDNS
|
||||||
|
TypeAPL uint16 = 42
|
||||||
TypeDS uint16 = 43
|
TypeDS uint16 = 43
|
||||||
TypeSSHFP uint16 = 44
|
TypeSSHFP uint16 = 44
|
||||||
TypeRRSIG uint16 = 46
|
TypeRRSIG uint16 = 46
|
||||||
|
@ -79,6 +81,9 @@ const (
|
||||||
TypeCDNSKEY uint16 = 60
|
TypeCDNSKEY uint16 = 60
|
||||||
TypeOPENPGPKEY uint16 = 61
|
TypeOPENPGPKEY uint16 = 61
|
||||||
TypeCSYNC uint16 = 62
|
TypeCSYNC uint16 = 62
|
||||||
|
TypeZONEMD uint16 = 63
|
||||||
|
TypeSVCB uint16 = 64
|
||||||
|
TypeHTTPS uint16 = 65
|
||||||
TypeSPF uint16 = 99
|
TypeSPF uint16 = 99
|
||||||
TypeUINFO uint16 = 100
|
TypeUINFO uint16 = 100
|
||||||
TypeUID uint16 = 101
|
TypeUID uint16 = 101
|
||||||
|
@ -146,6 +151,14 @@ const (
|
||||||
OpcodeUpdate = 5
|
OpcodeUpdate = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Used in ZONEMD https://tools.ietf.org/html/rfc8976
|
||||||
|
const (
|
||||||
|
ZoneMDSchemeSimple = 1
|
||||||
|
|
||||||
|
ZoneMDHashAlgSHA384 = 1
|
||||||
|
ZoneMDHashAlgSHA512 = 2
|
||||||
|
)
|
||||||
|
|
||||||
// Header is the wire format for the DNS packet header.
|
// Header is the wire format for the DNS packet header.
|
||||||
type Header struct {
|
type Header struct {
|
||||||
Id uint16
|
Id uint16
|
||||||
|
@ -163,11 +176,11 @@ const (
|
||||||
_RD = 1 << 8 // recursion desired
|
_RD = 1 << 8 // recursion desired
|
||||||
_RA = 1 << 7 // recursion available
|
_RA = 1 << 7 // recursion available
|
||||||
_Z = 1 << 6 // Z
|
_Z = 1 << 6 // Z
|
||||||
_AD = 1 << 5 // authticated data
|
_AD = 1 << 5 // authenticated data
|
||||||
_CD = 1 << 4 // checking disabled
|
_CD = 1 << 4 // checking disabled
|
||||||
)
|
)
|
||||||
|
|
||||||
// Various constants used in the LOC RR, See RFC 1887.
|
// Various constants used in the LOC RR. See RFC 1887.
|
||||||
const (
|
const (
|
||||||
LOC_EQUATOR = 1 << 31 // RFC 1876, Section 2.
|
LOC_EQUATOR = 1 << 31 // RFC 1876, Section 2.
|
||||||
LOC_PRIMEMERIDIAN = 1 << 31 // RFC 1876, Section 2.
|
LOC_PRIMEMERIDIAN = 1 << 31 // RFC 1876, Section 2.
|
||||||
|
@ -207,8 +220,11 @@ var CertTypeToString = map[uint16]string{
|
||||||
|
|
||||||
//go:generate go run types_generate.go
|
//go:generate go run types_generate.go
|
||||||
|
|
||||||
// Question holds a DNS question. There can be multiple questions in the
|
// Question holds a DNS question. Usually there is just one. While the
|
||||||
// question section of a message. Usually there is just one.
|
// original DNS RFCs allow multiple questions in the question section of a
|
||||||
|
// message, in practice it never works. Because most DNS servers see multiple
|
||||||
|
// questions as an error, it is recommended to only have one question per
|
||||||
|
// message.
|
||||||
type Question struct {
|
type Question struct {
|
||||||
Name string `dns:"cdomain-name"` // "cdomain-name" specifies encoding (and may be compressed)
|
Name string `dns:"cdomain-name"` // "cdomain-name" specifies encoding (and may be compressed)
|
||||||
Qtype uint16
|
Qtype uint16
|
||||||
|
@ -229,7 +245,7 @@ func (q *Question) String() (s string) {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ANY is a wildcard record. See RFC 1035, Section 3.2.3. ANY
|
// ANY is a wild card record. See RFC 1035, Section 3.2.3. ANY
|
||||||
// is named "*" there.
|
// is named "*" there.
|
||||||
type ANY struct {
|
type ANY struct {
|
||||||
Hdr RR_Header
|
Hdr RR_Header
|
||||||
|
@ -238,8 +254,8 @@ type ANY struct {
|
||||||
|
|
||||||
func (rr *ANY) String() string { return rr.Hdr.String() }
|
func (rr *ANY) String() string { return rr.Hdr.String() }
|
||||||
|
|
||||||
func (rr *ANY) parse(c *zlexer, origin, file string) *ParseError {
|
func (*ANY) parse(c *zlexer, origin string) *ParseError {
|
||||||
panic("dns: internal error: parse should never be called on ANY")
|
return &ParseError{err: "ANY records do not have a presentation format"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NULL RR. See RFC 1035.
|
// NULL RR. See RFC 1035.
|
||||||
|
@ -253,8 +269,8 @@ func (rr *NULL) String() string {
|
||||||
return ";" + rr.Hdr.String() + rr.Data
|
return ";" + rr.Hdr.String() + rr.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rr *NULL) parse(c *zlexer, origin, file string) *ParseError {
|
func (*NULL) parse(c *zlexer, origin string) *ParseError {
|
||||||
panic("dns: internal error: parse should never be called on NULL")
|
return &ParseError{err: "NULL records do not have a presentation format"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CNAME RR. See RFC 1034.
|
// CNAME RR. See RFC 1034.
|
||||||
|
@ -438,25 +454,47 @@ func (rr *TXT) String() string { return rr.Hdr.String() + sprintTxt(rr.Txt) }
|
||||||
|
|
||||||
func sprintName(s string) string {
|
func sprintName(s string) string {
|
||||||
var dst strings.Builder
|
var dst strings.Builder
|
||||||
dst.Grow(len(s))
|
|
||||||
for i := 0; i < len(s); {
|
for i := 0; i < len(s); {
|
||||||
if i+1 < len(s) && s[i] == '\\' && s[i+1] == '.' {
|
if s[i] == '.' {
|
||||||
dst.WriteString(s[i : i+2])
|
if dst.Len() != 0 {
|
||||||
i += 2
|
dst.WriteByte('.')
|
||||||
|
}
|
||||||
|
i++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
b, n := nextByte(s, i)
|
b, n := nextByte(s, i)
|
||||||
switch {
|
if n == 0 {
|
||||||
case n == 0:
|
// Drop "dangling" incomplete escapes.
|
||||||
i++ // dangling back slash
|
if dst.Len() == 0 {
|
||||||
case b == '.':
|
return s[:i]
|
||||||
dst.WriteByte('.')
|
}
|
||||||
default:
|
break
|
||||||
writeDomainNameByte(&dst, b)
|
}
|
||||||
|
if isDomainNameLabelSpecial(b) {
|
||||||
|
if dst.Len() == 0 {
|
||||||
|
dst.Grow(len(s) * 2)
|
||||||
|
dst.WriteString(s[:i])
|
||||||
|
}
|
||||||
|
dst.WriteByte('\\')
|
||||||
|
dst.WriteByte(b)
|
||||||
|
} else if b < ' ' || b > '~' { // unprintable, use \DDD
|
||||||
|
if dst.Len() == 0 {
|
||||||
|
dst.Grow(len(s) * 2)
|
||||||
|
dst.WriteString(s[:i])
|
||||||
|
}
|
||||||
|
dst.WriteString(escapeByte(b))
|
||||||
|
} else {
|
||||||
|
if dst.Len() != 0 {
|
||||||
|
dst.WriteByte(b)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
i += n
|
i += n
|
||||||
}
|
}
|
||||||
|
if dst.Len() == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
return dst.String()
|
return dst.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -472,15 +510,10 @@ func sprintTxtOctet(s string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
b, n := nextByte(s, i)
|
b, n := nextByte(s, i)
|
||||||
switch {
|
if n == 0 {
|
||||||
case n == 0:
|
|
||||||
i++ // dangling back slash
|
i++ // dangling back slash
|
||||||
case b == '.':
|
} else {
|
||||||
dst.WriteByte('.')
|
writeTXTStringByte(&dst, b)
|
||||||
case b < ' ' || b > '~':
|
|
||||||
dst.WriteString(escapeByte(b))
|
|
||||||
default:
|
|
||||||
dst.WriteByte(b)
|
|
||||||
}
|
}
|
||||||
i += n
|
i += n
|
||||||
}
|
}
|
||||||
|
@ -510,16 +543,6 @@ func sprintTxt(txt []string) string {
|
||||||
return out.String()
|
return out.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeDomainNameByte(s *strings.Builder, b byte) {
|
|
||||||
switch b {
|
|
||||||
case '.', ' ', '\'', '@', ';', '(', ')': // additional chars to escape
|
|
||||||
s.WriteByte('\\')
|
|
||||||
s.WriteByte(b)
|
|
||||||
default:
|
|
||||||
writeTXTStringByte(s, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeTXTStringByte(s *strings.Builder, b byte) {
|
func writeTXTStringByte(s *strings.Builder, b byte) {
|
||||||
switch {
|
switch {
|
||||||
case b == '"' || b == '\\':
|
case b == '"' || b == '\\':
|
||||||
|
@ -566,6 +589,17 @@ func escapeByte(b byte) string {
|
||||||
return escapedByteLarge[int(b)*4 : int(b)*4+4]
|
return escapedByteLarge[int(b)*4 : int(b)*4+4]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isDomainNameLabelSpecial returns true if
|
||||||
|
// a domain name label byte should be prefixed
|
||||||
|
// with an escaping backslash.
|
||||||
|
func isDomainNameLabelSpecial(b byte) bool {
|
||||||
|
switch b {
|
||||||
|
case '.', ' ', '\'', '@', ';', '(', ')', '"', '\\':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func nextByte(s string, offset int) (byte, int) {
|
func nextByte(s string, offset int) (byte, int) {
|
||||||
if offset >= len(s) {
|
if offset >= len(s) {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
|
@ -738,8 +772,8 @@ type LOC struct {
|
||||||
Altitude uint32
|
Altitude uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmToM takes a cm value expressed in RFC1876 SIZE mantissa/exponent
|
// cmToM takes a cm value expressed in RFC 1876 SIZE mantissa/exponent
|
||||||
// format and returns a string in m (two decimals for the cm)
|
// format and returns a string in m (two decimals for the cm).
|
||||||
func cmToM(m, e uint8) string {
|
func cmToM(m, e uint8) string {
|
||||||
if e < 2 {
|
if e < 2 {
|
||||||
if e == 1 {
|
if e == 1 {
|
||||||
|
@ -854,14 +888,7 @@ func (rr *NSEC) String() string {
|
||||||
func (rr *NSEC) len(off int, compression map[string]struct{}) int {
|
func (rr *NSEC) len(off int, compression map[string]struct{}) int {
|
||||||
l := rr.Hdr.len(off, compression)
|
l := rr.Hdr.len(off, compression)
|
||||||
l += domainNameLen(rr.NextDomain, off+l, compression, false)
|
l += domainNameLen(rr.NextDomain, off+l, compression, false)
|
||||||
lastwindow := uint32(2 ^ 32 + 1)
|
l += typeBitMapLen(rr.TypeBitMap)
|
||||||
for _, t := range rr.TypeBitMap {
|
|
||||||
window := t / 256
|
|
||||||
if uint32(window) != lastwindow {
|
|
||||||
l += 1 + 32
|
|
||||||
}
|
|
||||||
lastwindow = uint32(window)
|
|
||||||
}
|
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1020,14 +1047,7 @@ func (rr *NSEC3) String() string {
|
||||||
func (rr *NSEC3) len(off int, compression map[string]struct{}) int {
|
func (rr *NSEC3) len(off int, compression map[string]struct{}) int {
|
||||||
l := rr.Hdr.len(off, compression)
|
l := rr.Hdr.len(off, compression)
|
||||||
l += 6 + len(rr.Salt)/2 + 1 + len(rr.NextDomain) + 1
|
l += 6 + len(rr.Salt)/2 + 1 + len(rr.NextDomain) + 1
|
||||||
lastwindow := uint32(2 ^ 32 + 1)
|
l += typeBitMapLen(rr.TypeBitMap)
|
||||||
for _, t := range rr.TypeBitMap {
|
|
||||||
window := t / 256
|
|
||||||
if uint32(window) != lastwindow {
|
|
||||||
l += 1 + 32
|
|
||||||
}
|
|
||||||
lastwindow = uint32(window)
|
|
||||||
}
|
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1111,6 +1131,7 @@ type URI struct {
|
||||||
Target string `dns:"octet"`
|
Target string `dns:"octet"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rr.Target to be parsed as a sequence of character encoded octets according to RFC 3986
|
||||||
func (rr *URI) String() string {
|
func (rr *URI) String() string {
|
||||||
return rr.Hdr.String() + strconv.Itoa(int(rr.Priority)) +
|
return rr.Hdr.String() + strconv.Itoa(int(rr.Priority)) +
|
||||||
" " + strconv.Itoa(int(rr.Weight)) + " " + sprintTxtOctet(rr.Target)
|
" " + strconv.Itoa(int(rr.Weight)) + " " + sprintTxtOctet(rr.Target)
|
||||||
|
@ -1272,6 +1293,7 @@ type CAA struct {
|
||||||
Value string `dns:"octet"`
|
Value string `dns:"octet"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rr.Value Is the character-string encoding of the value field as specified in RFC 1035, Section 5.1.
|
||||||
func (rr *CAA) String() string {
|
func (rr *CAA) String() string {
|
||||||
return rr.Hdr.String() + strconv.Itoa(int(rr.Flag)) + " " + rr.Tag + " " + sprintTxtOctet(rr.Value)
|
return rr.Hdr.String() + strconv.Itoa(int(rr.Flag)) + " " + rr.Tag + " " + sprintTxtOctet(rr.Value)
|
||||||
}
|
}
|
||||||
|
@ -1344,17 +1366,109 @@ func (rr *CSYNC) String() string {
|
||||||
func (rr *CSYNC) len(off int, compression map[string]struct{}) int {
|
func (rr *CSYNC) len(off int, compression map[string]struct{}) int {
|
||||||
l := rr.Hdr.len(off, compression)
|
l := rr.Hdr.len(off, compression)
|
||||||
l += 4 + 2
|
l += 4 + 2
|
||||||
lastwindow := uint32(2 ^ 32 + 1)
|
l += typeBitMapLen(rr.TypeBitMap)
|
||||||
for _, t := range rr.TypeBitMap {
|
|
||||||
window := t / 256
|
|
||||||
if uint32(window) != lastwindow {
|
|
||||||
l += 1 + 32
|
|
||||||
}
|
|
||||||
lastwindow = uint32(window)
|
|
||||||
}
|
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ZONEMD RR, from draft-ietf-dnsop-dns-zone-digest
|
||||||
|
type ZONEMD struct {
|
||||||
|
Hdr RR_Header
|
||||||
|
Serial uint32
|
||||||
|
Scheme uint8
|
||||||
|
Hash uint8
|
||||||
|
Digest string `dns:"hex"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *ZONEMD) String() string {
|
||||||
|
return rr.Hdr.String() +
|
||||||
|
strconv.Itoa(int(rr.Serial)) +
|
||||||
|
" " + strconv.Itoa(int(rr.Scheme)) +
|
||||||
|
" " + strconv.Itoa(int(rr.Hash)) +
|
||||||
|
" " + rr.Digest
|
||||||
|
}
|
||||||
|
|
||||||
|
// APL RR. See RFC 3123.
|
||||||
|
type APL struct {
|
||||||
|
Hdr RR_Header
|
||||||
|
Prefixes []APLPrefix `dns:"apl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APLPrefix is an address prefix hold by an APL record.
|
||||||
|
type APLPrefix struct {
|
||||||
|
Negation bool
|
||||||
|
Network net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns presentation form of the APL record.
|
||||||
|
func (rr *APL) String() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(rr.Hdr.String())
|
||||||
|
for i, p := range rr.Prefixes {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
}
|
||||||
|
sb.WriteString(p.str())
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// str returns presentation form of the APL prefix.
|
||||||
|
func (a *APLPrefix) str() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
if a.Negation {
|
||||||
|
sb.WriteByte('!')
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(a.Network.IP) {
|
||||||
|
case net.IPv4len:
|
||||||
|
sb.WriteByte('1')
|
||||||
|
case net.IPv6len:
|
||||||
|
sb.WriteByte('2')
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteByte(':')
|
||||||
|
|
||||||
|
switch len(a.Network.IP) {
|
||||||
|
case net.IPv4len:
|
||||||
|
sb.WriteString(a.Network.IP.String())
|
||||||
|
case net.IPv6len:
|
||||||
|
// add prefix for IPv4-mapped IPv6
|
||||||
|
if v4 := a.Network.IP.To4(); v4 != nil {
|
||||||
|
sb.WriteString("::ffff:")
|
||||||
|
}
|
||||||
|
sb.WriteString(a.Network.IP.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteByte('/')
|
||||||
|
|
||||||
|
prefix, _ := a.Network.Mask.Size()
|
||||||
|
sb.WriteString(strconv.Itoa(prefix))
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// equals reports whether two APL prefixes are identical.
|
||||||
|
func (a *APLPrefix) equals(b *APLPrefix) bool {
|
||||||
|
return a.Negation == b.Negation &&
|
||||||
|
bytes.Equal(a.Network.IP, b.Network.IP) &&
|
||||||
|
bytes.Equal(a.Network.Mask, b.Network.Mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy returns a copy of the APL prefix.
|
||||||
|
func (a *APLPrefix) copy() APLPrefix {
|
||||||
|
return APLPrefix{
|
||||||
|
Negation: a.Negation,
|
||||||
|
Network: copyNet(a.Network),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// len returns size of the prefix in wire format.
|
||||||
|
func (a *APLPrefix) len() int {
|
||||||
|
// 4-byte header and the network address prefix (see Section 4 of RFC 3123)
|
||||||
|
prefix, _ := a.Network.Mask.Size()
|
||||||
|
return 4 + (prefix+7)/8
|
||||||
|
}
|
||||||
|
|
||||||
// TimeToString translates the RRSIG's incep. and expir. times to the
|
// TimeToString translates the RRSIG's incep. and expir. times to the
|
||||||
// string representation used when printing the record.
|
// string representation used when printing the record.
|
||||||
// It takes serial arithmetic (RFC 1982) into account.
|
// It takes serial arithmetic (RFC 1982) into account.
|
||||||
|
@ -1384,7 +1498,7 @@ func StringToTime(s string) (uint32, error) {
|
||||||
|
|
||||||
// saltToString converts a NSECX salt to uppercase and returns "-" when it is empty.
|
// saltToString converts a NSECX salt to uppercase and returns "-" when it is empty.
|
||||||
func saltToString(s string) string {
|
func saltToString(s string) string {
|
||||||
if len(s) == 0 {
|
if s == "" {
|
||||||
return "-"
|
return "-"
|
||||||
}
|
}
|
||||||
return strings.ToUpper(s)
|
return strings.ToUpper(s)
|
||||||
|
@ -1411,6 +1525,17 @@ func copyIP(ip net.IP) net.IP {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyNet returns a copy of a subnet.
|
||||||
|
func copyNet(n net.IPNet) net.IPNet {
|
||||||
|
m := make(net.IPMask, len(n.Mask))
|
||||||
|
copy(m, n.Mask)
|
||||||
|
|
||||||
|
return net.IPNet{
|
||||||
|
IP: copyIP(n.IP),
|
||||||
|
Mask: m,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SplitN splits a string into N sized string chunks.
|
// SplitN splits a string into N sized string chunks.
|
||||||
// This might become an exported function once.
|
// This might become an exported function once.
|
||||||
func splitN(s string, n int) []string {
|
func splitN(s string, n int) []string {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
//+build ignore
|
//go:build ignore
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
// types_generate.go is meant to run with go generate. It will use
|
// types_generate.go is meant to run with go generate. It will use
|
||||||
// go/{importer,types} to track down all the RR struct types. Then for each type
|
// go/{importer,types} to track down all the RR struct types. Then for each type
|
||||||
|
@ -11,12 +12,13 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/format"
|
"go/format"
|
||||||
"go/importer"
|
|
||||||
"go/types"
|
"go/types"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
)
|
)
|
||||||
|
|
||||||
var skipLen = map[string]struct{}{
|
var skipLen = map[string]struct{}{
|
||||||
|
@ -71,6 +73,9 @@ func getTypeStruct(t types.Type, scope *types.Scope) (*types.Struct, bool) {
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
if st.NumFields() == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
if st.Field(0).Type() == scope.Lookup("RR_Header").Type() {
|
if st.Field(0).Type() == scope.Lookup("RR_Header").Type() {
|
||||||
return st, false
|
return st, false
|
||||||
}
|
}
|
||||||
|
@ -81,9 +86,19 @@ func getTypeStruct(t types.Type, scope *types.Scope) (*types.Struct, bool) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadModule retrieves package description for a given module.
|
||||||
|
func loadModule(name string) (*types.Package, error) {
|
||||||
|
conf := packages.Config{Mode: packages.NeedTypes | packages.NeedTypesInfo}
|
||||||
|
pkgs, err := packages.Load(&conf, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pkgs[0].Types, nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Import and type-check the package
|
// Import and type-check the package
|
||||||
pkg, err := importer.Default().Import("github.com/miekg/dns")
|
pkg, err := loadModule("github.com/miekg/dns")
|
||||||
fatalIfErr(err)
|
fatalIfErr(err)
|
||||||
scope := pkg.Scope()
|
scope := pkg.Scope()
|
||||||
|
|
||||||
|
@ -168,6 +183,10 @@ func main() {
|
||||||
o("for _, x := range rr.%s { l += domainNameLen(x, off+l, compression, false) }\n")
|
o("for _, x := range rr.%s { l += domainNameLen(x, off+l, compression, false) }\n")
|
||||||
case `dns:"txt"`:
|
case `dns:"txt"`:
|
||||||
o("for _, x := range rr.%s { l += len(x) + 1 }\n")
|
o("for _, x := range rr.%s { l += len(x) + 1 }\n")
|
||||||
|
case `dns:"apl"`:
|
||||||
|
o("for _, x := range rr.%s { l += x.len() }\n")
|
||||||
|
case `dns:"pairs"`:
|
||||||
|
o("for _, x := range rr.%s { l += 4 + int(x.len()) }\n")
|
||||||
default:
|
default:
|
||||||
log.Fatalln(name, st.Field(i).Name(), st.Tag(i))
|
log.Fatalln(name, st.Field(i).Name(), st.Tag(i))
|
||||||
}
|
}
|
||||||
|
@ -189,16 +208,14 @@ func main() {
|
||||||
o("l += base64.StdEncoding.DecodedLen(len(rr.%s))\n")
|
o("l += base64.StdEncoding.DecodedLen(len(rr.%s))\n")
|
||||||
case strings.HasPrefix(st.Tag(i), `dns:"size-hex:`): // this has an extra field where the length is stored
|
case strings.HasPrefix(st.Tag(i), `dns:"size-hex:`): // this has an extra field where the length is stored
|
||||||
o("l += len(rr.%s)/2\n")
|
o("l += len(rr.%s)/2\n")
|
||||||
case strings.HasPrefix(st.Tag(i), `dns:"size-hex`):
|
|
||||||
fallthrough
|
|
||||||
case st.Tag(i) == `dns:"hex"`:
|
case st.Tag(i) == `dns:"hex"`:
|
||||||
o("l += len(rr.%s)/2 + 1\n")
|
o("l += len(rr.%s)/2\n")
|
||||||
case st.Tag(i) == `dns:"any"`:
|
case st.Tag(i) == `dns:"any"`:
|
||||||
o("l += len(rr.%s)\n")
|
o("l += len(rr.%s)\n")
|
||||||
case st.Tag(i) == `dns:"a"`:
|
case st.Tag(i) == `dns:"a"`:
|
||||||
o("l += net.IPv4len // %s\n")
|
o("if len(rr.%s) != 0 { l += net.IPv4len }\n")
|
||||||
case st.Tag(i) == `dns:"aaaa"`:
|
case st.Tag(i) == `dns:"aaaa"`:
|
||||||
o("l += net.IPv6len // %s\n")
|
o("if len(rr.%s) != 0 { l += net.IPv6len }\n")
|
||||||
case st.Tag(i) == `dns:"txt"`:
|
case st.Tag(i) == `dns:"txt"`:
|
||||||
o("for _, t := range rr.%s { l += len(t) + 1 }\n")
|
o("for _, t := range rr.%s { l += len(t) + 1 }\n")
|
||||||
case st.Tag(i) == `dns:"uint48"`:
|
case st.Tag(i) == `dns:"uint48"`:
|
||||||
|
@ -230,11 +247,15 @@ func main() {
|
||||||
for _, name := range namedTypes {
|
for _, name := range namedTypes {
|
||||||
o := scope.Lookup(name)
|
o := scope.Lookup(name)
|
||||||
st, isEmbedded := getTypeStruct(o.Type(), scope)
|
st, isEmbedded := getTypeStruct(o.Type(), scope)
|
||||||
if isEmbedded {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Fprintf(b, "func (rr *%s) copy() RR {\n", name)
|
fmt.Fprintf(b, "func (rr *%s) copy() RR {\n", name)
|
||||||
fields := []string{"rr.Hdr"}
|
fields := make([]string, 0, st.NumFields())
|
||||||
|
if isEmbedded {
|
||||||
|
a, _ := o.Type().Underlying().(*types.Struct)
|
||||||
|
parent := a.Field(0).Name()
|
||||||
|
fields = append(fields, "*rr."+parent+".copy().(*"+parent+")")
|
||||||
|
goto WriteCopy
|
||||||
|
}
|
||||||
|
fields = append(fields, "rr.Hdr")
|
||||||
for i := 1; i < st.NumFields(); i++ {
|
for i := 1; i < st.NumFields(); i++ {
|
||||||
f := st.Field(i).Name()
|
f := st.Field(i).Name()
|
||||||
if sl, ok := st.Field(i).Type().(*types.Slice); ok {
|
if sl, ok := st.Field(i).Type().(*types.Slice); ok {
|
||||||
|
@ -251,6 +272,18 @@ func main() {
|
||||||
fields = append(fields, f)
|
fields = append(fields, f)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if t == "APLPrefix" {
|
||||||
|
fmt.Fprintf(b, "%s := make([]%s, len(rr.%s));\nfor i,e := range rr.%s {\n %s[i] = e.copy()\n}\n",
|
||||||
|
f, t, f, f, f)
|
||||||
|
fields = append(fields, f)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t == "SVCBKeyValue" {
|
||||||
|
fmt.Fprintf(b, "%s := make([]%s, len(rr.%s));\nfor i,e := range rr.%s {\n %s[i] = e.copy()\n}\n",
|
||||||
|
f, t, f, f, f)
|
||||||
|
fields = append(fields, f)
|
||||||
|
continue
|
||||||
|
}
|
||||||
fmt.Fprintf(b, "%s := make([]%s, len(rr.%s)); copy(%s, rr.%s)\n",
|
fmt.Fprintf(b, "%s := make([]%s, len(rr.%s)); copy(%s, rr.%s)\n",
|
||||||
f, t, f, f, f)
|
f, t, f, f, f)
|
||||||
fields = append(fields, f)
|
fields = append(fields, f)
|
||||||
|
@ -262,6 +295,7 @@ func main() {
|
||||||
}
|
}
|
||||||
fields = append(fields, "rr."+f)
|
fields = append(fields, "rr."+f)
|
||||||
}
|
}
|
||||||
|
WriteCopy:
|
||||||
fmt.Fprintf(b, "return &%s{%s}\n", name, strings.Join(fields, ","))
|
fmt.Fprintf(b, "return &%s{%s}\n", name, strings.Join(fields, ","))
|
||||||
fmt.Fprintf(b, "}\n")
|
fmt.Fprintf(b, "}\n")
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,18 +74,41 @@ func TestSplitN(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSprintName(t *testing.T) {
|
func TestSprintName(t *testing.T) {
|
||||||
got := sprintName("abc\\.def\007\"\127@\255\x05\xef\\")
|
tests := map[string]string{
|
||||||
|
// Non-numeric escaping of special printable characters.
|
||||||
|
" '@;()\"\\..example": `\ \'\@\;\(\)\"\..example`,
|
||||||
|
"\\032\\039\\064\\059\\040\\041\\034\\046\\092.example": `\ \'\@\;\(\)\"\.\\.example`,
|
||||||
|
|
||||||
if want := "abc\\.def\\007\\\"W\\@\\173\\005\\239"; got != want {
|
// Numeric escaping of nonprintable characters.
|
||||||
t.Errorf("expected %q, got %q", got, want)
|
"\x00\x07\x09\x0a\x1f.\x7f\x80\xad\xef\xff": `\000\007\009\010\031.\127\128\173\239\255`,
|
||||||
|
"\\000\\007\\009\\010\\031.\\127\\128\\173\\239\\255": `\000\007\009\010\031.\127\128\173\239\255`,
|
||||||
|
|
||||||
|
// No escaping of other printable characters, at least after a prior escape.
|
||||||
|
";[a-zA-Z0-9_]+/*.~": `\;[a-zA-Z0-9_]+/*.~`,
|
||||||
|
";\\091\\097\\045\\122\\065\\045\\090\\048\\045\\057\\095\\093\\043\\047\\042.\\126": `\;[a-zA-Z0-9_]+/*.~`,
|
||||||
|
// "\\091\\097\\045\\122\\065\\045\\090\\048\\045\\057\\095\\093\\043\\047\\042.\\126": `[a-zA-Z0-9_]+/*.~`,
|
||||||
|
|
||||||
|
// Incomplete "dangling" escapes are dropped regardless of prior escaping.
|
||||||
|
"a\\": `a`,
|
||||||
|
";\\": `\;`,
|
||||||
|
|
||||||
|
// Escaped dots stay escaped regardless of prior escaping.
|
||||||
|
"a\\.\\046.\\.\\046": `a\.\..\.\.`,
|
||||||
|
"a\\046\\..\\046\\.": `a\.\..\.\.`,
|
||||||
|
}
|
||||||
|
for input, want := range tests {
|
||||||
|
got := sprintName(input)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("input %q: expected %q, got %q", input, want, got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSprintTxtOctet(t *testing.T) {
|
func TestSprintTxtOctet(t *testing.T) {
|
||||||
got := sprintTxtOctet("abc\\.def\007\"\127@\255\x05\xef\\")
|
got := sprintTxtOctet("abc\\.def\007\"\127@\255\x05\xef\\")
|
||||||
|
|
||||||
if want := "\"abc\\.def\\007\"W@\\173\\005\\239\""; got != want {
|
if want := "\"abc\\.def\\007\\\"W@\\173\\005\\239\""; got != want {
|
||||||
t.Errorf("expected %q, got %q", got, want)
|
t.Errorf("expected %q, got %q", want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +119,7 @@ func TestSprintTxt(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
if want := "\"abc.def\\007\\\"W@\\173\\005\\239\" \"example.com\""; got != want {
|
if want := "\"abc.def\\007\\\"W@\\173\\005\\239\" \"example.com\""; got != want {
|
||||||
t.Errorf("expected %q, got %q", got, want)
|
t.Errorf("expected %q, got %q", want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +151,17 @@ func BenchmarkSprintName(b *testing.B) {
|
||||||
got := sprintName("abc\\.def\007\"\127@\255\x05\xef\\")
|
got := sprintName("abc\\.def\007\"\127@\255\x05\xef\\")
|
||||||
|
|
||||||
if want := "abc\\.def\\007\\\"W\\@\\173\\005\\239"; got != want {
|
if want := "abc\\.def\\007\\\"W\\@\\173\\005\\239"; got != want {
|
||||||
b.Fatalf("expected %q, got %q", got, want)
|
b.Fatalf("expected %q, got %q", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSprintName_NoEscape(b *testing.B) {
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
got := sprintName("large.example.com")
|
||||||
|
|
||||||
|
if want := "large.example.com"; got != want {
|
||||||
|
b.Fatalf("expected %q, got %q", want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,8 +170,8 @@ func BenchmarkSprintTxtOctet(b *testing.B) {
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
got := sprintTxtOctet("abc\\.def\007\"\127@\255\x05\xef\\")
|
got := sprintTxtOctet("abc\\.def\007\"\127@\255\x05\xef\\")
|
||||||
|
|
||||||
if want := "\"abc\\.def\\007\"W@\\173\\005\\239\""; got != want {
|
if want := "\"abc\\.def\\007\\\"W@\\173\\005\\239\""; got != want {
|
||||||
b.Fatalf("expected %q, got %q", got, want)
|
b.Fatalf("expected %q, got %q", want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,6 +182,7 @@ func BenchmarkSprintTxt(b *testing.B) {
|
||||||
"example.com",
|
"example.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
got := sprintTxt(txt)
|
got := sprintTxt(txt)
|
||||||
|
|
||||||
|
|
1
udp.go
1
udp.go
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build linux && !appengine
|
||||||
// +build linux,!appengine
|
// +build linux,!appengine
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//go:build windows
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
|
@ -32,7 +32,9 @@ func (u *Msg) Used(rr []RR) {
|
||||||
u.Answer = make([]RR, 0, len(rr))
|
u.Answer = make([]RR, 0, len(rr))
|
||||||
}
|
}
|
||||||
for _, r := range rr {
|
for _, r := range rr {
|
||||||
r.Header().Class = u.Question[0].Qclass
|
hdr := r.Header()
|
||||||
|
hdr.Class = u.Question[0].Qclass
|
||||||
|
hdr.Ttl = 0
|
||||||
u.Answer = append(u.Answer, r)
|
u.Answer = append(u.Answer, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,28 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDynamicUpdateParsing(t *testing.T) {
|
func TestDynamicUpdateParsing(t *testing.T) {
|
||||||
prefix := "example.com. IN "
|
const prefix = "example.com. IN "
|
||||||
for _, typ := range TypeToString {
|
|
||||||
if typ == "OPT" || typ == "AXFR" || typ == "IXFR" || typ == "ANY" || typ == "TKEY" ||
|
for typ, name := range TypeToString {
|
||||||
typ == "TSIG" || typ == "ISDN" || typ == "UNSPEC" || typ == "NULL" || typ == "ATMA" ||
|
switch typ {
|
||||||
typ == "Reserved" || typ == "None" || typ == "NXT" || typ == "MAILB" || typ == "MAILA" {
|
case TypeNone, TypeReserved:
|
||||||
|
continue
|
||||||
|
case TypeANY:
|
||||||
|
// ANY is ambiguous here and ends up parsed as a CLASS.
|
||||||
|
//
|
||||||
|
// TODO(tmthrgd): Using TYPE255 here doesn't seem to work and also
|
||||||
|
// seems to fail for some other record types. Investigate.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err := NewRR(prefix + typ); err != nil {
|
|
||||||
t.Errorf("failure to parse: %s %s: %v", prefix, typ, err)
|
s := prefix + name
|
||||||
|
if _, err := NewRR(s); err != nil {
|
||||||
|
t.Errorf("failure to parse: %s: %v", s, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s += " \\# 0"
|
||||||
|
if _, err := NewRR(s); err != nil {
|
||||||
|
t.Errorf("failure to parse: %s: %v", s, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +92,7 @@ func TestRemoveRRset(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPreReqAndRemovals(t *testing.T) {
|
func TestPreReqAndRemovals(t *testing.T) {
|
||||||
// Build a list of multiple prereqs and then somes removes followed by an insert.
|
// Build a list of multiple prereqs and then some removes followed by an insert.
|
||||||
// We should be able to add multiple prereqs and updates.
|
// We should be able to add multiple prereqs and updates.
|
||||||
m := new(Msg)
|
m := new(Msg)
|
||||||
m.SetUpdate("example.org.")
|
m.SetUpdate("example.org.")
|
||||||
|
@ -122,7 +135,7 @@ func TestPreReqAndRemovals(t *testing.T) {
|
||||||
name_used. 0 CLASS255 ANY
|
name_used. 0 CLASS255 ANY
|
||||||
name_not_used. 0 NONE ANY
|
name_not_used. 0 NONE ANY
|
||||||
rrset_used1. 0 CLASS255 A
|
rrset_used1. 0 CLASS255 A
|
||||||
rrset_used2. 3600 IN A 127.0.0.1
|
rrset_used2. 0 IN A 127.0.0.1
|
||||||
rrset_not_used. 0 NONE A
|
rrset_not_used. 0 NONE A
|
||||||
|
|
||||||
;; AUTHORITY SECTION:
|
;; AUTHORITY SECTION:
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Treat all files in this repo as binary, with no git magic updating
|
|
||||||
# line endings. Windows users contributing to Go will need to use a
|
|
||||||
# modern version of git and editors capable of LF line endings.
|
|
||||||
#
|
|
||||||
# We'll prevent accidental CRLF line endings from entering the repo
|
|
||||||
# via the git-review gofmt checks.
|
|
||||||
#
|
|
||||||
# See golang.org/issue/9281
|
|
||||||
|
|
||||||
* -text
|
|
|
@ -1,2 +0,0 @@
|
||||||
# Add no patterns to .hgignore except for files generated by the build.
|
|
||||||
last-change
|
|
|
@ -1,3 +0,0 @@
|
||||||
# This source code refers to The Go Authors for copyright purposes.
|
|
||||||
# The master list of authors is in the main Go distribution,
|
|
||||||
# visible at https://tip.golang.org/AUTHORS.
|
|
|
@ -1,26 +0,0 @@
|
||||||
# Contributing to Go
|
|
||||||
|
|
||||||
Go is an open source project.
|
|
||||||
|
|
||||||
It is the work of hundreds of contributors. We appreciate your help!
|
|
||||||
|
|
||||||
## Filing issues
|
|
||||||
|
|
||||||
When [filing an issue](https://golang.org/issue/new), make sure to answer these five questions:
|
|
||||||
|
|
||||||
1. What version of Go are you using (`go version`)?
|
|
||||||
2. What operating system and processor architecture are you using?
|
|
||||||
3. What did you do?
|
|
||||||
4. What did you expect to see?
|
|
||||||
5. What did you see instead?
|
|
||||||
|
|
||||||
General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker.
|
|
||||||
The gophers there will answer or ask you to file an issue if you've tripped over a bug.
|
|
||||||
|
|
||||||
## Contributing code
|
|
||||||
|
|
||||||
Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html)
|
|
||||||
before sending patches.
|
|
||||||
|
|
||||||
Unless otherwise noted, the Go source files are distributed under
|
|
||||||
the BSD-style license found in the LICENSE file.
|
|
|
@ -1,3 +0,0 @@
|
||||||
# This source code was written by the Go contributors.
|
|
||||||
# The master list of contributors is in the main Go distribution,
|
|
||||||
# visible at https://tip.golang.org/CONTRIBUTORS.
|
|
|
@ -1,27 +0,0 @@
|
||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
@ -1,22 +0,0 @@
|
||||||
Additional IP Rights Grant (Patents)
|
|
||||||
|
|
||||||
"This implementation" means the copyrightable works distributed by
|
|
||||||
Google as part of the Go project.
|
|
||||||
|
|
||||||
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
|
||||||
no-charge, royalty-free, irrevocable (except as stated in this section)
|
|
||||||
patent license to make, have made, use, offer to sell, sell, import,
|
|
||||||
transfer and otherwise run, modify and propagate the contents of this
|
|
||||||
implementation of Go, where such license applies only to those patent
|
|
||||||
claims, both currently owned or controlled by Google and acquired in
|
|
||||||
the future, licensable by Google that are necessarily infringed by this
|
|
||||||
implementation of Go. This grant does not include claims that would be
|
|
||||||
infringed only as a consequence of further modification of this
|
|
||||||
implementation. If you or your agent or exclusive licensee institute or
|
|
||||||
order or agree to the institution of patent litigation against any
|
|
||||||
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
|
||||||
that this implementation of Go or any code incorporated within this
|
|
||||||
implementation of Go constitutes direct or contributory patent
|
|
||||||
infringement, or inducement of patent infringement, then any patent
|
|
||||||
rights granted to you under this License for this implementation of Go
|
|
||||||
shall terminate as of the date such litigation is filed.
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Go Cryptography
|
|
||||||
|
|
||||||
This repository holds supplementary Go cryptography libraries.
|
|
||||||
|
|
||||||
## Download/Install
|
|
||||||
|
|
||||||
The easiest way to install is to run `go get -u golang.org/x/crypto/...`. You
|
|
||||||
can also manually git clone the repository to `$GOPATH/src/golang.org/x/crypto`.
|
|
||||||
|
|
||||||
## Report Issues / Send Patches
|
|
||||||
|
|
||||||
This repository uses Gerrit for code changes. To learn how to submit changes to
|
|
||||||
this repository, see https://golang.org/doc/contribute.html.
|
|
||||||
|
|
||||||
The main issue tracker for the crypto repository is located at
|
|
||||||
https://github.com/golang/go/issues. Prefix your issue with "x/crypto:" in the
|
|
||||||
subject line, so it is easy to find.
|
|
||||||
|
|
||||||
Note that contributions to the cryptography package receive additional scrutiny
|
|
||||||
due to their sensitive nature. Patches may take longer than normal to receive
|
|
||||||
feedback.
|
|
|
@ -1,922 +0,0 @@
|
||||||
// Copyright 2015 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package acme provides an implementation of the
|
|
||||||
// Automatic Certificate Management Environment (ACME) spec.
|
|
||||||
// See https://tools.ietf.org/html/draft-ietf-acme-acme-02 for details.
|
|
||||||
//
|
|
||||||
// Most common scenarios will want to use autocert subdirectory instead,
|
|
||||||
// which provides automatic access to certificates from Let's Encrypt
|
|
||||||
// and any other ACME-based CA.
|
|
||||||
//
|
|
||||||
// This package is a work in progress and makes no API stability promises.
|
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/asn1"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"math/big"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
|
|
||||||
LetsEncryptURL = "https://acme-v01.api.letsencrypt.org/directory"
|
|
||||||
|
|
||||||
// ALPNProto is the ALPN protocol name used by a CA server when validating
|
|
||||||
// tls-alpn-01 challenges.
|
|
||||||
//
|
|
||||||
// Package users must ensure their servers can negotiate the ACME ALPN in
|
|
||||||
// order for tls-alpn-01 challenge verifications to succeed.
|
|
||||||
// See the crypto/tls package's Config.NextProtos field.
|
|
||||||
ALPNProto = "acme-tls/1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// idPeACMEIdentifierV1 is the OID for the ACME extension for the TLS-ALPN challenge.
|
|
||||||
var idPeACMEIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxChainLen = 5 // max depth and breadth of a certificate chain
|
|
||||||
maxCertSize = 1 << 20 // max size of a certificate, in bytes
|
|
||||||
|
|
||||||
// Max number of collected nonces kept in memory.
|
|
||||||
// Expect usual peak of 1 or 2.
|
|
||||||
maxNonces = 100
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client is an ACME client.
|
|
||||||
// The only required field is Key. An example of creating a client with a new key
|
|
||||||
// is as follows:
|
|
||||||
//
|
|
||||||
// key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
// }
|
|
||||||
// client := &Client{Key: key}
|
|
||||||
//
|
|
||||||
type Client struct {
|
|
||||||
// Key is the account key used to register with a CA and sign requests.
|
|
||||||
// Key.Public() must return a *rsa.PublicKey or *ecdsa.PublicKey.
|
|
||||||
Key crypto.Signer
|
|
||||||
|
|
||||||
// HTTPClient optionally specifies an HTTP client to use
|
|
||||||
// instead of http.DefaultClient.
|
|
||||||
HTTPClient *http.Client
|
|
||||||
|
|
||||||
// DirectoryURL points to the CA directory endpoint.
|
|
||||||
// If empty, LetsEncryptURL is used.
|
|
||||||
// Mutating this value after a successful call of Client's Discover method
|
|
||||||
// will have no effect.
|
|
||||||
DirectoryURL string
|
|
||||||
|
|
||||||
// RetryBackoff computes the duration after which the nth retry of a failed request
|
|
||||||
// should occur. The value of n for the first call on failure is 1.
|
|
||||||
// The values of r and resp are the request and response of the last failed attempt.
|
|
||||||
// If the returned value is negative or zero, no more retries are done and an error
|
|
||||||
// is returned to the caller of the original method.
|
|
||||||
//
|
|
||||||
// Requests which result in a 4xx client error are not retried,
|
|
||||||
// except for 400 Bad Request due to "bad nonce" errors and 429 Too Many Requests.
|
|
||||||
//
|
|
||||||
// If RetryBackoff is nil, a truncated exponential backoff algorithm
|
|
||||||
// with the ceiling of 10 seconds is used, where each subsequent retry n
|
|
||||||
// is done after either ("Retry-After" + jitter) or (2^n seconds + jitter),
|
|
||||||
// preferring the former if "Retry-After" header is found in the resp.
|
|
||||||
// The jitter is a random value up to 1 second.
|
|
||||||
RetryBackoff func(n int, r *http.Request, resp *http.Response) time.Duration
|
|
||||||
|
|
||||||
dirMu sync.Mutex // guards writes to dir
|
|
||||||
dir *Directory // cached result of Client's Discover method
|
|
||||||
|
|
||||||
noncesMu sync.Mutex
|
|
||||||
nonces map[string]struct{} // nonces collected from previous responses
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discover performs ACME server discovery using c.DirectoryURL.
|
|
||||||
//
|
|
||||||
// It caches successful result. So, subsequent calls will not result in
|
|
||||||
// a network round-trip. This also means mutating c.DirectoryURL after successful call
|
|
||||||
// of this method will have no effect.
|
|
||||||
func (c *Client) Discover(ctx context.Context) (Directory, error) {
|
|
||||||
c.dirMu.Lock()
|
|
||||||
defer c.dirMu.Unlock()
|
|
||||||
if c.dir != nil {
|
|
||||||
return *c.dir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dirURL := c.DirectoryURL
|
|
||||||
if dirURL == "" {
|
|
||||||
dirURL = LetsEncryptURL
|
|
||||||
}
|
|
||||||
res, err := c.get(ctx, dirURL, wantStatus(http.StatusOK))
|
|
||||||
if err != nil {
|
|
||||||
return Directory{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
c.addNonce(res.Header)
|
|
||||||
|
|
||||||
var v struct {
|
|
||||||
Reg string `json:"new-reg"`
|
|
||||||
Authz string `json:"new-authz"`
|
|
||||||
Cert string `json:"new-cert"`
|
|
||||||
Revoke string `json:"revoke-cert"`
|
|
||||||
Meta struct {
|
|
||||||
Terms string `json:"terms-of-service"`
|
|
||||||
Website string `json:"website"`
|
|
||||||
CAA []string `json:"caa-identities"`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
|
||||||
return Directory{}, err
|
|
||||||
}
|
|
||||||
c.dir = &Directory{
|
|
||||||
RegURL: v.Reg,
|
|
||||||
AuthzURL: v.Authz,
|
|
||||||
CertURL: v.Cert,
|
|
||||||
RevokeURL: v.Revoke,
|
|
||||||
Terms: v.Meta.Terms,
|
|
||||||
Website: v.Meta.Website,
|
|
||||||
CAA: v.Meta.CAA,
|
|
||||||
}
|
|
||||||
return *c.dir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateCert requests a new certificate using the Certificate Signing Request csr encoded in DER format.
|
|
||||||
// The exp argument indicates the desired certificate validity duration. CA may issue a certificate
|
|
||||||
// with a different duration.
|
|
||||||
// If the bundle argument is true, the returned value will also contain the CA (issuer) certificate chain.
|
|
||||||
//
|
|
||||||
// In the case where CA server does not provide the issued certificate in the response,
|
|
||||||
// CreateCert will poll certURL using c.FetchCert, which will result in additional round-trips.
|
|
||||||
// In such a scenario, the caller can cancel the polling with ctx.
|
|
||||||
//
|
|
||||||
// CreateCert returns an error if the CA's response or chain was unreasonably large.
|
|
||||||
// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features.
|
|
||||||
func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, bundle bool) (der [][]byte, certURL string, err error) {
|
|
||||||
if _, err := c.Discover(ctx); err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
req := struct {
|
|
||||||
Resource string `json:"resource"`
|
|
||||||
CSR string `json:"csr"`
|
|
||||||
NotBefore string `json:"notBefore,omitempty"`
|
|
||||||
NotAfter string `json:"notAfter,omitempty"`
|
|
||||||
}{
|
|
||||||
Resource: "new-cert",
|
|
||||||
CSR: base64.RawURLEncoding.EncodeToString(csr),
|
|
||||||
}
|
|
||||||
now := timeNow()
|
|
||||||
req.NotBefore = now.Format(time.RFC3339)
|
|
||||||
if exp > 0 {
|
|
||||||
req.NotAfter = now.Add(exp).Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.post(ctx, c.Key, c.dir.CertURL, req, wantStatus(http.StatusCreated))
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
curl := res.Header.Get("Location") // cert permanent URL
|
|
||||||
if res.ContentLength == 0 {
|
|
||||||
// no cert in the body; poll until we get it
|
|
||||||
cert, err := c.FetchCert(ctx, curl, bundle)
|
|
||||||
return cert, curl, err
|
|
||||||
}
|
|
||||||
// slurp issued cert and CA chain, if requested
|
|
||||||
cert, err := c.responseCert(ctx, res, bundle)
|
|
||||||
return cert, curl, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchCert retrieves already issued certificate from the given url, in DER format.
|
|
||||||
// It retries the request until the certificate is successfully retrieved,
|
|
||||||
// context is cancelled by the caller or an error response is received.
|
|
||||||
//
|
|
||||||
// The returned value will also contain the CA (issuer) certificate if the bundle argument is true.
|
|
||||||
//
|
|
||||||
// FetchCert returns an error if the CA's response or chain was unreasonably large.
|
|
||||||
// Callers are encouraged to parse the returned value to ensure the certificate is valid
|
|
||||||
// and has expected features.
|
|
||||||
func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]byte, error) {
|
|
||||||
res, err := c.get(ctx, url, wantStatus(http.StatusOK))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return c.responseCert(ctx, res, bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RevokeCert revokes a previously issued certificate cert, provided in DER format.
|
|
||||||
//
|
|
||||||
// The key argument, used to sign the request, must be authorized
|
|
||||||
// to revoke the certificate. It's up to the CA to decide which keys are authorized.
|
|
||||||
// For instance, the key pair of the certificate may be authorized.
|
|
||||||
// If the key is nil, c.Key is used instead.
|
|
||||||
func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error {
|
|
||||||
if _, err := c.Discover(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body := &struct {
|
|
||||||
Resource string `json:"resource"`
|
|
||||||
Cert string `json:"certificate"`
|
|
||||||
Reason int `json:"reason"`
|
|
||||||
}{
|
|
||||||
Resource: "revoke-cert",
|
|
||||||
Cert: base64.RawURLEncoding.EncodeToString(cert),
|
|
||||||
Reason: int(reason),
|
|
||||||
}
|
|
||||||
if key == nil {
|
|
||||||
key = c.Key
|
|
||||||
}
|
|
||||||
res, err := c.post(ctx, key, c.dir.RevokeURL, body, wantStatus(http.StatusOK))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AcceptTOS always returns true to indicate the acceptance of a CA's Terms of Service
|
|
||||||
// during account registration. See Register method of Client for more details.
|
|
||||||
func AcceptTOS(tosURL string) bool { return true }
|
|
||||||
|
|
||||||
// Register creates a new account registration by following the "new-reg" flow.
|
|
||||||
// It returns the registered account. The account is not modified.
|
|
||||||
//
|
|
||||||
// The registration may require the caller to agree to the CA's Terms of Service (TOS).
|
|
||||||
// If so, and the account has not indicated the acceptance of the terms (see Account for details),
|
|
||||||
// Register calls prompt with a TOS URL provided by the CA. Prompt should report
|
|
||||||
// whether the caller agrees to the terms. To always accept the terms, the caller can use AcceptTOS.
|
|
||||||
func (c *Client) Register(ctx context.Context, a *Account, prompt func(tosURL string) bool) (*Account, error) {
|
|
||||||
if _, err := c.Discover(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
if a, err = c.doReg(ctx, c.dir.RegURL, "new-reg", a); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var accept bool
|
|
||||||
if a.CurrentTerms != "" && a.CurrentTerms != a.AgreedTerms {
|
|
||||||
accept = prompt(a.CurrentTerms)
|
|
||||||
}
|
|
||||||
if accept {
|
|
||||||
a.AgreedTerms = a.CurrentTerms
|
|
||||||
a, err = c.UpdateReg(ctx, a)
|
|
||||||
}
|
|
||||||
return a, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetReg retrieves an existing registration.
|
|
||||||
// The url argument is an Account URI.
|
|
||||||
func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) {
|
|
||||||
a, err := c.doReg(ctx, url, "reg", nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
a.URI = url
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateReg updates an existing registration.
|
|
||||||
// It returns an updated account copy. The provided account is not modified.
|
|
||||||
func (c *Client) UpdateReg(ctx context.Context, a *Account) (*Account, error) {
|
|
||||||
uri := a.URI
|
|
||||||
a, err := c.doReg(ctx, uri, "reg", a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
a.URI = uri
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authorize performs the initial step in an authorization flow.
|
|
||||||
// The caller will then need to choose from and perform a set of returned
|
|
||||||
// challenges using c.Accept in order to successfully complete authorization.
|
|
||||||
//
|
|
||||||
// If an authorization has been previously granted, the CA may return
|
|
||||||
// a valid authorization (Authorization.Status is StatusValid). If so, the caller
|
|
||||||
// need not fulfill any challenge and can proceed to requesting a certificate.
|
|
||||||
func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization, error) {
|
|
||||||
if _, err := c.Discover(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type authzID struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
req := struct {
|
|
||||||
Resource string `json:"resource"`
|
|
||||||
Identifier authzID `json:"identifier"`
|
|
||||||
}{
|
|
||||||
Resource: "new-authz",
|
|
||||||
Identifier: authzID{Type: "dns", Value: domain},
|
|
||||||
}
|
|
||||||
res, err := c.post(ctx, c.Key, c.dir.AuthzURL, req, wantStatus(http.StatusCreated))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
var v wireAuthz
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
|
||||||
return nil, fmt.Errorf("acme: invalid response: %v", err)
|
|
||||||
}
|
|
||||||
if v.Status != StatusPending && v.Status != StatusValid {
|
|
||||||
return nil, fmt.Errorf("acme: unexpected status: %s", v.Status)
|
|
||||||
}
|
|
||||||
return v.authorization(res.Header.Get("Location")), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthorization retrieves an authorization identified by the given URL.
|
|
||||||
//
|
|
||||||
// If a caller needs to poll an authorization until its status is final,
|
|
||||||
// see the WaitAuthorization method.
|
|
||||||
func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorization, error) {
|
|
||||||
res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
var v wireAuthz
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
|
||||||
return nil, fmt.Errorf("acme: invalid response: %v", err)
|
|
||||||
}
|
|
||||||
return v.authorization(url), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RevokeAuthorization relinquishes an existing authorization identified
|
|
||||||
// by the given URL.
|
|
||||||
// The url argument is an Authorization.URI value.
|
|
||||||
//
|
|
||||||
// If successful, the caller will be required to obtain a new authorization
|
|
||||||
// using the Authorize method before being able to request a new certificate
|
|
||||||
// for the domain associated with the authorization.
|
|
||||||
//
|
|
||||||
// It does not revoke existing certificates.
|
|
||||||
func (c *Client) RevokeAuthorization(ctx context.Context, url string) error {
|
|
||||||
req := struct {
|
|
||||||
Resource string `json:"resource"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Delete bool `json:"delete"`
|
|
||||||
}{
|
|
||||||
Resource: "authz",
|
|
||||||
Status: "deactivated",
|
|
||||||
Delete: true,
|
|
||||||
}
|
|
||||||
res, err := c.post(ctx, c.Key, url, req, wantStatus(http.StatusOK))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitAuthorization polls an authorization at the given URL
|
|
||||||
// until it is in one of the final states, StatusValid or StatusInvalid,
|
|
||||||
// the ACME CA responded with a 4xx error code, or the context is done.
|
|
||||||
//
|
|
||||||
// It returns a non-nil Authorization only if its Status is StatusValid.
|
|
||||||
// In all other cases WaitAuthorization returns an error.
|
|
||||||
// If the Status is StatusInvalid, the returned error is of type *AuthorizationError.
|
|
||||||
func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) {
|
|
||||||
for {
|
|
||||||
res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var raw wireAuthz
|
|
||||||
err = json.NewDecoder(res.Body).Decode(&raw)
|
|
||||||
res.Body.Close()
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
// Skip and retry.
|
|
||||||
case raw.Status == StatusValid:
|
|
||||||
return raw.authorization(url), nil
|
|
||||||
case raw.Status == StatusInvalid:
|
|
||||||
return nil, raw.error(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exponential backoff is implemented in c.get above.
|
|
||||||
// This is just to prevent continuously hitting the CA
|
|
||||||
// while waiting for a final authorization status.
|
|
||||||
d := retryAfter(res.Header.Get("Retry-After"))
|
|
||||||
if d == 0 {
|
|
||||||
// Given that the fastest challenges TLS-SNI and HTTP-01
|
|
||||||
// require a CA to make at least 1 network round trip
|
|
||||||
// and most likely persist a challenge state,
|
|
||||||
// this default delay seems reasonable.
|
|
||||||
d = time.Second
|
|
||||||
}
|
|
||||||
t := time.NewTimer(d)
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
t.Stop()
|
|
||||||
return nil, ctx.Err()
|
|
||||||
case <-t.C:
|
|
||||||
// Retry.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetChallenge retrieves the current status of an challenge.
|
|
||||||
//
|
|
||||||
// A client typically polls a challenge status using this method.
|
|
||||||
func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, error) {
|
|
||||||
res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
v := wireChallenge{URI: url}
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
|
||||||
return nil, fmt.Errorf("acme: invalid response: %v", err)
|
|
||||||
}
|
|
||||||
return v.challenge(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept informs the server that the client accepts one of its challenges
|
|
||||||
// previously obtained with c.Authorize.
|
|
||||||
//
|
|
||||||
// The server will then perform the validation asynchronously.
|
|
||||||
func (c *Client) Accept(ctx context.Context, chal *Challenge) (*Challenge, error) {
|
|
||||||
auth, err := keyAuth(c.Key.Public(), chal.Token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req := struct {
|
|
||||||
Resource string `json:"resource"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Auth string `json:"keyAuthorization"`
|
|
||||||
}{
|
|
||||||
Resource: "challenge",
|
|
||||||
Type: chal.Type,
|
|
||||||
Auth: auth,
|
|
||||||
}
|
|
||||||
res, err := c.post(ctx, c.Key, chal.URI, req, wantStatus(
|
|
||||||
http.StatusOK, // according to the spec
|
|
||||||
http.StatusAccepted, // Let's Encrypt: see https://goo.gl/WsJ7VT (acme-divergences.md)
|
|
||||||
))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
var v wireChallenge
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
|
||||||
return nil, fmt.Errorf("acme: invalid response: %v", err)
|
|
||||||
}
|
|
||||||
return v.challenge(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNS01ChallengeRecord returns a DNS record value for a dns-01 challenge response.
|
|
||||||
// A TXT record containing the returned value must be provisioned under
|
|
||||||
// "_acme-challenge" name of the domain being validated.
|
|
||||||
//
|
|
||||||
// The token argument is a Challenge.Token value.
|
|
||||||
func (c *Client) DNS01ChallengeRecord(token string) (string, error) {
|
|
||||||
ka, err := keyAuth(c.Key.Public(), token)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
b := sha256.Sum256([]byte(ka))
|
|
||||||
return base64.RawURLEncoding.EncodeToString(b[:]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP01ChallengeResponse returns the response for an http-01 challenge.
|
|
||||||
// Servers should respond with the value to HTTP requests at the URL path
|
|
||||||
// provided by HTTP01ChallengePath to validate the challenge and prove control
|
|
||||||
// over a domain name.
|
|
||||||
//
|
|
||||||
// The token argument is a Challenge.Token value.
|
|
||||||
func (c *Client) HTTP01ChallengeResponse(token string) (string, error) {
|
|
||||||
return keyAuth(c.Key.Public(), token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP01ChallengePath returns the URL path at which the response for an http-01 challenge
|
|
||||||
// should be provided by the servers.
|
|
||||||
// The response value can be obtained with HTTP01ChallengeResponse.
|
|
||||||
//
|
|
||||||
// The token argument is a Challenge.Token value.
|
|
||||||
func (c *Client) HTTP01ChallengePath(token string) string {
|
|
||||||
return "/.well-known/acme-challenge/" + token
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLSSNI01ChallengeCert creates a certificate for TLS-SNI-01 challenge response.
|
|
||||||
// Servers can present the certificate to validate the challenge and prove control
|
|
||||||
// over a domain name.
|
|
||||||
//
|
|
||||||
// The implementation is incomplete in that the returned value is a single certificate,
|
|
||||||
// computed only for Z0 of the key authorization. ACME CAs are expected to update
|
|
||||||
// their implementations to use the newer version, TLS-SNI-02.
|
|
||||||
// For more details on TLS-SNI-01 see https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.3.
|
|
||||||
//
|
|
||||||
// The token argument is a Challenge.Token value.
|
|
||||||
// If a WithKey option is provided, its private part signs the returned cert,
|
|
||||||
// and the public part is used to specify the signee.
|
|
||||||
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
|
|
||||||
//
|
|
||||||
// The returned certificate is valid for the next 24 hours and must be presented only when
|
|
||||||
// the server name of the TLS ClientHello matches exactly the returned name value.
|
|
||||||
func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
|
|
||||||
ka, err := keyAuth(c.Key.Public(), token)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, "", err
|
|
||||||
}
|
|
||||||
b := sha256.Sum256([]byte(ka))
|
|
||||||
h := hex.EncodeToString(b[:])
|
|
||||||
name = fmt.Sprintf("%s.%s.acme.invalid", h[:32], h[32:])
|
|
||||||
cert, err = tlsChallengeCert([]string{name}, opt)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, "", err
|
|
||||||
}
|
|
||||||
return cert, name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLSSNI02ChallengeCert creates a certificate for TLS-SNI-02 challenge response.
|
|
||||||
// Servers can present the certificate to validate the challenge and prove control
|
|
||||||
// over a domain name. For more details on TLS-SNI-02 see
|
|
||||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3.
|
|
||||||
//
|
|
||||||
// The token argument is a Challenge.Token value.
|
|
||||||
// If a WithKey option is provided, its private part signs the returned cert,
|
|
||||||
// and the public part is used to specify the signee.
|
|
||||||
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
|
|
||||||
//
|
|
||||||
// The returned certificate is valid for the next 24 hours and must be presented only when
|
|
||||||
// the server name in the TLS ClientHello matches exactly the returned name value.
|
|
||||||
func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
|
|
||||||
b := sha256.Sum256([]byte(token))
|
|
||||||
h := hex.EncodeToString(b[:])
|
|
||||||
sanA := fmt.Sprintf("%s.%s.token.acme.invalid", h[:32], h[32:])
|
|
||||||
|
|
||||||
ka, err := keyAuth(c.Key.Public(), token)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, "", err
|
|
||||||
}
|
|
||||||
b = sha256.Sum256([]byte(ka))
|
|
||||||
h = hex.EncodeToString(b[:])
|
|
||||||
sanB := fmt.Sprintf("%s.%s.ka.acme.invalid", h[:32], h[32:])
|
|
||||||
|
|
||||||
cert, err = tlsChallengeCert([]string{sanA, sanB}, opt)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, "", err
|
|
||||||
}
|
|
||||||
return cert, sanA, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLSALPN01ChallengeCert creates a certificate for TLS-ALPN-01 challenge response.
|
|
||||||
// Servers can present the certificate to validate the challenge and prove control
|
|
||||||
// over a domain name. For more details on TLS-ALPN-01 see
|
|
||||||
// https://tools.ietf.org/html/draft-shoemaker-acme-tls-alpn-00#section-3
|
|
||||||
//
|
|
||||||
// The token argument is a Challenge.Token value.
|
|
||||||
// If a WithKey option is provided, its private part signs the returned cert,
|
|
||||||
// and the public part is used to specify the signee.
|
|
||||||
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
|
|
||||||
//
|
|
||||||
// The returned certificate is valid for the next 24 hours and must be presented only when
|
|
||||||
// the server name in the TLS ClientHello matches the domain, and the special acme-tls/1 ALPN protocol
|
|
||||||
// has been specified.
|
|
||||||
func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption) (cert tls.Certificate, err error) {
|
|
||||||
ka, err := keyAuth(c.Key.Public(), token)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, err
|
|
||||||
}
|
|
||||||
shasum := sha256.Sum256([]byte(ka))
|
|
||||||
extValue, err := asn1.Marshal(shasum[:])
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, err
|
|
||||||
}
|
|
||||||
acmeExtension := pkix.Extension{
|
|
||||||
Id: idPeACMEIdentifierV1,
|
|
||||||
Critical: true,
|
|
||||||
Value: extValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl := defaultTLSChallengeCertTemplate()
|
|
||||||
|
|
||||||
var newOpt []CertOption
|
|
||||||
for _, o := range opt {
|
|
||||||
switch o := o.(type) {
|
|
||||||
case *certOptTemplate:
|
|
||||||
t := *(*x509.Certificate)(o) // shallow copy is ok
|
|
||||||
tmpl = &t
|
|
||||||
default:
|
|
||||||
newOpt = append(newOpt, o)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tmpl.ExtraExtensions = append(tmpl.ExtraExtensions, acmeExtension)
|
|
||||||
newOpt = append(newOpt, WithTemplate(tmpl))
|
|
||||||
return tlsChallengeCert([]string{domain}, newOpt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// doReg sends all types of registration requests.
|
|
||||||
// The type of request is identified by typ argument, which is a "resource"
|
|
||||||
// in the ACME spec terms.
|
|
||||||
//
|
|
||||||
// A non-nil acct argument indicates whether the intention is to mutate data
|
|
||||||
// of the Account. Only Contact and Agreement of its fields are used
|
|
||||||
// in such cases.
|
|
||||||
func (c *Client) doReg(ctx context.Context, url string, typ string, acct *Account) (*Account, error) {
|
|
||||||
req := struct {
|
|
||||||
Resource string `json:"resource"`
|
|
||||||
Contact []string `json:"contact,omitempty"`
|
|
||||||
Agreement string `json:"agreement,omitempty"`
|
|
||||||
}{
|
|
||||||
Resource: typ,
|
|
||||||
}
|
|
||||||
if acct != nil {
|
|
||||||
req.Contact = acct.Contact
|
|
||||||
req.Agreement = acct.AgreedTerms
|
|
||||||
}
|
|
||||||
res, err := c.post(ctx, c.Key, url, req, wantStatus(
|
|
||||||
http.StatusOK, // updates and deletes
|
|
||||||
http.StatusCreated, // new account creation
|
|
||||||
http.StatusAccepted, // Let's Encrypt divergent implementation
|
|
||||||
))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
var v struct {
|
|
||||||
Contact []string
|
|
||||||
Agreement string
|
|
||||||
Authorizations string
|
|
||||||
Certificates string
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
|
|
||||||
return nil, fmt.Errorf("acme: invalid response: %v", err)
|
|
||||||
}
|
|
||||||
var tos string
|
|
||||||
if v := linkHeader(res.Header, "terms-of-service"); len(v) > 0 {
|
|
||||||
tos = v[0]
|
|
||||||
}
|
|
||||||
var authz string
|
|
||||||
if v := linkHeader(res.Header, "next"); len(v) > 0 {
|
|
||||||
authz = v[0]
|
|
||||||
}
|
|
||||||
return &Account{
|
|
||||||
URI: res.Header.Get("Location"),
|
|
||||||
Contact: v.Contact,
|
|
||||||
AgreedTerms: v.Agreement,
|
|
||||||
CurrentTerms: tos,
|
|
||||||
Authz: authz,
|
|
||||||
Authorizations: v.Authorizations,
|
|
||||||
Certificates: v.Certificates,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// popNonce returns a nonce value previously stored with c.addNonce
|
|
||||||
// or fetches a fresh one from the given URL.
|
|
||||||
func (c *Client) popNonce(ctx context.Context, url string) (string, error) {
|
|
||||||
c.noncesMu.Lock()
|
|
||||||
defer c.noncesMu.Unlock()
|
|
||||||
if len(c.nonces) == 0 {
|
|
||||||
return c.fetchNonce(ctx, url)
|
|
||||||
}
|
|
||||||
var nonce string
|
|
||||||
for nonce = range c.nonces {
|
|
||||||
delete(c.nonces, nonce)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return nonce, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// clearNonces clears any stored nonces
|
|
||||||
func (c *Client) clearNonces() {
|
|
||||||
c.noncesMu.Lock()
|
|
||||||
defer c.noncesMu.Unlock()
|
|
||||||
c.nonces = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// addNonce stores a nonce value found in h (if any) for future use.
|
|
||||||
func (c *Client) addNonce(h http.Header) {
|
|
||||||
v := nonceFromHeader(h)
|
|
||||||
if v == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.noncesMu.Lock()
|
|
||||||
defer c.noncesMu.Unlock()
|
|
||||||
if len(c.nonces) >= maxNonces {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.nonces == nil {
|
|
||||||
c.nonces = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
c.nonces[v] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) fetchNonce(ctx context.Context, url string) (string, error) {
|
|
||||||
r, err := http.NewRequest("HEAD", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
resp, err := c.doNoRetry(ctx, r)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
nonce := nonceFromHeader(resp.Header)
|
|
||||||
if nonce == "" {
|
|
||||||
if resp.StatusCode > 299 {
|
|
||||||
return "", responseError(resp)
|
|
||||||
}
|
|
||||||
return "", errors.New("acme: nonce not found")
|
|
||||||
}
|
|
||||||
return nonce, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func nonceFromHeader(h http.Header) string {
|
|
||||||
return h.Get("Replay-Nonce")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) responseCert(ctx context.Context, res *http.Response, bundle bool) ([][]byte, error) {
|
|
||||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, maxCertSize+1))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("acme: response stream: %v", err)
|
|
||||||
}
|
|
||||||
if len(b) > maxCertSize {
|
|
||||||
return nil, errors.New("acme: certificate is too big")
|
|
||||||
}
|
|
||||||
cert := [][]byte{b}
|
|
||||||
if !bundle {
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append CA chain cert(s).
|
|
||||||
// At least one is required according to the spec:
|
|
||||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-6.3.1
|
|
||||||
up := linkHeader(res.Header, "up")
|
|
||||||
if len(up) == 0 {
|
|
||||||
return nil, errors.New("acme: rel=up link not found")
|
|
||||||
}
|
|
||||||
if len(up) > maxChainLen {
|
|
||||||
return nil, errors.New("acme: rel=up link is too large")
|
|
||||||
}
|
|
||||||
for _, url := range up {
|
|
||||||
cc, err := c.chainCert(ctx, url, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cert = append(cert, cc...)
|
|
||||||
}
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// chainCert fetches CA certificate chain recursively by following "up" links.
|
|
||||||
// Each recursive call increments the depth by 1, resulting in an error
|
|
||||||
// if the recursion level reaches maxChainLen.
|
|
||||||
//
|
|
||||||
// First chainCert call starts with depth of 0.
|
|
||||||
func (c *Client) chainCert(ctx context.Context, url string, depth int) ([][]byte, error) {
|
|
||||||
if depth >= maxChainLen {
|
|
||||||
return nil, errors.New("acme: certificate chain is too deep")
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.get(ctx, url, wantStatus(http.StatusOK))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, maxCertSize+1))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(b) > maxCertSize {
|
|
||||||
return nil, errors.New("acme: certificate is too big")
|
|
||||||
}
|
|
||||||
chain := [][]byte{b}
|
|
||||||
|
|
||||||
uplink := linkHeader(res.Header, "up")
|
|
||||||
if len(uplink) > maxChainLen {
|
|
||||||
return nil, errors.New("acme: certificate chain is too large")
|
|
||||||
}
|
|
||||||
for _, up := range uplink {
|
|
||||||
cc, err := c.chainCert(ctx, up, depth+1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
chain = append(chain, cc...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return chain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// linkHeader returns URI-Reference values of all Link headers
|
|
||||||
// with relation-type rel.
|
|
||||||
// See https://tools.ietf.org/html/rfc5988#section-5 for details.
|
|
||||||
func linkHeader(h http.Header, rel string) []string {
|
|
||||||
var links []string
|
|
||||||
for _, v := range h["Link"] {
|
|
||||||
parts := strings.Split(v, ";")
|
|
||||||
for _, p := range parts {
|
|
||||||
p = strings.TrimSpace(p)
|
|
||||||
if !strings.HasPrefix(p, "rel=") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if v := strings.Trim(p[4:], `"`); v == rel {
|
|
||||||
links = append(links, strings.Trim(parts[0], "<>"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return links
|
|
||||||
}
|
|
||||||
|
|
||||||
// keyAuth generates a key authorization string for a given token.
|
|
||||||
func keyAuth(pub crypto.PublicKey, token string) (string, error) {
|
|
||||||
th, err := JWKThumbprint(pub)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s.%s", token, th), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultTLSChallengeCertTemplate is a template used to create challenge certs for TLS challenges.
|
|
||||||
func defaultTLSChallengeCertTemplate() *x509.Certificate {
|
|
||||||
return &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1),
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(24 * time.Hour),
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tlsChallengeCert creates a temporary certificate for TLS-SNI challenges
|
|
||||||
// with the given SANs and auto-generated public/private key pair.
|
|
||||||
// The Subject Common Name is set to the first SAN to aid debugging.
|
|
||||||
// To create a cert with a custom key pair, specify WithKey option.
|
|
||||||
func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
|
|
||||||
var key crypto.Signer
|
|
||||||
tmpl := defaultTLSChallengeCertTemplate()
|
|
||||||
for _, o := range opt {
|
|
||||||
switch o := o.(type) {
|
|
||||||
case *certOptKey:
|
|
||||||
if key != nil {
|
|
||||||
return tls.Certificate{}, errors.New("acme: duplicate key option")
|
|
||||||
}
|
|
||||||
key = o.key
|
|
||||||
case *certOptTemplate:
|
|
||||||
t := *(*x509.Certificate)(o) // shallow copy is ok
|
|
||||||
tmpl = &t
|
|
||||||
default:
|
|
||||||
// package's fault, if we let this happen:
|
|
||||||
panic(fmt.Sprintf("unsupported option type %T", o))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if key == nil {
|
|
||||||
var err error
|
|
||||||
if key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader); err != nil {
|
|
||||||
return tls.Certificate{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tmpl.DNSNames = san
|
|
||||||
if len(san) > 0 {
|
|
||||||
tmpl.Subject.CommonName = san[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
|
|
||||||
if err != nil {
|
|
||||||
return tls.Certificate{}, err
|
|
||||||
}
|
|
||||||
return tls.Certificate{
|
|
||||||
Certificate: [][]byte{der},
|
|
||||||
PrivateKey: key,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodePEM returns b encoded as PEM with block of type typ.
|
|
||||||
func encodePEM(typ string, b []byte) []byte {
|
|
||||||
pb := &pem.Block{Type: typ, Bytes: b}
|
|
||||||
return pem.EncodeToMemory(pb)
|
|
||||||
}
|
|
||||||
|
|
||||||
// timeNow is useful for testing for fixed current time.
|
|
||||||
var timeNow = time.Now
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,130 +0,0 @@
|
||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package autocert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrCacheMiss is returned when a certificate is not found in cache.
|
|
||||||
var ErrCacheMiss = errors.New("acme/autocert: certificate cache miss")
|
|
||||||
|
|
||||||
// Cache is used by Manager to store and retrieve previously obtained certificates
|
|
||||||
// and other account data as opaque blobs.
|
|
||||||
//
|
|
||||||
// Cache implementations should not rely on the key naming pattern. Keys can
|
|
||||||
// include any printable ASCII characters, except the following: \/:*?"<>|
|
|
||||||
type Cache interface {
|
|
||||||
// Get returns a certificate data for the specified key.
|
|
||||||
// If there's no such key, Get returns ErrCacheMiss.
|
|
||||||
Get(ctx context.Context, key string) ([]byte, error)
|
|
||||||
|
|
||||||
// Put stores the data in the cache under the specified key.
|
|
||||||
// Underlying implementations may use any data storage format,
|
|
||||||
// as long as the reverse operation, Get, results in the original data.
|
|
||||||
Put(ctx context.Context, key string, data []byte) error
|
|
||||||
|
|
||||||
// Delete removes a certificate data from the cache under the specified key.
|
|
||||||
// If there's no such key in the cache, Delete returns nil.
|
|
||||||
Delete(ctx context.Context, key string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// DirCache implements Cache using a directory on the local filesystem.
|
|
||||||
// If the directory does not exist, it will be created with 0700 permissions.
|
|
||||||
type DirCache string
|
|
||||||
|
|
||||||
// Get reads a certificate data from the specified file name.
|
|
||||||
func (d DirCache) Get(ctx context.Context, name string) ([]byte, error) {
|
|
||||||
name = filepath.Join(string(d), name)
|
|
||||||
var (
|
|
||||||
data []byte
|
|
||||||
err error
|
|
||||||
done = make(chan struct{})
|
|
||||||
)
|
|
||||||
go func() {
|
|
||||||
data, err = ioutil.ReadFile(name)
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
case <-done:
|
|
||||||
}
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, ErrCacheMiss
|
|
||||||
}
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put writes the certificate data to the specified file name.
|
|
||||||
// The file will be created with 0600 permissions.
|
|
||||||
func (d DirCache) Put(ctx context.Context, name string, data []byte) error {
|
|
||||||
if err := os.MkdirAll(string(d), 0700); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
var err error
|
|
||||||
go func() {
|
|
||||||
defer close(done)
|
|
||||||
var tmp string
|
|
||||||
if tmp, err = d.writeTempFile(name, data); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
// Don't overwrite the file if the context was canceled.
|
|
||||||
default:
|
|
||||||
newName := filepath.Join(string(d), name)
|
|
||||||
err = os.Rename(tmp, newName)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-done:
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the specified file name.
|
|
||||||
func (d DirCache) Delete(ctx context.Context, name string) error {
|
|
||||||
name = filepath.Join(string(d), name)
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
done = make(chan struct{})
|
|
||||||
)
|
|
||||||
go func() {
|
|
||||||
err = os.Remove(name)
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-done:
|
|
||||||
}
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeTempFile writes b to a temporary file, closes the file and returns its path.
|
|
||||||
func (d DirCache) writeTempFile(prefix string, b []byte) (string, error) {
|
|
||||||
// TempFile uses 0600 permissions
|
|
||||||
f, err := ioutil.TempFile(string(d), prefix)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if _, err := f.Write(b); err != nil {
|
|
||||||
f.Close()
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return f.Name(), f.Close()
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package autocert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// make sure DirCache satisfies Cache interface
|
|
||||||
var _ Cache = DirCache("/")
|
|
||||||
|
|
||||||
func TestDirCache(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "autocert")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
dir = filepath.Join(dir, "certs") // a nonexistent dir
|
|
||||||
cache := DirCache(dir)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// test cache miss
|
|
||||||
if _, err := cache.Get(ctx, "nonexistent"); err != ErrCacheMiss {
|
|
||||||
t.Errorf("get: %v; want ErrCacheMiss", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test put/get
|
|
||||||
b1 := []byte{1}
|
|
||||||
if err := cache.Put(ctx, "dummy", b1); err != nil {
|
|
||||||
t.Fatalf("put: %v", err)
|
|
||||||
}
|
|
||||||
b2, err := cache.Get(ctx, "dummy")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("get: %v", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(b1, b2) {
|
|
||||||
t.Errorf("b1 = %v; want %v", b1, b2)
|
|
||||||
}
|
|
||||||
name := filepath.Join(dir, "dummy")
|
|
||||||
if _, err := os.Stat(name); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// test delete
|
|
||||||
if err := cache.Delete(ctx, "dummy"); err != nil {
|
|
||||||
t.Fatalf("delete: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := cache.Get(ctx, "dummy"); err != ErrCacheMiss {
|
|
||||||
t.Errorf("get: %v; want ErrCacheMiss", err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
// Copyright 2017 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package autocert_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/acme/autocert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExampleNewListener() {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
fmt.Fprintf(w, "Hello, TLS user! Your config: %+v", r.TLS)
|
|
||||||
})
|
|
||||||
log.Fatal(http.Serve(autocert.NewListener("example.com"), mux))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleManager() {
|
|
||||||
m := &autocert.Manager{
|
|
||||||
Cache: autocert.DirCache("secret-dir"),
|
|
||||||
Prompt: autocert.AcceptTOS,
|
|
||||||
HostPolicy: autocert.HostWhitelist("example.org", "www.example.org"),
|
|
||||||
}
|
|
||||||
s := &http.Server{
|
|
||||||
Addr: ":https",
|
|
||||||
TLSConfig: m.TLSConfig(),
|
|
||||||
}
|
|
||||||
s.ListenAndServeTLS("", "")
|
|
||||||
}
|
|
|
@ -1,416 +0,0 @@
|
||||||
// Copyright 2018 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package acmetest provides types for testing acme and autocert packages.
|
|
||||||
//
|
|
||||||
// TODO: Consider moving this to x/crypto/acme/internal/acmetest for acme tests as well.
|
|
||||||
package acmetest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/big"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CAServer is a simple test server which implements ACME spec bits needed for testing.
|
|
||||||
type CAServer struct {
|
|
||||||
URL string // server URL after it has been started
|
|
||||||
Roots *x509.CertPool // CA root certificates; initialized in NewCAServer
|
|
||||||
|
|
||||||
rootKey crypto.Signer
|
|
||||||
rootCert []byte // DER encoding
|
|
||||||
rootTemplate *x509.Certificate
|
|
||||||
|
|
||||||
server *httptest.Server
|
|
||||||
challengeTypes []string // supported challenge types
|
|
||||||
domainsWhitelist []string // only these domains are valid for issuing, unless empty
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
certCount int // number of issued certs
|
|
||||||
domainAddr map[string]string // domain name to addr:port resolution
|
|
||||||
authorizations map[string]*authorization // keyed by domain name
|
|
||||||
errors []error // encountered client errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCAServer creates a new ACME test server and starts serving requests.
|
|
||||||
// The returned CAServer issues certs signed with the CA roots
|
|
||||||
// available in the Roots field.
|
|
||||||
//
|
|
||||||
// The challengeTypes argument defines the supported ACME challenge types
|
|
||||||
// sent to a client in a response for a domain authorization.
|
|
||||||
// If domainsWhitelist is non-empty, the certs will be issued only for the specified
|
|
||||||
// list of domains. Otherwise, any domain name is allowed.
|
|
||||||
func NewCAServer(challengeTypes []string, domainsWhitelist []string) *CAServer {
|
|
||||||
var whitelist []string
|
|
||||||
for _, name := range domainsWhitelist {
|
|
||||||
whitelist = append(whitelist, name)
|
|
||||||
}
|
|
||||||
sort.Strings(whitelist)
|
|
||||||
ca := &CAServer{
|
|
||||||
challengeTypes: challengeTypes,
|
|
||||||
domainsWhitelist: whitelist,
|
|
||||||
domainAddr: make(map[string]string),
|
|
||||||
authorizations: make(map[string]*authorization),
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("ecdsa.GenerateKey: %v", err))
|
|
||||||
}
|
|
||||||
tmpl := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1),
|
|
||||||
Subject: pkix.Name{
|
|
||||||
Organization: []string{"Test Acme Co"},
|
|
||||||
CommonName: "Root CA",
|
|
||||||
},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
|
||||||
KeyUsage: x509.KeyUsageCertSign,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
IsCA: true,
|
|
||||||
}
|
|
||||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("x509.CreateCertificate: %v", err))
|
|
||||||
}
|
|
||||||
cert, err := x509.ParseCertificate(der)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("x509.ParseCertificate: %v", err))
|
|
||||||
}
|
|
||||||
ca.Roots = x509.NewCertPool()
|
|
||||||
ca.Roots.AddCert(cert)
|
|
||||||
ca.rootKey = key
|
|
||||||
ca.rootCert = der
|
|
||||||
ca.rootTemplate = tmpl
|
|
||||||
|
|
||||||
ca.server = httptest.NewServer(http.HandlerFunc(ca.handle))
|
|
||||||
ca.URL = ca.server.URL
|
|
||||||
return ca
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close shuts down the server and blocks until all outstanding
|
|
||||||
// requests on this server have completed.
|
|
||||||
func (ca *CAServer) Close() {
|
|
||||||
ca.server.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errors returns all client errors.
|
|
||||||
func (ca *CAServer) Errors() []error {
|
|
||||||
ca.mu.Lock()
|
|
||||||
defer ca.mu.Unlock()
|
|
||||||
return ca.errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve adds a domain to address resolution for the ca to dial to
|
|
||||||
// when validating challenges for the domain authorization.
|
|
||||||
func (ca *CAServer) Resolve(domain, addr string) {
|
|
||||||
ca.mu.Lock()
|
|
||||||
defer ca.mu.Unlock()
|
|
||||||
ca.domainAddr[domain] = addr
|
|
||||||
}
|
|
||||||
|
|
||||||
type discovery struct {
|
|
||||||
NewReg string `json:"new-reg"`
|
|
||||||
NewAuthz string `json:"new-authz"`
|
|
||||||
NewCert string `json:"new-cert"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type challenge struct {
|
|
||||||
URI string `json:"uri"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type authorization struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Challenges []challenge `json:"challenges"`
|
|
||||||
|
|
||||||
id int
|
|
||||||
domain string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Replay-Nonce", "nonce")
|
|
||||||
if r.Method == "HEAD" {
|
|
||||||
// a nonce request
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Verify nonce header for all POST requests.
|
|
||||||
|
|
||||||
switch {
|
|
||||||
default:
|
|
||||||
err := fmt.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
|
|
||||||
ca.addError(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
|
|
||||||
// Discovery request.
|
|
||||||
case r.URL.Path == "/":
|
|
||||||
resp := &discovery{
|
|
||||||
NewReg: ca.serverURL("/new-reg"),
|
|
||||||
NewAuthz: ca.serverURL("/new-authz"),
|
|
||||||
NewCert: ca.serverURL("/new-cert"),
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
||||||
panic(fmt.Sprintf("discovery response: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client key registration request.
|
|
||||||
case r.URL.Path == "/new-reg":
|
|
||||||
// TODO: Check the user account key against a ca.accountKeys?
|
|
||||||
w.Write([]byte("{}"))
|
|
||||||
|
|
||||||
// Domain authorization request.
|
|
||||||
case r.URL.Path == "/new-authz":
|
|
||||||
var req struct {
|
|
||||||
Identifier struct{ Value string }
|
|
||||||
}
|
|
||||||
if err := decodePayload(&req, r.Body); err != nil {
|
|
||||||
ca.addError(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ca.mu.Lock()
|
|
||||||
defer ca.mu.Unlock()
|
|
||||||
authz, ok := ca.authorizations[req.Identifier.Value]
|
|
||||||
if !ok {
|
|
||||||
authz = &authorization{
|
|
||||||
domain: req.Identifier.Value,
|
|
||||||
Status: "pending",
|
|
||||||
}
|
|
||||||
for _, typ := range ca.challengeTypes {
|
|
||||||
authz.Challenges = append(authz.Challenges, challenge{
|
|
||||||
Type: typ,
|
|
||||||
URI: ca.serverURL("/challenge/%s/%s", typ, authz.domain),
|
|
||||||
Token: challengeToken(authz.domain, typ),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ca.authorizations[authz.domain] = authz
|
|
||||||
}
|
|
||||||
w.Header().Set("Location", ca.serverURL("/authz/%s", authz.domain))
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
if err := json.NewEncoder(w).Encode(authz); err != nil {
|
|
||||||
panic(fmt.Sprintf("new authz response: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept tls-alpn-01 challenge type requests.
|
|
||||||
// TODO: Add http-01 and dns-01 handlers.
|
|
||||||
case strings.HasPrefix(r.URL.Path, "/challenge/tls-alpn-01/"):
|
|
||||||
domain := strings.TrimPrefix(r.URL.Path, "/challenge/tls-alpn-01/")
|
|
||||||
ca.mu.Lock()
|
|
||||||
defer ca.mu.Unlock()
|
|
||||||
if _, ok := ca.authorizations[domain]; !ok {
|
|
||||||
err := fmt.Errorf("challenge accept: no authz for %q", domain)
|
|
||||||
ca.addError(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go func(domain string) {
|
|
||||||
err := ca.verifyALPNChallenge(domain)
|
|
||||||
ca.mu.Lock()
|
|
||||||
defer ca.mu.Unlock()
|
|
||||||
authz := ca.authorizations[domain]
|
|
||||||
if err != nil {
|
|
||||||
authz.Status = "invalid"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
authz.Status = "valid"
|
|
||||||
|
|
||||||
}(domain)
|
|
||||||
w.Write([]byte("{}"))
|
|
||||||
|
|
||||||
// Get authorization status requests.
|
|
||||||
case strings.HasPrefix(r.URL.Path, "/authz/"):
|
|
||||||
domain := strings.TrimPrefix(r.URL.Path, "/authz/")
|
|
||||||
ca.mu.Lock()
|
|
||||||
defer ca.mu.Unlock()
|
|
||||||
authz, ok := ca.authorizations[domain]
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, fmt.Sprintf("no authz for %q", domain), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(w).Encode(authz); err != nil {
|
|
||||||
panic(fmt.Sprintf("get authz for %q response: %v", domain, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cert issuance request.
|
|
||||||
case r.URL.Path == "/new-cert":
|
|
||||||
var req struct {
|
|
||||||
CSR string `json:"csr"`
|
|
||||||
}
|
|
||||||
decodePayload(&req, r.Body)
|
|
||||||
b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
|
|
||||||
csr, err := x509.ParseCertificateRequest(b)
|
|
||||||
if err != nil {
|
|
||||||
ca.addError(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
names := unique(append(csr.DNSNames, csr.Subject.CommonName))
|
|
||||||
if err := ca.matchWhitelist(names); err != nil {
|
|
||||||
ca.addError(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := ca.authorized(names); err != nil {
|
|
||||||
ca.addError(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
der, err := ca.leafCert(csr)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("new-cert response: ca.leafCert: %v", err)
|
|
||||||
ca.addError(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
w.Header().Set("Link", fmt.Sprintf("<%s>; rel=up", ca.serverURL("/ca-cert")))
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
w.Write(der)
|
|
||||||
|
|
||||||
// CA chain cert request.
|
|
||||||
case r.URL.Path == "/ca-cert":
|
|
||||||
w.Write(ca.rootCert)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ca *CAServer) addError(err error) {
|
|
||||||
ca.mu.Lock()
|
|
||||||
defer ca.mu.Unlock()
|
|
||||||
ca.errors = append(ca.errors, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ca *CAServer) serverURL(format string, arg ...interface{}) string {
|
|
||||||
return ca.server.URL + fmt.Sprintf(format, arg...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ca *CAServer) matchWhitelist(dnsNames []string) error {
|
|
||||||
if len(ca.domainsWhitelist) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var nomatch []string
|
|
||||||
for _, name := range dnsNames {
|
|
||||||
i := sort.SearchStrings(ca.domainsWhitelist, name)
|
|
||||||
if i == len(ca.domainsWhitelist) || ca.domainsWhitelist[i] != name {
|
|
||||||
nomatch = append(nomatch, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(nomatch) > 0 {
|
|
||||||
return fmt.Errorf("matchWhitelist: some domains don't match: %q", nomatch)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ca *CAServer) authorized(dnsNames []string) error {
|
|
||||||
ca.mu.Lock()
|
|
||||||
defer ca.mu.Unlock()
|
|
||||||
var noauthz []string
|
|
||||||
for _, name := range dnsNames {
|
|
||||||
authz, ok := ca.authorizations[name]
|
|
||||||
if !ok || authz.Status != "valid" {
|
|
||||||
noauthz = append(noauthz, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(noauthz) > 0 {
|
|
||||||
return fmt.Errorf("CAServer: no authz for %q", noauthz)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err error) {
|
|
||||||
ca.mu.Lock()
|
|
||||||
defer ca.mu.Unlock()
|
|
||||||
ca.certCount++ // next leaf cert serial number
|
|
||||||
leaf := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(int64(ca.certCount)),
|
|
||||||
Subject: pkix.Name{Organization: []string{"Test Acme Co"}},
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
DNSNames: csr.DNSNames,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
}
|
|
||||||
if len(csr.DNSNames) == 0 {
|
|
||||||
leaf.DNSNames = []string{csr.Subject.CommonName}
|
|
||||||
}
|
|
||||||
return x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, csr.PublicKey, ca.rootKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ca *CAServer) addr(domain string) (string, error) {
|
|
||||||
ca.mu.Lock()
|
|
||||||
defer ca.mu.Unlock()
|
|
||||||
addr, ok := ca.domainAddr[domain]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("CAServer: no addr resolution for %q", domain)
|
|
||||||
}
|
|
||||||
return addr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ca *CAServer) verifyALPNChallenge(domain string) error {
|
|
||||||
const acmeALPNProto = "acme-tls/1"
|
|
||||||
|
|
||||||
addr, err := ca.addr(domain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
conn, err := tls.Dial("tcp", addr, &tls.Config{
|
|
||||||
ServerName: domain,
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
NextProtos: []string{acmeALPNProto},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto {
|
|
||||||
return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto)
|
|
||||||
}
|
|
||||||
if n := len(conn.ConnectionState().PeerCertificates); n != 1 {
|
|
||||||
return fmt.Errorf("len(PeerCertificates) = %d; want 1", n)
|
|
||||||
}
|
|
||||||
// TODO: verify conn.ConnectionState().PeerCertificates[0]
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodePayload(v interface{}, r io.Reader) error {
|
|
||||||
var req struct{ Payload string }
|
|
||||||
if err := json.NewDecoder(r).Decode(&req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
payload, err := base64.RawURLEncoding.DecodeString(req.Payload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return json.Unmarshal(payload, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func challengeToken(domain, challType string) string {
|
|
||||||
return fmt.Sprintf("token-%s-%s", domain, challType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unique(a []string) []string {
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
var res []string
|
|
||||||
for _, s := range a {
|
|
||||||
if s != "" && !seen[s] {
|
|
||||||
seen[s] = true
|
|
||||||
res = append(res, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
// Copyright 2017 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package autocert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewListener returns a net.Listener that listens on the standard TLS
|
|
||||||
// port (443) on all interfaces and returns *tls.Conn connections with
|
|
||||||
// LetsEncrypt certificates for the provided domain or domains.
|
|
||||||
//
|
|
||||||
// It enables one-line HTTPS servers:
|
|
||||||
//
|
|
||||||
// log.Fatal(http.Serve(autocert.NewListener("example.com"), handler))
|
|
||||||
//
|
|
||||||
// NewListener is a convenience function for a common configuration.
|
|
||||||
// More complex or custom configurations can use the autocert.Manager
|
|
||||||
// type instead.
|
|
||||||
//
|
|
||||||
// Use of this function implies acceptance of the LetsEncrypt Terms of
|
|
||||||
// Service. If domains is not empty, the provided domains are passed
|
|
||||||
// to HostWhitelist. If domains is empty, the listener will do
|
|
||||||
// LetsEncrypt challenges for any requested domain, which is not
|
|
||||||
// recommended.
|
|
||||||
//
|
|
||||||
// Certificates are cached in a "golang-autocert" directory under an
|
|
||||||
// operating system-specific cache or temp directory. This may not
|
|
||||||
// be suitable for servers spanning multiple machines.
|
|
||||||
//
|
|
||||||
// The returned listener uses a *tls.Config that enables HTTP/2, and
|
|
||||||
// should only be used with servers that support HTTP/2.
|
|
||||||
//
|
|
||||||
// The returned Listener also enables TCP keep-alives on the accepted
|
|
||||||
// connections. The returned *tls.Conn are returned before their TLS
|
|
||||||
// handshake has completed.
|
|
||||||
func NewListener(domains ...string) net.Listener {
|
|
||||||
m := &Manager{
|
|
||||||
Prompt: AcceptTOS,
|
|
||||||
}
|
|
||||||
if len(domains) > 0 {
|
|
||||||
m.HostPolicy = HostWhitelist(domains...)
|
|
||||||
}
|
|
||||||
dir := cacheDir()
|
|
||||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
||||||
log.Printf("warning: autocert.NewListener not using a cache: %v", err)
|
|
||||||
} else {
|
|
||||||
m.Cache = DirCache(dir)
|
|
||||||
}
|
|
||||||
return m.Listener()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listener listens on the standard TLS port (443) on all interfaces
|
|
||||||
// and returns a net.Listener returning *tls.Conn connections.
|
|
||||||
//
|
|
||||||
// The returned listener uses a *tls.Config that enables HTTP/2, and
|
|
||||||
// should only be used with servers that support HTTP/2.
|
|
||||||
//
|
|
||||||
// The returned Listener also enables TCP keep-alives on the accepted
|
|
||||||
// connections. The returned *tls.Conn are returned before their TLS
|
|
||||||
// handshake has completed.
|
|
||||||
//
|
|
||||||
// Unlike NewListener, it is the caller's responsibility to initialize
|
|
||||||
// the Manager m's Prompt, Cache, HostPolicy, and other desired options.
|
|
||||||
func (m *Manager) Listener() net.Listener {
|
|
||||||
ln := &listener{
|
|
||||||
m: m,
|
|
||||||
conf: m.TLSConfig(),
|
|
||||||
}
|
|
||||||
ln.tcpListener, ln.tcpListenErr = net.Listen("tcp", ":443")
|
|
||||||
return ln
|
|
||||||
}
|
|
||||||
|
|
||||||
type listener struct {
|
|
||||||
m *Manager
|
|
||||||
conf *tls.Config
|
|
||||||
|
|
||||||
tcpListener net.Listener
|
|
||||||
tcpListenErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *listener) Accept() (net.Conn, error) {
|
|
||||||
if ln.tcpListenErr != nil {
|
|
||||||
return nil, ln.tcpListenErr
|
|
||||||
}
|
|
||||||
conn, err := ln.tcpListener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tcpConn := conn.(*net.TCPConn)
|
|
||||||
|
|
||||||
// Because Listener is a convenience function, help out with
|
|
||||||
// this too. This is not possible for the caller to set once
|
|
||||||
// we return a *tcp.Conn wrapping an inaccessible net.Conn.
|
|
||||||
// If callers don't want this, they can do things the manual
|
|
||||||
// way and tweak as needed. But this is what net/http does
|
|
||||||
// itself, so copy that. If net/http changes, we can change
|
|
||||||
// here too.
|
|
||||||
tcpConn.SetKeepAlive(true)
|
|
||||||
tcpConn.SetKeepAlivePeriod(3 * time.Minute)
|
|
||||||
|
|
||||||
return tls.Server(tcpConn, ln.conf), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *listener) Addr() net.Addr {
|
|
||||||
if ln.tcpListener != nil {
|
|
||||||
return ln.tcpListener.Addr()
|
|
||||||
}
|
|
||||||
// net.Listen failed. Return something non-nil in case callers
|
|
||||||
// call Addr before Accept:
|
|
||||||
return &net.TCPAddr{IP: net.IP{0, 0, 0, 0}, Port: 443}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *listener) Close() error {
|
|
||||||
if ln.tcpListenErr != nil {
|
|
||||||
return ln.tcpListenErr
|
|
||||||
}
|
|
||||||
return ln.tcpListener.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func homeDir() string {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
|
||||||
}
|
|
||||||
if h := os.Getenv("HOME"); h != "" {
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
return "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
func cacheDir() string {
|
|
||||||
const base = "golang-autocert"
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "darwin":
|
|
||||||
return filepath.Join(homeDir(), "Library", "Caches", base)
|
|
||||||
case "windows":
|
|
||||||
for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} {
|
|
||||||
if v := os.Getenv(ev); v != "" {
|
|
||||||
return filepath.Join(v, base)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Worst case:
|
|
||||||
return filepath.Join(homeDir(), base)
|
|
||||||
}
|
|
||||||
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
|
|
||||||
return filepath.Join(xdg, base)
|
|
||||||
}
|
|
||||||
return filepath.Join(homeDir(), ".cache", base)
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package autocert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// renewJitter is the maximum deviation from Manager.RenewBefore.
|
|
||||||
const renewJitter = time.Hour
|
|
||||||
|
|
||||||
// domainRenewal tracks the state used by the periodic timers
|
|
||||||
// renewing a single domain's cert.
|
|
||||||
type domainRenewal struct {
|
|
||||||
m *Manager
|
|
||||||
ck certKey
|
|
||||||
key crypto.Signer
|
|
||||||
|
|
||||||
timerMu sync.Mutex
|
|
||||||
timer *time.Timer
|
|
||||||
}
|
|
||||||
|
|
||||||
// start starts a cert renewal timer at the time
|
|
||||||
// defined by the certificate expiration time exp.
|
|
||||||
//
|
|
||||||
// If the timer is already started, calling start is a noop.
|
|
||||||
func (dr *domainRenewal) start(exp time.Time) {
|
|
||||||
dr.timerMu.Lock()
|
|
||||||
defer dr.timerMu.Unlock()
|
|
||||||
if dr.timer != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dr.timer = time.AfterFunc(dr.next(exp), dr.renew)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop stops the cert renewal timer.
|
|
||||||
// If the timer is already stopped, calling stop is a noop.
|
|
||||||
func (dr *domainRenewal) stop() {
|
|
||||||
dr.timerMu.Lock()
|
|
||||||
defer dr.timerMu.Unlock()
|
|
||||||
if dr.timer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dr.timer.Stop()
|
|
||||||
dr.timer = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// renew is called periodically by a timer.
|
|
||||||
// The first renew call is kicked off by dr.start.
|
|
||||||
func (dr *domainRenewal) renew() {
|
|
||||||
dr.timerMu.Lock()
|
|
||||||
defer dr.timerMu.Unlock()
|
|
||||||
if dr.timer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
// TODO: rotate dr.key at some point?
|
|
||||||
next, err := dr.do(ctx)
|
|
||||||
if err != nil {
|
|
||||||
next = renewJitter / 2
|
|
||||||
next += time.Duration(pseudoRand.int63n(int64(next)))
|
|
||||||
}
|
|
||||||
dr.timer = time.AfterFunc(next, dr.renew)
|
|
||||||
testDidRenewLoop(next, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateState locks and replaces the relevant Manager.state item with the given
|
|
||||||
// state. It additionally updates dr.key with the given state's key.
|
|
||||||
func (dr *domainRenewal) updateState(state *certState) {
|
|
||||||
dr.m.stateMu.Lock()
|
|
||||||
defer dr.m.stateMu.Unlock()
|
|
||||||
dr.key = state.key
|
|
||||||
dr.m.state[dr.ck] = state
|
|
||||||
}
|
|
||||||
|
|
||||||
// do is similar to Manager.createCert but it doesn't lock a Manager.state item.
|
|
||||||
// Instead, it requests a new certificate independently and, upon success,
|
|
||||||
// replaces dr.m.state item with a new one and updates cache for the given domain.
|
|
||||||
//
|
|
||||||
// It may lock and update the Manager.state if the expiration date of the currently
|
|
||||||
// cached cert is far enough in the future.
|
|
||||||
//
|
|
||||||
// The returned value is a time interval after which the renewal should occur again.
|
|
||||||
func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
|
|
||||||
// a race is likely unavoidable in a distributed environment
|
|
||||||
// but we try nonetheless
|
|
||||||
if tlscert, err := dr.m.cacheGet(ctx, dr.ck); err == nil {
|
|
||||||
next := dr.next(tlscert.Leaf.NotAfter)
|
|
||||||
if next > dr.m.renewBefore()+renewJitter {
|
|
||||||
signer, ok := tlscert.PrivateKey.(crypto.Signer)
|
|
||||||
if ok {
|
|
||||||
state := &certState{
|
|
||||||
key: signer,
|
|
||||||
cert: tlscert.Certificate,
|
|
||||||
leaf: tlscert.Leaf,
|
|
||||||
}
|
|
||||||
dr.updateState(state)
|
|
||||||
return next, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
der, leaf, err := dr.m.authorizedCert(ctx, dr.key, dr.ck)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
state := &certState{
|
|
||||||
key: dr.key,
|
|
||||||
cert: der,
|
|
||||||
leaf: leaf,
|
|
||||||
}
|
|
||||||
tlscert, err := state.tlscert()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if err := dr.m.cachePut(ctx, dr.ck, tlscert); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
dr.updateState(state)
|
|
||||||
return dr.next(leaf.NotAfter), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dr *domainRenewal) next(expiry time.Time) time.Duration {
|
|
||||||
d := expiry.Sub(dr.m.now()) - dr.m.renewBefore()
|
|
||||||
// add a bit of randomness to renew deadline
|
|
||||||
n := pseudoRand.int63n(int64(renewJitter))
|
|
||||||
d -= time.Duration(n)
|
|
||||||
if d < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
var testDidRenewLoop = func(next time.Duration, err error) {}
|
|
|
@ -1,329 +0,0 @@
|
||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package autocert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/acme"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRenewalNext(t *testing.T) {
|
|
||||||
now := time.Now()
|
|
||||||
man := &Manager{
|
|
||||||
RenewBefore: 7 * 24 * time.Hour,
|
|
||||||
nowFunc: func() time.Time { return now },
|
|
||||||
}
|
|
||||||
defer man.stopRenew()
|
|
||||||
tt := []struct {
|
|
||||||
expiry time.Time
|
|
||||||
min, max time.Duration
|
|
||||||
}{
|
|
||||||
{now.Add(90 * 24 * time.Hour), 83*24*time.Hour - renewJitter, 83 * 24 * time.Hour},
|
|
||||||
{now.Add(time.Hour), 0, 1},
|
|
||||||
{now, 0, 1},
|
|
||||||
{now.Add(-time.Hour), 0, 1},
|
|
||||||
}
|
|
||||||
|
|
||||||
dr := &domainRenewal{m: man}
|
|
||||||
for i, test := range tt {
|
|
||||||
next := dr.next(test.expiry)
|
|
||||||
if next < test.min || test.max < next {
|
|
||||||
t.Errorf("%d: next = %v; want between %v and %v", i, next, test.min, test.max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenewFromCache(t *testing.T) {
|
|
||||||
// ACME CA server stub
|
|
||||||
var ca *httptest.Server
|
|
||||||
ca = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Replay-Nonce", "nonce")
|
|
||||||
if r.Method == "HEAD" {
|
|
||||||
// a nonce request
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.URL.Path {
|
|
||||||
// discovery
|
|
||||||
case "/":
|
|
||||||
if err := discoTmpl.Execute(w, ca.URL); err != nil {
|
|
||||||
t.Fatalf("discoTmpl: %v", err)
|
|
||||||
}
|
|
||||||
// client key registration
|
|
||||||
case "/new-reg":
|
|
||||||
w.Write([]byte("{}"))
|
|
||||||
// domain authorization
|
|
||||||
case "/new-authz":
|
|
||||||
w.Header().Set("Location", ca.URL+"/authz/1")
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
w.Write([]byte(`{"status": "valid"}`))
|
|
||||||
// cert request
|
|
||||||
case "/new-cert":
|
|
||||||
var req struct {
|
|
||||||
CSR string `json:"csr"`
|
|
||||||
}
|
|
||||||
decodePayload(&req, r.Body)
|
|
||||||
b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
|
|
||||||
csr, err := x509.ParseCertificateRequest(b)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("new-cert: CSR: %v", err)
|
|
||||||
}
|
|
||||||
der, err := dummyCert(csr.PublicKey, exampleDomain)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("new-cert: dummyCert: %v", err)
|
|
||||||
}
|
|
||||||
chainUp := fmt.Sprintf("<%s/ca-cert>; rel=up", ca.URL)
|
|
||||||
w.Header().Set("Link", chainUp)
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
w.Write(der)
|
|
||||||
// CA chain cert
|
|
||||||
case "/ca-cert":
|
|
||||||
der, err := dummyCert(nil, "ca")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ca-cert: dummyCert: %v", err)
|
|
||||||
}
|
|
||||||
w.Write(der)
|
|
||||||
default:
|
|
||||||
t.Errorf("unrecognized r.URL.Path: %s", r.URL.Path)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer ca.Close()
|
|
||||||
|
|
||||||
man := &Manager{
|
|
||||||
Prompt: AcceptTOS,
|
|
||||||
Cache: newMemCache(t),
|
|
||||||
RenewBefore: 24 * time.Hour,
|
|
||||||
Client: &acme.Client{
|
|
||||||
DirectoryURL: ca.URL,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
defer man.stopRenew()
|
|
||||||
|
|
||||||
// cache an almost expired cert
|
|
||||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
cert, err := dateDummyCert(key.Public(), now.Add(-2*time.Hour), now.Add(time.Minute), exampleDomain)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
tlscert := &tls.Certificate{PrivateKey: key, Certificate: [][]byte{cert}}
|
|
||||||
if err := man.cachePut(context.Background(), exampleCertKey, tlscert); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// veriy the renewal happened
|
|
||||||
defer func() {
|
|
||||||
testDidRenewLoop = func(next time.Duration, err error) {}
|
|
||||||
}()
|
|
||||||
done := make(chan struct{})
|
|
||||||
testDidRenewLoop = func(next time.Duration, err error) {
|
|
||||||
defer close(done)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("testDidRenewLoop: %v", err)
|
|
||||||
}
|
|
||||||
// Next should be about 90 days:
|
|
||||||
// dummyCert creates 90days expiry + account for man.RenewBefore.
|
|
||||||
// Previous expiration was within 1 min.
|
|
||||||
future := 88 * 24 * time.Hour
|
|
||||||
if next < future {
|
|
||||||
t.Errorf("testDidRenewLoop: next = %v; want >= %v", next, future)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the new cert is cached
|
|
||||||
after := time.Now().Add(future)
|
|
||||||
tlscert, err := man.cacheGet(context.Background(), exampleCertKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("man.cacheGet: %v", err)
|
|
||||||
}
|
|
||||||
if !tlscert.Leaf.NotAfter.After(after) {
|
|
||||||
t.Errorf("cache leaf.NotAfter = %v; want > %v", tlscert.Leaf.NotAfter, after)
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify the old cert is also replaced in memory
|
|
||||||
man.stateMu.Lock()
|
|
||||||
defer man.stateMu.Unlock()
|
|
||||||
s := man.state[exampleCertKey]
|
|
||||||
if s == nil {
|
|
||||||
t.Fatalf("m.state[%q] is nil", exampleCertKey)
|
|
||||||
}
|
|
||||||
tlscert, err = s.tlscert()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("s.tlscert: %v", err)
|
|
||||||
}
|
|
||||||
if !tlscert.Leaf.NotAfter.After(after) {
|
|
||||||
t.Errorf("state leaf.NotAfter = %v; want > %v", tlscert.Leaf.NotAfter, after)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger renew
|
|
||||||
hello := clientHelloInfo(exampleDomain, true)
|
|
||||||
if _, err := man.GetCertificate(hello); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for renew loop
|
|
||||||
select {
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
t.Fatal("renew took too long to occur")
|
|
||||||
case <-done:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenewFromCacheAlreadyRenewed(t *testing.T) {
|
|
||||||
man := &Manager{
|
|
||||||
Prompt: AcceptTOS,
|
|
||||||
Cache: newMemCache(t),
|
|
||||||
RenewBefore: 24 * time.Hour,
|
|
||||||
Client: &acme.Client{
|
|
||||||
DirectoryURL: "invalid",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
defer man.stopRenew()
|
|
||||||
|
|
||||||
// cache a recently renewed cert with a different private key
|
|
||||||
newKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
newCert, err := dateDummyCert(newKey.Public(), now.Add(-2*time.Hour), now.Add(time.Hour*24*90), exampleDomain)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
newLeaf, err := validCert(exampleCertKey, [][]byte{newCert}, newKey, now)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
newTLSCert := &tls.Certificate{PrivateKey: newKey, Certificate: [][]byte{newCert}, Leaf: newLeaf}
|
|
||||||
if err := man.cachePut(context.Background(), exampleCertKey, newTLSCert); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set internal state to an almost expired cert
|
|
||||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
oldCert, err := dateDummyCert(key.Public(), now.Add(-2*time.Hour), now.Add(time.Minute), exampleDomain)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
oldLeaf, err := validCert(exampleCertKey, [][]byte{oldCert}, key, now)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
man.stateMu.Lock()
|
|
||||||
if man.state == nil {
|
|
||||||
man.state = make(map[certKey]*certState)
|
|
||||||
}
|
|
||||||
s := &certState{
|
|
||||||
key: key,
|
|
||||||
cert: [][]byte{oldCert},
|
|
||||||
leaf: oldLeaf,
|
|
||||||
}
|
|
||||||
man.state[exampleCertKey] = s
|
|
||||||
man.stateMu.Unlock()
|
|
||||||
|
|
||||||
// veriy the renewal accepted the newer cached cert
|
|
||||||
defer func() {
|
|
||||||
testDidRenewLoop = func(next time.Duration, err error) {}
|
|
||||||
}()
|
|
||||||
done := make(chan struct{})
|
|
||||||
testDidRenewLoop = func(next time.Duration, err error) {
|
|
||||||
defer close(done)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("testDidRenewLoop: %v", err)
|
|
||||||
}
|
|
||||||
// Next should be about 90 days
|
|
||||||
// Previous expiration was within 1 min.
|
|
||||||
future := 88 * 24 * time.Hour
|
|
||||||
if next < future {
|
|
||||||
t.Errorf("testDidRenewLoop: next = %v; want >= %v", next, future)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the cached cert was not modified
|
|
||||||
tlscert, err := man.cacheGet(context.Background(), exampleCertKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("man.cacheGet: %v", err)
|
|
||||||
}
|
|
||||||
if !tlscert.Leaf.NotAfter.Equal(newLeaf.NotAfter) {
|
|
||||||
t.Errorf("cache leaf.NotAfter = %v; want == %v", tlscert.Leaf.NotAfter, newLeaf.NotAfter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify the old cert is also replaced in memory
|
|
||||||
man.stateMu.Lock()
|
|
||||||
defer man.stateMu.Unlock()
|
|
||||||
s := man.state[exampleCertKey]
|
|
||||||
if s == nil {
|
|
||||||
t.Fatalf("m.state[%q] is nil", exampleCertKey)
|
|
||||||
}
|
|
||||||
stateKey := s.key.Public().(*ecdsa.PublicKey)
|
|
||||||
if stateKey.X.Cmp(newKey.X) != 0 || stateKey.Y.Cmp(newKey.Y) != 0 {
|
|
||||||
t.Fatalf("state key was not updated from cache x: %v y: %v; want x: %v y: %v", stateKey.X, stateKey.Y, newKey.X, newKey.Y)
|
|
||||||
}
|
|
||||||
tlscert, err = s.tlscert()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("s.tlscert: %v", err)
|
|
||||||
}
|
|
||||||
if !tlscert.Leaf.NotAfter.Equal(newLeaf.NotAfter) {
|
|
||||||
t.Errorf("state leaf.NotAfter = %v; want == %v", tlscert.Leaf.NotAfter, newLeaf.NotAfter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify the private key is replaced in the renewal state
|
|
||||||
r := man.renewal[exampleCertKey]
|
|
||||||
if r == nil {
|
|
||||||
t.Fatalf("m.renewal[%q] is nil", exampleCertKey)
|
|
||||||
}
|
|
||||||
renewalKey := r.key.Public().(*ecdsa.PublicKey)
|
|
||||||
if renewalKey.X.Cmp(newKey.X) != 0 || renewalKey.Y.Cmp(newKey.Y) != 0 {
|
|
||||||
t.Fatalf("renewal private key was not updated from cache x: %v y: %v; want x: %v y: %v", renewalKey.X, renewalKey.Y, newKey.X, newKey.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert the expiring cert is returned from state
|
|
||||||
hello := clientHelloInfo(exampleDomain, true)
|
|
||||||
tlscert, err := man.GetCertificate(hello)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !oldLeaf.NotAfter.Equal(tlscert.Leaf.NotAfter) {
|
|
||||||
t.Errorf("state leaf.NotAfter = %v; want == %v", tlscert.Leaf.NotAfter, oldLeaf.NotAfter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger renew
|
|
||||||
go man.renew(exampleCertKey, s.key, s.leaf.NotAfter)
|
|
||||||
|
|
||||||
// wait for renew loop
|
|
||||||
select {
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
t.Fatal("renew took too long to occur")
|
|
||||||
case <-done:
|
|
||||||
// assert the new cert is returned from state after renew
|
|
||||||
hello := clientHelloInfo(exampleDomain, true)
|
|
||||||
tlscert, err := man.GetCertificate(hello)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !newTLSCert.Leaf.NotAfter.Equal(tlscert.Leaf.NotAfter) {
|
|
||||||
t.Errorf("state leaf.NotAfter = %v; want == %v", tlscert.Leaf.NotAfter, newTLSCert.Leaf.NotAfter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,281 +0,0 @@
|
||||||
// Copyright 2018 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"math/big"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// retryTimer encapsulates common logic for retrying unsuccessful requests.
|
|
||||||
// It is not safe for concurrent use.
|
|
||||||
type retryTimer struct {
|
|
||||||
// backoffFn provides backoff delay sequence for retries.
|
|
||||||
// See Client.RetryBackoff doc comment.
|
|
||||||
backoffFn func(n int, r *http.Request, res *http.Response) time.Duration
|
|
||||||
// n is the current retry attempt.
|
|
||||||
n int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *retryTimer) inc() {
|
|
||||||
t.n++
|
|
||||||
}
|
|
||||||
|
|
||||||
// backoff pauses the current goroutine as described in Client.RetryBackoff.
|
|
||||||
func (t *retryTimer) backoff(ctx context.Context, r *http.Request, res *http.Response) error {
|
|
||||||
d := t.backoffFn(t.n, r, res)
|
|
||||||
if d <= 0 {
|
|
||||||
return fmt.Errorf("acme: no more retries for %s; tried %d time(s)", r.URL, t.n)
|
|
||||||
}
|
|
||||||
wakeup := time.NewTimer(d)
|
|
||||||
defer wakeup.Stop()
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-wakeup.C:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) retryTimer() *retryTimer {
|
|
||||||
f := c.RetryBackoff
|
|
||||||
if f == nil {
|
|
||||||
f = defaultBackoff
|
|
||||||
}
|
|
||||||
return &retryTimer{backoffFn: f}
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultBackoff provides default Client.RetryBackoff implementation
|
|
||||||
// using a truncated exponential backoff algorithm,
|
|
||||||
// as described in Client.RetryBackoff.
|
|
||||||
//
|
|
||||||
// The n argument is always bounded between 1 and 30.
|
|
||||||
// The returned value is always greater than 0.
|
|
||||||
func defaultBackoff(n int, r *http.Request, res *http.Response) time.Duration {
|
|
||||||
const max = 10 * time.Second
|
|
||||||
var jitter time.Duration
|
|
||||||
if x, err := rand.Int(rand.Reader, big.NewInt(1000)); err == nil {
|
|
||||||
// Set the minimum to 1ms to avoid a case where
|
|
||||||
// an invalid Retry-After value is parsed into 0 below,
|
|
||||||
// resulting in the 0 returned value which would unintentionally
|
|
||||||
// stop the retries.
|
|
||||||
jitter = (1 + time.Duration(x.Int64())) * time.Millisecond
|
|
||||||
}
|
|
||||||
if v, ok := res.Header["Retry-After"]; ok {
|
|
||||||
return retryAfter(v[0]) + jitter
|
|
||||||
}
|
|
||||||
|
|
||||||
if n < 1 {
|
|
||||||
n = 1
|
|
||||||
}
|
|
||||||
if n > 30 {
|
|
||||||
n = 30
|
|
||||||
}
|
|
||||||
d := time.Duration(1<<uint(n-1))*time.Second + jitter
|
|
||||||
if d > max {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// retryAfter parses a Retry-After HTTP header value,
|
|
||||||
// trying to convert v into an int (seconds) or use http.ParseTime otherwise.
|
|
||||||
// It returns zero value if v cannot be parsed.
|
|
||||||
func retryAfter(v string) time.Duration {
|
|
||||||
if i, err := strconv.Atoi(v); err == nil {
|
|
||||||
return time.Duration(i) * time.Second
|
|
||||||
}
|
|
||||||
t, err := http.ParseTime(v)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return t.Sub(timeNow())
|
|
||||||
}
|
|
||||||
|
|
||||||
// resOkay is a function that reports whether the provided response is okay.
|
|
||||||
// It is expected to keep the response body unread.
|
|
||||||
type resOkay func(*http.Response) bool
|
|
||||||
|
|
||||||
// wantStatus returns a function which reports whether the code
|
|
||||||
// matches the status code of a response.
|
|
||||||
func wantStatus(codes ...int) resOkay {
|
|
||||||
return func(res *http.Response) bool {
|
|
||||||
for _, code := range codes {
|
|
||||||
if code == res.StatusCode {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get issues an unsigned GET request to the specified URL.
|
|
||||||
// It returns a non-error value only when ok reports true.
|
|
||||||
//
|
|
||||||
// get retries unsuccessful attempts according to c.RetryBackoff
|
|
||||||
// until the context is done or a non-retriable error is received.
|
|
||||||
func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Response, error) {
|
|
||||||
retry := c.retryTimer()
|
|
||||||
for {
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res, err := c.doNoRetry(ctx, req)
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
return nil, err
|
|
||||||
case ok(res):
|
|
||||||
return res, nil
|
|
||||||
case isRetriable(res.StatusCode):
|
|
||||||
retry.inc()
|
|
||||||
resErr := responseError(res)
|
|
||||||
res.Body.Close()
|
|
||||||
// Ignore the error value from retry.backoff
|
|
||||||
// and return the one from last retry, as received from the CA.
|
|
||||||
if retry.backoff(ctx, req, res) != nil {
|
|
||||||
return nil, resErr
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
defer res.Body.Close()
|
|
||||||
return nil, responseError(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// post issues a signed POST request in JWS format using the provided key
|
|
||||||
// to the specified URL.
|
|
||||||
// It returns a non-error value only when ok reports true.
|
|
||||||
//
|
|
||||||
// post retries unsuccessful attempts according to c.RetryBackoff
|
|
||||||
// until the context is done or a non-retriable error is received.
|
|
||||||
// It uses postNoRetry to make individual requests.
|
|
||||||
func (c *Client) post(ctx context.Context, key crypto.Signer, url string, body interface{}, ok resOkay) (*http.Response, error) {
|
|
||||||
retry := c.retryTimer()
|
|
||||||
for {
|
|
||||||
res, req, err := c.postNoRetry(ctx, key, url, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if ok(res) {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
resErr := responseError(res)
|
|
||||||
res.Body.Close()
|
|
||||||
switch {
|
|
||||||
// Check for bad nonce before isRetriable because it may have been returned
|
|
||||||
// with an unretriable response code such as 400 Bad Request.
|
|
||||||
case isBadNonce(resErr):
|
|
||||||
// Consider any previously stored nonce values to be invalid.
|
|
||||||
c.clearNonces()
|
|
||||||
case !isRetriable(res.StatusCode):
|
|
||||||
return nil, resErr
|
|
||||||
}
|
|
||||||
retry.inc()
|
|
||||||
// Ignore the error value from retry.backoff
|
|
||||||
// and return the one from last retry, as received from the CA.
|
|
||||||
if err := retry.backoff(ctx, req, res); err != nil {
|
|
||||||
return nil, resErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// postNoRetry signs the body with the given key and POSTs it to the provided url.
|
|
||||||
// The body argument must be JSON-serializable.
|
|
||||||
// It is used by c.post to retry unsuccessful attempts.
|
|
||||||
func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, *http.Request, error) {
|
|
||||||
nonce, err := c.popNonce(ctx, url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
b, err := jwsEncodeJSON(body, key, nonce)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewReader(b))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/jose+json")
|
|
||||||
res, err := c.doNoRetry(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
c.addNonce(res.Header)
|
|
||||||
return res, req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// doNoRetry issues a request req, replacing its context (if any) with ctx.
|
|
||||||
func (c *Client) doNoRetry(ctx context.Context, req *http.Request) (*http.Response, error) {
|
|
||||||
res, err := c.httpClient().Do(req.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
// Prefer the unadorned context error.
|
|
||||||
// (The acme package had tests assuming this, previously from ctxhttp's
|
|
||||||
// behavior, predating net/http supporting contexts natively)
|
|
||||||
// TODO(bradfitz): reconsider this in the future. But for now this
|
|
||||||
// requires no test updates.
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) httpClient() *http.Client {
|
|
||||||
if c.HTTPClient != nil {
|
|
||||||
return c.HTTPClient
|
|
||||||
}
|
|
||||||
return http.DefaultClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// isBadNonce reports whether err is an ACME "badnonce" error.
|
|
||||||
func isBadNonce(err error) bool {
|
|
||||||
// According to the spec badNonce is urn:ietf:params:acme:error:badNonce.
|
|
||||||
// However, ACME servers in the wild return their versions of the error.
|
|
||||||
// See https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-5.4
|
|
||||||
// and https://github.com/letsencrypt/boulder/blob/0e07eacb/docs/acme-divergences.md#section-66.
|
|
||||||
ae, ok := err.(*Error)
|
|
||||||
return ok && strings.HasSuffix(strings.ToLower(ae.ProblemType), ":badnonce")
|
|
||||||
}
|
|
||||||
|
|
||||||
// isRetriable reports whether a request can be retried
|
|
||||||
// based on the response status code.
|
|
||||||
//
|
|
||||||
// Note that a "bad nonce" error is returned with a non-retriable 400 Bad Request code.
|
|
||||||
// Callers should parse the response and check with isBadNonce.
|
|
||||||
func isRetriable(code int) bool {
|
|
||||||
return code <= 399 || code >= 500 || code == http.StatusTooManyRequests
|
|
||||||
}
|
|
||||||
|
|
||||||
// responseError creates an error of Error type from resp.
|
|
||||||
func responseError(resp *http.Response) error {
|
|
||||||
// don't care if ReadAll returns an error:
|
|
||||||
// json.Unmarshal will fail in that case anyway
|
|
||||||
b, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
e := &wireError{Status: resp.StatusCode}
|
|
||||||
if err := json.Unmarshal(b, e); err != nil {
|
|
||||||
// this is not a regular error response:
|
|
||||||
// populate detail with anything we received,
|
|
||||||
// e.Status will already contain HTTP response code value
|
|
||||||
e.Detail = string(b)
|
|
||||||
if e.Detail == "" {
|
|
||||||
e.Detail = resp.Status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return e.error(resp.Header)
|
|
||||||
}
|
|
|
@ -1,209 +0,0 @@
|
||||||
// Copyright 2018 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDefaultBackoff(t *testing.T) {
|
|
||||||
tt := []struct {
|
|
||||||
nretry int
|
|
||||||
retryAfter string // Retry-After header
|
|
||||||
out time.Duration // expected min; max = min + jitter
|
|
||||||
}{
|
|
||||||
{-1, "", time.Second}, // verify the lower bound is 1
|
|
||||||
{0, "", time.Second}, // verify the lower bound is 1
|
|
||||||
{100, "", 10 * time.Second}, // verify the ceiling
|
|
||||||
{1, "3600", time.Hour}, // verify the header value is used
|
|
||||||
{1, "", 1 * time.Second},
|
|
||||||
{2, "", 2 * time.Second},
|
|
||||||
{3, "", 4 * time.Second},
|
|
||||||
{4, "", 8 * time.Second},
|
|
||||||
}
|
|
||||||
for i, test := range tt {
|
|
||||||
r := httptest.NewRequest("GET", "/", nil)
|
|
||||||
resp := &http.Response{Header: http.Header{}}
|
|
||||||
if test.retryAfter != "" {
|
|
||||||
resp.Header.Set("Retry-After", test.retryAfter)
|
|
||||||
}
|
|
||||||
d := defaultBackoff(test.nretry, r, resp)
|
|
||||||
max := test.out + time.Second // + max jitter
|
|
||||||
if d < test.out || max < d {
|
|
||||||
t.Errorf("%d: defaultBackoff(%v) = %v; want between %v and %v", i, test.nretry, d, test.out, max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrorResponse(t *testing.T) {
|
|
||||||
s := `{
|
|
||||||
"status": 400,
|
|
||||||
"type": "urn:acme:error:xxx",
|
|
||||||
"detail": "text"
|
|
||||||
}`
|
|
||||||
res := &http.Response{
|
|
||||||
StatusCode: 400,
|
|
||||||
Status: "400 Bad Request",
|
|
||||||
Body: ioutil.NopCloser(strings.NewReader(s)),
|
|
||||||
Header: http.Header{"X-Foo": {"bar"}},
|
|
||||||
}
|
|
||||||
err := responseError(res)
|
|
||||||
v, ok := err.(*Error)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("err = %+v (%T); want *Error type", err, err)
|
|
||||||
}
|
|
||||||
if v.StatusCode != 400 {
|
|
||||||
t.Errorf("v.StatusCode = %v; want 400", v.StatusCode)
|
|
||||||
}
|
|
||||||
if v.ProblemType != "urn:acme:error:xxx" {
|
|
||||||
t.Errorf("v.ProblemType = %q; want urn:acme:error:xxx", v.ProblemType)
|
|
||||||
}
|
|
||||||
if v.Detail != "text" {
|
|
||||||
t.Errorf("v.Detail = %q; want text", v.Detail)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(v.Header, res.Header) {
|
|
||||||
t.Errorf("v.Header = %+v; want %+v", v.Header, res.Header)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPostWithRetries(t *testing.T) {
|
|
||||||
var count int
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
count++
|
|
||||||
w.Header().Set("Replay-Nonce", fmt.Sprintf("nonce%d", count))
|
|
||||||
if r.Method == "HEAD" {
|
|
||||||
// We expect the client to do 2 head requests to fetch
|
|
||||||
// nonces, one to start and another after getting badNonce
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
head, err := decodeJWSHead(r)
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
t.Errorf("decodeJWSHead: %v", err)
|
|
||||||
case head.Nonce == "":
|
|
||||||
t.Error("head.Nonce is empty")
|
|
||||||
case head.Nonce == "nonce1":
|
|
||||||
// Return a badNonce error to force the call to retry.
|
|
||||||
w.Header().Set("Retry-After", "0")
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
w.Write([]byte(`{"type":"urn:ietf:params:acme:error:badNonce"}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Make client.Authorize happy; we're not testing its result.
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
w.Write([]byte(`{"status":"valid"}`))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
client := &Client{Key: testKey, dir: &Directory{AuthzURL: ts.URL}}
|
|
||||||
// This call will fail with badNonce, causing a retry
|
|
||||||
if _, err := client.Authorize(context.Background(), "example.com"); err != nil {
|
|
||||||
t.Errorf("client.Authorize 1: %v", err)
|
|
||||||
}
|
|
||||||
if count != 4 {
|
|
||||||
t.Errorf("total requests count: %d; want 4", count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRetryErrorType(t *testing.T) {
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Replay-Nonce", "nonce")
|
|
||||||
w.WriteHeader(http.StatusTooManyRequests)
|
|
||||||
w.Write([]byte(`{"type":"rateLimited"}`))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
client := &Client{
|
|
||||||
Key: testKey,
|
|
||||||
RetryBackoff: func(n int, r *http.Request, res *http.Response) time.Duration {
|
|
||||||
// Do no retries.
|
|
||||||
return 0
|
|
||||||
},
|
|
||||||
dir: &Directory{AuthzURL: ts.URL},
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("post", func(t *testing.T) {
|
|
||||||
testRetryErrorType(t, func() error {
|
|
||||||
_, err := client.Authorize(context.Background(), "example.com")
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
})
|
|
||||||
t.Run("get", func(t *testing.T) {
|
|
||||||
testRetryErrorType(t, func() error {
|
|
||||||
_, err := client.GetAuthorization(context.Background(), ts.URL)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRetryErrorType(t *testing.T, callClient func() error) {
|
|
||||||
t.Helper()
|
|
||||||
err := callClient()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("client.Authorize returned nil error")
|
|
||||||
}
|
|
||||||
acmeErr, ok := err.(*Error)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("err is %v (%T); want *Error", err, err)
|
|
||||||
}
|
|
||||||
if acmeErr.StatusCode != http.StatusTooManyRequests {
|
|
||||||
t.Errorf("acmeErr.StatusCode = %d; want %d", acmeErr.StatusCode, http.StatusTooManyRequests)
|
|
||||||
}
|
|
||||||
if acmeErr.ProblemType != "rateLimited" {
|
|
||||||
t.Errorf("acmeErr.ProblemType = %q; want 'rateLimited'", acmeErr.ProblemType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRetryBackoffArgs(t *testing.T) {
|
|
||||||
const resCode = http.StatusInternalServerError
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Replay-Nonce", "test-nonce")
|
|
||||||
w.WriteHeader(resCode)
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
// Canceled in backoff.
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
var nretry int
|
|
||||||
backoff := func(n int, r *http.Request, res *http.Response) time.Duration {
|
|
||||||
nretry++
|
|
||||||
if n != nretry {
|
|
||||||
t.Errorf("n = %d; want %d", n, nretry)
|
|
||||||
}
|
|
||||||
if nretry == 3 {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
if r == nil {
|
|
||||||
t.Error("r is nil")
|
|
||||||
}
|
|
||||||
if res.StatusCode != resCode {
|
|
||||||
t.Errorf("res.StatusCode = %d; want %d", res.StatusCode, resCode)
|
|
||||||
}
|
|
||||||
return time.Millisecond
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &Client{
|
|
||||||
Key: testKey,
|
|
||||||
RetryBackoff: backoff,
|
|
||||||
dir: &Directory{AuthzURL: ts.URL},
|
|
||||||
}
|
|
||||||
if _, err := client.Authorize(ctx, "example.com"); err == nil {
|
|
||||||
t.Error("err is nil")
|
|
||||||
}
|
|
||||||
if nretry != 3 {
|
|
||||||
t.Errorf("nretry = %d; want 3", nretry)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
// Copyright 2015 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
_ "crypto/sha512" // need for EC keys
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
)
|
|
||||||
|
|
||||||
// jwsEncodeJSON signs claimset using provided key and a nonce.
|
|
||||||
// The result is serialized in JSON format.
|
|
||||||
// See https://tools.ietf.org/html/rfc7515#section-7.
|
|
||||||
func jwsEncodeJSON(claimset interface{}, key crypto.Signer, nonce string) ([]byte, error) {
|
|
||||||
jwk, err := jwkEncode(key.Public())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
alg, sha := jwsHasher(key)
|
|
||||||
if alg == "" || !sha.Available() {
|
|
||||||
return nil, ErrUnsupportedKey
|
|
||||||
}
|
|
||||||
phead := fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q}`, alg, jwk, nonce)
|
|
||||||
phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
|
|
||||||
cs, err := json.Marshal(claimset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
payload := base64.RawURLEncoding.EncodeToString(cs)
|
|
||||||
hash := sha.New()
|
|
||||||
hash.Write([]byte(phead + "." + payload))
|
|
||||||
sig, err := jwsSign(key, sha, hash.Sum(nil))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := struct {
|
|
||||||
Protected string `json:"protected"`
|
|
||||||
Payload string `json:"payload"`
|
|
||||||
Sig string `json:"signature"`
|
|
||||||
}{
|
|
||||||
Protected: phead,
|
|
||||||
Payload: payload,
|
|
||||||
Sig: base64.RawURLEncoding.EncodeToString(sig),
|
|
||||||
}
|
|
||||||
return json.Marshal(&enc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
|
|
||||||
// The result is also suitable for creating a JWK thumbprint.
|
|
||||||
// https://tools.ietf.org/html/rfc7517
|
|
||||||
func jwkEncode(pub crypto.PublicKey) (string, error) {
|
|
||||||
switch pub := pub.(type) {
|
|
||||||
case *rsa.PublicKey:
|
|
||||||
// https://tools.ietf.org/html/rfc7518#section-6.3.1
|
|
||||||
n := pub.N
|
|
||||||
e := big.NewInt(int64(pub.E))
|
|
||||||
// Field order is important.
|
|
||||||
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
|
|
||||||
return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
|
|
||||||
base64.RawURLEncoding.EncodeToString(e.Bytes()),
|
|
||||||
base64.RawURLEncoding.EncodeToString(n.Bytes()),
|
|
||||||
), nil
|
|
||||||
case *ecdsa.PublicKey:
|
|
||||||
// https://tools.ietf.org/html/rfc7518#section-6.2.1
|
|
||||||
p := pub.Curve.Params()
|
|
||||||
n := p.BitSize / 8
|
|
||||||
if p.BitSize%8 != 0 {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
x := pub.X.Bytes()
|
|
||||||
if n > len(x) {
|
|
||||||
x = append(make([]byte, n-len(x)), x...)
|
|
||||||
}
|
|
||||||
y := pub.Y.Bytes()
|
|
||||||
if n > len(y) {
|
|
||||||
y = append(make([]byte, n-len(y)), y...)
|
|
||||||
}
|
|
||||||
// Field order is important.
|
|
||||||
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
|
|
||||||
return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
|
|
||||||
p.Name,
|
|
||||||
base64.RawURLEncoding.EncodeToString(x),
|
|
||||||
base64.RawURLEncoding.EncodeToString(y),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
return "", ErrUnsupportedKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// jwsSign signs the digest using the given key.
|
|
||||||
// It returns ErrUnsupportedKey if the key type is unknown.
|
|
||||||
// The hash is used only for RSA keys.
|
|
||||||
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
|
|
||||||
switch key := key.(type) {
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
return key.Sign(rand.Reader, digest, hash)
|
|
||||||
case *ecdsa.PrivateKey:
|
|
||||||
r, s, err := ecdsa.Sign(rand.Reader, key, digest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rb, sb := r.Bytes(), s.Bytes()
|
|
||||||
size := key.Params().BitSize / 8
|
|
||||||
if size%8 > 0 {
|
|
||||||
size++
|
|
||||||
}
|
|
||||||
sig := make([]byte, size*2)
|
|
||||||
copy(sig[size-len(rb):], rb)
|
|
||||||
copy(sig[size*2-len(sb):], sb)
|
|
||||||
return sig, nil
|
|
||||||
}
|
|
||||||
return nil, ErrUnsupportedKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// jwsHasher indicates suitable JWS algorithm name and a hash function
|
|
||||||
// to use for signing a digest with the provided key.
|
|
||||||
// It returns ("", 0) if the key is not supported.
|
|
||||||
func jwsHasher(key crypto.Signer) (string, crypto.Hash) {
|
|
||||||
switch key := key.(type) {
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
return "RS256", crypto.SHA256
|
|
||||||
case *ecdsa.PrivateKey:
|
|
||||||
switch key.Params().Name {
|
|
||||||
case "P-256":
|
|
||||||
return "ES256", crypto.SHA256
|
|
||||||
case "P-384":
|
|
||||||
return "ES384", crypto.SHA384
|
|
||||||
case "P-521":
|
|
||||||
return "ES512", crypto.SHA512
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// JWKThumbprint creates a JWK thumbprint out of pub
|
|
||||||
// as specified in https://tools.ietf.org/html/rfc7638.
|
|
||||||
func JWKThumbprint(pub crypto.PublicKey) (string, error) {
|
|
||||||
jwk, err := jwkEncode(pub)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
b := sha256.Sum256([]byte(jwk))
|
|
||||||
return base64.RawURLEncoding.EncodeToString(b[:]), nil
|
|
||||||
}
|
|
|
@ -1,319 +0,0 @@
|
||||||
// Copyright 2015 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
testKeyPEM = `
|
|
||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEowIBAAKCAQEA4xgZ3eRPkwoRvy7qeRUbmMDe0V+xH9eWLdu0iheeLlrmD2mq
|
|
||||||
WXfP9IeSKApbn34g8TuAS9g5zhq8ELQ3kmjr+KV86GAMgI6VAcGlq3QrzpTCf/30
|
|
||||||
Ab7+zawrfRaFONa1HwEzPY1KHnGVkxJc85gNkwYI9SY2RHXtvln3zs5wITNrdosq
|
|
||||||
EXeaIkVYBEhbhNu54pp3kxo6TuWLi9e6pXeWetEwmlBwtWZlPoib2j3TxLBksKZf
|
|
||||||
oyFyek380mHgJAumQ/I2fjj98/97mk3ihOY4AgVdCDj1z/GCoZkG5Rq7nbCGyosy
|
|
||||||
KWyDX00Zs+nNqVhoLeIvXC4nnWdJMZ6rogxyQQIDAQABAoIBACIEZTOI1Kao9nmV
|
|
||||||
9IeIsuaR1Y61b9neOF/MLmIVIZu+AAJFCMB4Iw11FV6sFodwpEyeZhx2WkpWVN+H
|
|
||||||
r19eGiLX3zsL0DOdqBJoSIHDWCCMxgnYJ6nvS0nRxX3qVrBp8R2g12Ub+gNPbmFm
|
|
||||||
ecf/eeERIVxfifd9VsyRu34eDEvcmKFuLYbElFcPh62xE3x12UZvV/sN7gXbawpP
|
|
||||||
G+w255vbE5MoaKdnnO83cTFlcHvhn24M/78qP7Te5OAeelr1R89kYxQLpuGe4fbS
|
|
||||||
zc6E3ym5Td6urDetGGrSY1Eu10/8sMusX+KNWkm+RsBRbkyKq72ks/qKpOxOa+c6
|
|
||||||
9gm+Y8ECgYEA/iNUyg1ubRdH11p82l8KHtFC1DPE0V1gSZsX29TpM5jS4qv46K+s
|
|
||||||
8Ym1zmrORM8x+cynfPx1VQZQ34EYeCMIX212ryJ+zDATl4NE0I4muMvSiH9vx6Xc
|
|
||||||
7FmhNnaYzPsBL5Tm9nmtQuP09YEn8poiOJFiDs/4olnD5ogA5O4THGkCgYEA5MIL
|
|
||||||
qWYBUuqbEWLRtMruUtpASclrBqNNsJEsMGbeqBJmoMxdHeSZckbLOrqm7GlMyNRJ
|
|
||||||
Ne/5uWRGSzaMYuGmwsPpERzqEvYFnSrpjW5YtXZ+JtxFXNVfm9Z1gLLgvGpOUCIU
|
|
||||||
RbpoDckDe1vgUuk3y5+DjZihs+rqIJ45XzXTzBkCgYBWuf3segruJZy5rEKhTv+o
|
|
||||||
JqeUvRn0jNYYKFpLBeyTVBrbie6GkbUGNIWbrK05pC+c3K9nosvzuRUOQQL1tJbd
|
|
||||||
4gA3oiD9U4bMFNr+BRTHyZ7OQBcIXdz3t1qhuHVKtnngIAN1p25uPlbRFUNpshnt
|
|
||||||
jgeVoHlsBhApcs5DUc+pyQKBgDzeHPg/+g4z+nrPznjKnktRY1W+0El93kgi+J0Q
|
|
||||||
YiJacxBKEGTJ1MKBb8X6sDurcRDm22wMpGfd9I5Cv2v4GsUsF7HD/cx5xdih+G73
|
|
||||||
c4clNj/k0Ff5Nm1izPUno4C+0IOl7br39IPmfpSuR6wH/h6iHQDqIeybjxyKvT1G
|
|
||||||
N0rRAoGBAKGD+4ZI/E1MoJ5CXB8cDDMHagbE3cq/DtmYzE2v1DFpQYu5I4PCm5c7
|
|
||||||
EQeIP6dZtv8IMgtGIb91QX9pXvP0aznzQKwYIA8nZgoENCPfiMTPiEDT9e/0lObO
|
|
||||||
9XWsXpbSTsRPj0sv1rB+UzBJ0PgjK4q2zOF0sNo7b1+6nlM3BWPx
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
||||||
`
|
|
||||||
|
|
||||||
// This thumbprint is for the testKey defined above.
|
|
||||||
testKeyThumbprint = "6nicxzh6WETQlrvdchkz-U3e3DOQZ4heJKU63rfqMqQ"
|
|
||||||
|
|
||||||
// openssl ecparam -name secp256k1 -genkey -noout
|
|
||||||
testKeyECPEM = `
|
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MHcCAQEEIK07hGLr0RwyUdYJ8wbIiBS55CjnkMD23DWr+ccnypWLoAoGCCqGSM49
|
|
||||||
AwEHoUQDQgAE5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HThqIrvawF5
|
|
||||||
QAaS/RNouybCiRhRjI3EaxLkQwgrCw0gqQ==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
||||||
`
|
|
||||||
// openssl ecparam -name secp384r1 -genkey -noout
|
|
||||||
testKeyEC384PEM = `
|
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MIGkAgEBBDAQ4lNtXRORWr1bgKR1CGysr9AJ9SyEk4jiVnlUWWUChmSNL+i9SLSD
|
|
||||||
Oe/naPqXJ6CgBwYFK4EEACKhZANiAAQzKtj+Ms0vHoTX5dzv3/L5YMXOWuI5UKRj
|
|
||||||
JigpahYCqXD2BA1j0E/2xt5vlPf+gm0PL+UHSQsCokGnIGuaHCsJAp3ry0gHQEke
|
|
||||||
WYXapUUFdvaK1R2/2hn5O+eiQM8YzCg=
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
||||||
`
|
|
||||||
// openssl ecparam -name secp521r1 -genkey -noout
|
|
||||||
testKeyEC512PEM = `
|
|
||||||
-----BEGIN EC PRIVATE KEY-----
|
|
||||||
MIHcAgEBBEIBSNZKFcWzXzB/aJClAb305ibalKgtDA7+70eEkdPt28/3LZMM935Z
|
|
||||||
KqYHh/COcxuu3Kt8azRAUz3gyr4zZKhlKUSgBwYFK4EEACOhgYkDgYYABAHUNKbx
|
|
||||||
7JwC7H6pa2sV0tERWhHhB3JmW+OP6SUgMWryvIKajlx73eS24dy4QPGrWO9/ABsD
|
|
||||||
FqcRSkNVTXnIv6+0mAF25knqIBIg5Q8M9BnOu9GGAchcwt3O7RDHmqewnJJDrbjd
|
|
||||||
GGnm6rb+NnWR9DIopM0nKNkToWoF/hzopxu4Ae/GsQ==
|
|
||||||
-----END EC PRIVATE KEY-----
|
|
||||||
`
|
|
||||||
// 1. openssl ec -in key.pem -noout -text
|
|
||||||
// 2. remove first byte, 04 (the header); the rest is X and Y
|
|
||||||
// 3. convert each with: echo <val> | xxd -r -p | base64 -w 100 | tr -d '=' | tr '/+' '_-'
|
|
||||||
testKeyECPubX = "5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HQ"
|
|
||||||
testKeyECPubY = "4aiK72sBeUAGkv0TaLsmwokYUYyNxGsS5EMIKwsNIKk"
|
|
||||||
testKeyEC384PubX = "MyrY_jLNLx6E1-Xc79_y-WDFzlriOVCkYyYoKWoWAqlw9gQNY9BP9sbeb5T3_oJt"
|
|
||||||
testKeyEC384PubY = "Dy_lB0kLAqJBpyBrmhwrCQKd68tIB0BJHlmF2qVFBXb2itUdv9oZ-TvnokDPGMwo"
|
|
||||||
testKeyEC512PubX = "AdQ0pvHsnALsfqlraxXS0RFaEeEHcmZb44_pJSAxavK8gpqOXHvd5Lbh3LhA8atY738AGwMWpxFKQ1VNeci_r7SY"
|
|
||||||
testKeyEC512PubY = "AXbmSeogEiDlDwz0Gc670YYByFzC3c7tEMeap7CckkOtuN0Yaebqtv42dZH0MiikzSco2ROhagX-HOinG7gB78ax"
|
|
||||||
|
|
||||||
// echo -n '{"crv":"P-256","kty":"EC","x":"<testKeyECPubX>","y":"<testKeyECPubY>"}' | \
|
|
||||||
// openssl dgst -binary -sha256 | base64 | tr -d '=' | tr '/+' '_-'
|
|
||||||
testKeyECThumbprint = "zedj-Bd1Zshp8KLePv2MB-lJ_Hagp7wAwdkA0NUTniU"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
testKey *rsa.PrivateKey
|
|
||||||
testKeyEC *ecdsa.PrivateKey
|
|
||||||
testKeyEC384 *ecdsa.PrivateKey
|
|
||||||
testKeyEC512 *ecdsa.PrivateKey
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
testKey = parseRSA(testKeyPEM, "testKeyPEM")
|
|
||||||
testKeyEC = parseEC(testKeyECPEM, "testKeyECPEM")
|
|
||||||
testKeyEC384 = parseEC(testKeyEC384PEM, "testKeyEC384PEM")
|
|
||||||
testKeyEC512 = parseEC(testKeyEC512PEM, "testKeyEC512PEM")
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodePEM(s, name string) []byte {
|
|
||||||
d, _ := pem.Decode([]byte(s))
|
|
||||||
if d == nil {
|
|
||||||
panic("no block found in " + name)
|
|
||||||
}
|
|
||||||
return d.Bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRSA(s, name string) *rsa.PrivateKey {
|
|
||||||
b := decodePEM(s, name)
|
|
||||||
k, err := x509.ParsePKCS1PrivateKey(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("%s: %v", name, err))
|
|
||||||
}
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseEC(s, name string) *ecdsa.PrivateKey {
|
|
||||||
b := decodePEM(s, name)
|
|
||||||
k, err := x509.ParseECPrivateKey(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("%s: %v", name, err))
|
|
||||||
}
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJWSEncodeJSON(t *testing.T) {
|
|
||||||
claims := struct{ Msg string }{"Hello JWS"}
|
|
||||||
// JWS signed with testKey and "nonce" as the nonce value
|
|
||||||
// JSON-serialized JWS fields are split for easier testing
|
|
||||||
const (
|
|
||||||
// {"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"..."},"nonce":"nonce"}
|
|
||||||
protected = "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6" +
|
|
||||||
"IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eTdxZVJVYm1NRGUwVi14" +
|
|
||||||
"SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1" +
|
|
||||||
"QVM5ZzV6aHE4RUxRM2ttanItS1Y4NkdBTWdJNlZBY0dscTNRcnpw" +
|
|
||||||
"VENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4" +
|
|
||||||
"NWdOa3dZSTlTWTJSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZ" +
|
|
||||||
"QkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFda" +
|
|
||||||
"bFBvaWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZq" +
|
|
||||||
"ajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN25iQ0d5" +
|
|
||||||
"b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5" +
|
|
||||||
"UVEifSwibm9uY2UiOiJub25jZSJ9"
|
|
||||||
// {"Msg":"Hello JWS"}
|
|
||||||
payload = "eyJNc2ciOiJIZWxsbyBKV1MifQ"
|
|
||||||
signature = "eAGUikStX_UxyiFhxSLMyuyBcIB80GeBkFROCpap2sW3EmkU_ggF" +
|
|
||||||
"knaQzxrTfItICSAXsCLIquZ5BbrSWA_4vdEYrwWtdUj7NqFKjHRa" +
|
|
||||||
"zpLHcoR7r1rEHvkoP1xj49lS5fc3Wjjq8JUhffkhGbWZ8ZVkgPdC" +
|
|
||||||
"4tMBWiQDoth-x8jELP_3LYOB_ScUXi2mETBawLgOT2K8rA0Vbbmx" +
|
|
||||||
"hWNlOWuUf-8hL5YX4IOEwsS8JK_TrTq5Zc9My0zHJmaieqDV0UlP" +
|
|
||||||
"k0onFjPFkGm7MrPSgd0MqRG-4vSAg2O4hDo7rKv4n8POjjXlNQvM" +
|
|
||||||
"9IPLr8qZ7usYBKhEGwX3yq_eicAwBw"
|
|
||||||
)
|
|
||||||
|
|
||||||
b, err := jwsEncodeJSON(claims, testKey, "nonce")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var jws struct{ Protected, Payload, Signature string }
|
|
||||||
if err := json.Unmarshal(b, &jws); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if jws.Protected != protected {
|
|
||||||
t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, protected)
|
|
||||||
}
|
|
||||||
if jws.Payload != payload {
|
|
||||||
t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, payload)
|
|
||||||
}
|
|
||||||
if jws.Signature != signature {
|
|
||||||
t.Errorf("signature:\n%s\nwant:\n%s", jws.Signature, signature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJWSEncodeJSONEC(t *testing.T) {
|
|
||||||
tt := []struct {
|
|
||||||
key *ecdsa.PrivateKey
|
|
||||||
x, y string
|
|
||||||
alg, crv string
|
|
||||||
}{
|
|
||||||
{testKeyEC, testKeyECPubX, testKeyECPubY, "ES256", "P-256"},
|
|
||||||
{testKeyEC384, testKeyEC384PubX, testKeyEC384PubY, "ES384", "P-384"},
|
|
||||||
{testKeyEC512, testKeyEC512PubX, testKeyEC512PubY, "ES512", "P-521"},
|
|
||||||
}
|
|
||||||
for i, test := range tt {
|
|
||||||
claims := struct{ Msg string }{"Hello JWS"}
|
|
||||||
b, err := jwsEncodeJSON(claims, test.key, "nonce")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%d: %v", i, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var jws struct{ Protected, Payload, Signature string }
|
|
||||||
if err := json.Unmarshal(b, &jws); err != nil {
|
|
||||||
t.Errorf("%d: %v", i, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err = base64.RawURLEncoding.DecodeString(jws.Protected)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%d: jws.Protected: %v", i, err)
|
|
||||||
}
|
|
||||||
var head struct {
|
|
||||||
Alg string
|
|
||||||
Nonce string
|
|
||||||
JWK struct {
|
|
||||||
Crv string
|
|
||||||
Kty string
|
|
||||||
X string
|
|
||||||
Y string
|
|
||||||
} `json:"jwk"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(b, &head); err != nil {
|
|
||||||
t.Errorf("%d: jws.Protected: %v", i, err)
|
|
||||||
}
|
|
||||||
if head.Alg != test.alg {
|
|
||||||
t.Errorf("%d: head.Alg = %q; want %q", i, head.Alg, test.alg)
|
|
||||||
}
|
|
||||||
if head.Nonce != "nonce" {
|
|
||||||
t.Errorf("%d: head.Nonce = %q; want nonce", i, head.Nonce)
|
|
||||||
}
|
|
||||||
if head.JWK.Crv != test.crv {
|
|
||||||
t.Errorf("%d: head.JWK.Crv = %q; want %q", i, head.JWK.Crv, test.crv)
|
|
||||||
}
|
|
||||||
if head.JWK.Kty != "EC" {
|
|
||||||
t.Errorf("%d: head.JWK.Kty = %q; want EC", i, head.JWK.Kty)
|
|
||||||
}
|
|
||||||
if head.JWK.X != test.x {
|
|
||||||
t.Errorf("%d: head.JWK.X = %q; want %q", i, head.JWK.X, test.x)
|
|
||||||
}
|
|
||||||
if head.JWK.Y != test.y {
|
|
||||||
t.Errorf("%d: head.JWK.Y = %q; want %q", i, head.JWK.Y, test.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJWKThumbprintRSA(t *testing.T) {
|
|
||||||
// Key example from RFC 7638
|
|
||||||
const base64N = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" +
|
|
||||||
"VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6" +
|
|
||||||
"4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD" +
|
|
||||||
"W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9" +
|
|
||||||
"1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH" +
|
|
||||||
"aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw"
|
|
||||||
const base64E = "AQAB"
|
|
||||||
const expected = "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
|
|
||||||
|
|
||||||
b, err := base64.RawURLEncoding.DecodeString(base64N)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error parsing example key N: %v", err)
|
|
||||||
}
|
|
||||||
n := new(big.Int).SetBytes(b)
|
|
||||||
|
|
||||||
b, err = base64.RawURLEncoding.DecodeString(base64E)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error parsing example key E: %v", err)
|
|
||||||
}
|
|
||||||
e := new(big.Int).SetBytes(b)
|
|
||||||
|
|
||||||
pub := &rsa.PublicKey{N: n, E: int(e.Uint64())}
|
|
||||||
th, err := JWKThumbprint(pub)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if th != expected {
|
|
||||||
t.Errorf("thumbprint = %q; want %q", th, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJWKThumbprintEC(t *testing.T) {
|
|
||||||
// Key example from RFC 7520
|
|
||||||
// expected was computed with
|
|
||||||
// echo -n '{"crv":"P-521","kty":"EC","x":"<base64X>","y":"<base64Y>"}' | \
|
|
||||||
// openssl dgst -binary -sha256 | \
|
|
||||||
// base64 | \
|
|
||||||
// tr -d '=' | tr '/+' '_-'
|
|
||||||
const (
|
|
||||||
base64X = "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkT" +
|
|
||||||
"KqjqvjyekWF-7ytDyRXYgCF5cj0Kt"
|
|
||||||
base64Y = "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUda" +
|
|
||||||
"QkAgDPrwQrJmbnX9cwlGfP-HqHZR1"
|
|
||||||
expected = "dHri3SADZkrush5HU_50AoRhcKFryN-PI6jPBtPL55M"
|
|
||||||
)
|
|
||||||
|
|
||||||
b, err := base64.RawURLEncoding.DecodeString(base64X)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error parsing example key X: %v", err)
|
|
||||||
}
|
|
||||||
x := new(big.Int).SetBytes(b)
|
|
||||||
|
|
||||||
b, err = base64.RawURLEncoding.DecodeString(base64Y)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error parsing example key Y: %v", err)
|
|
||||||
}
|
|
||||||
y := new(big.Int).SetBytes(b)
|
|
||||||
|
|
||||||
pub := &ecdsa.PublicKey{Curve: elliptic.P521(), X: x, Y: y}
|
|
||||||
th, err := JWKThumbprint(pub)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if th != expected {
|
|
||||||
t.Errorf("thumbprint = %q; want %q", th, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJWKThumbprintErrUnsupportedKey(t *testing.T) {
|
|
||||||
_, err := JWKThumbprint(struct{}{})
|
|
||||||
if err != ErrUnsupportedKey {
|
|
||||||
t.Errorf("err = %q; want %q", err, ErrUnsupportedKey)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,329 +0,0 @@
|
||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ACME server response statuses used to describe Authorization and Challenge states.
|
|
||||||
const (
|
|
||||||
StatusUnknown = "unknown"
|
|
||||||
StatusPending = "pending"
|
|
||||||
StatusProcessing = "processing"
|
|
||||||
StatusValid = "valid"
|
|
||||||
StatusInvalid = "invalid"
|
|
||||||
StatusRevoked = "revoked"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CRLReasonCode identifies the reason for a certificate revocation.
|
|
||||||
type CRLReasonCode int
|
|
||||||
|
|
||||||
// CRL reason codes as defined in RFC 5280.
|
|
||||||
const (
|
|
||||||
CRLReasonUnspecified CRLReasonCode = 0
|
|
||||||
CRLReasonKeyCompromise CRLReasonCode = 1
|
|
||||||
CRLReasonCACompromise CRLReasonCode = 2
|
|
||||||
CRLReasonAffiliationChanged CRLReasonCode = 3
|
|
||||||
CRLReasonSuperseded CRLReasonCode = 4
|
|
||||||
CRLReasonCessationOfOperation CRLReasonCode = 5
|
|
||||||
CRLReasonCertificateHold CRLReasonCode = 6
|
|
||||||
CRLReasonRemoveFromCRL CRLReasonCode = 8
|
|
||||||
CRLReasonPrivilegeWithdrawn CRLReasonCode = 9
|
|
||||||
CRLReasonAACompromise CRLReasonCode = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrUnsupportedKey is returned when an unsupported key type is encountered.
|
|
||||||
var ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
|
|
||||||
|
|
||||||
// Error is an ACME error, defined in Problem Details for HTTP APIs doc
|
|
||||||
// http://tools.ietf.org/html/draft-ietf-appsawg-http-problem.
|
|
||||||
type Error struct {
|
|
||||||
// StatusCode is The HTTP status code generated by the origin server.
|
|
||||||
StatusCode int
|
|
||||||
// ProblemType is a URI reference that identifies the problem type,
|
|
||||||
// typically in a "urn:acme:error:xxx" form.
|
|
||||||
ProblemType string
|
|
||||||
// Detail is a human-readable explanation specific to this occurrence of the problem.
|
|
||||||
Detail string
|
|
||||||
// Header is the original server error response headers.
|
|
||||||
// It may be nil.
|
|
||||||
Header http.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
|
||||||
return fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthorizationError indicates that an authorization for an identifier
|
|
||||||
// did not succeed.
|
|
||||||
// It contains all errors from Challenge items of the failed Authorization.
|
|
||||||
type AuthorizationError struct {
|
|
||||||
// URI uniquely identifies the failed Authorization.
|
|
||||||
URI string
|
|
||||||
|
|
||||||
// Identifier is an AuthzID.Value of the failed Authorization.
|
|
||||||
Identifier string
|
|
||||||
|
|
||||||
// Errors is a collection of non-nil error values of Challenge items
|
|
||||||
// of the failed Authorization.
|
|
||||||
Errors []error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AuthorizationError) Error() string {
|
|
||||||
e := make([]string, len(a.Errors))
|
|
||||||
for i, err := range a.Errors {
|
|
||||||
e[i] = err.Error()
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("acme: authorization error for %s: %s", a.Identifier, strings.Join(e, "; "))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RateLimit reports whether err represents a rate limit error and
|
|
||||||
// any Retry-After duration returned by the server.
|
|
||||||
//
|
|
||||||
// See the following for more details on rate limiting:
|
|
||||||
// https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-5.6
|
|
||||||
func RateLimit(err error) (time.Duration, bool) {
|
|
||||||
e, ok := err.(*Error)
|
|
||||||
if !ok {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
// Some CA implementations may return incorrect values.
|
|
||||||
// Use case-insensitive comparison.
|
|
||||||
if !strings.HasSuffix(strings.ToLower(e.ProblemType), ":ratelimited") {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
if e.Header == nil {
|
|
||||||
return 0, true
|
|
||||||
}
|
|
||||||
return retryAfter(e.Header.Get("Retry-After")), true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account is a user account. It is associated with a private key.
|
|
||||||
type Account struct {
|
|
||||||
// URI is the account unique ID, which is also a URL used to retrieve
|
|
||||||
// account data from the CA.
|
|
||||||
URI string
|
|
||||||
|
|
||||||
// Contact is a slice of contact info used during registration.
|
|
||||||
Contact []string
|
|
||||||
|
|
||||||
// The terms user has agreed to.
|
|
||||||
// A value not matching CurrentTerms indicates that the user hasn't agreed
|
|
||||||
// to the actual Terms of Service of the CA.
|
|
||||||
AgreedTerms string
|
|
||||||
|
|
||||||
// Actual terms of a CA.
|
|
||||||
CurrentTerms string
|
|
||||||
|
|
||||||
// Authz is the authorization URL used to initiate a new authz flow.
|
|
||||||
Authz string
|
|
||||||
|
|
||||||
// Authorizations is a URI from which a list of authorizations
|
|
||||||
// granted to this account can be fetched via a GET request.
|
|
||||||
Authorizations string
|
|
||||||
|
|
||||||
// Certificates is a URI from which a list of certificates
|
|
||||||
// issued for this account can be fetched via a GET request.
|
|
||||||
Certificates string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directory is ACME server discovery data.
|
|
||||||
type Directory struct {
|
|
||||||
// RegURL is an account endpoint URL, allowing for creating new
|
|
||||||
// and modifying existing accounts.
|
|
||||||
RegURL string
|
|
||||||
|
|
||||||
// AuthzURL is used to initiate Identifier Authorization flow.
|
|
||||||
AuthzURL string
|
|
||||||
|
|
||||||
// CertURL is a new certificate issuance endpoint URL.
|
|
||||||
CertURL string
|
|
||||||
|
|
||||||
// RevokeURL is used to initiate a certificate revocation flow.
|
|
||||||
RevokeURL string
|
|
||||||
|
|
||||||
// Term is a URI identifying the current terms of service.
|
|
||||||
Terms string
|
|
||||||
|
|
||||||
// Website is an HTTP or HTTPS URL locating a website
|
|
||||||
// providing more information about the ACME server.
|
|
||||||
Website string
|
|
||||||
|
|
||||||
// CAA consists of lowercase hostname elements, which the ACME server
|
|
||||||
// recognises as referring to itself for the purposes of CAA record validation
|
|
||||||
// as defined in RFC6844.
|
|
||||||
CAA []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Challenge encodes a returned CA challenge.
|
|
||||||
// Its Error field may be non-nil if the challenge is part of an Authorization
|
|
||||||
// with StatusInvalid.
|
|
||||||
type Challenge struct {
|
|
||||||
// Type is the challenge type, e.g. "http-01", "tls-sni-02", "dns-01".
|
|
||||||
Type string
|
|
||||||
|
|
||||||
// URI is where a challenge response can be posted to.
|
|
||||||
URI string
|
|
||||||
|
|
||||||
// Token is a random value that uniquely identifies the challenge.
|
|
||||||
Token string
|
|
||||||
|
|
||||||
// Status identifies the status of this challenge.
|
|
||||||
Status string
|
|
||||||
|
|
||||||
// Error indicates the reason for an authorization failure
|
|
||||||
// when this challenge was used.
|
|
||||||
// The type of a non-nil value is *Error.
|
|
||||||
Error error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authorization encodes an authorization response.
|
|
||||||
type Authorization struct {
|
|
||||||
// URI uniquely identifies a authorization.
|
|
||||||
URI string
|
|
||||||
|
|
||||||
// Status identifies the status of an authorization.
|
|
||||||
Status string
|
|
||||||
|
|
||||||
// Identifier is what the account is authorized to represent.
|
|
||||||
Identifier AuthzID
|
|
||||||
|
|
||||||
// Challenges that the client needs to fulfill in order to prove possession
|
|
||||||
// of the identifier (for pending authorizations).
|
|
||||||
// For final authorizations, the challenges that were used.
|
|
||||||
Challenges []*Challenge
|
|
||||||
|
|
||||||
// A collection of sets of challenges, each of which would be sufficient
|
|
||||||
// to prove possession of the identifier.
|
|
||||||
// Clients must complete a set of challenges that covers at least one set.
|
|
||||||
// Challenges are identified by their indices in the challenges array.
|
|
||||||
// If this field is empty, the client needs to complete all challenges.
|
|
||||||
Combinations [][]int
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthzID is an identifier that an account is authorized to represent.
|
|
||||||
type AuthzID struct {
|
|
||||||
Type string // The type of identifier, e.g. "dns".
|
|
||||||
Value string // The identifier itself, e.g. "example.org".
|
|
||||||
}
|
|
||||||
|
|
||||||
// wireAuthz is ACME JSON representation of Authorization objects.
|
|
||||||
type wireAuthz struct {
|
|
||||||
Status string
|
|
||||||
Challenges []wireChallenge
|
|
||||||
Combinations [][]int
|
|
||||||
Identifier struct {
|
|
||||||
Type string
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *wireAuthz) authorization(uri string) *Authorization {
|
|
||||||
a := &Authorization{
|
|
||||||
URI: uri,
|
|
||||||
Status: z.Status,
|
|
||||||
Identifier: AuthzID{Type: z.Identifier.Type, Value: z.Identifier.Value},
|
|
||||||
Combinations: z.Combinations, // shallow copy
|
|
||||||
Challenges: make([]*Challenge, len(z.Challenges)),
|
|
||||||
}
|
|
||||||
for i, v := range z.Challenges {
|
|
||||||
a.Challenges[i] = v.challenge()
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
func (z *wireAuthz) error(uri string) *AuthorizationError {
|
|
||||||
err := &AuthorizationError{
|
|
||||||
URI: uri,
|
|
||||||
Identifier: z.Identifier.Value,
|
|
||||||
}
|
|
||||||
for _, raw := range z.Challenges {
|
|
||||||
if raw.Error != nil {
|
|
||||||
err.Errors = append(err.Errors, raw.Error.error(nil))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// wireChallenge is ACME JSON challenge representation.
|
|
||||||
type wireChallenge struct {
|
|
||||||
URI string `json:"uri"`
|
|
||||||
Type string
|
|
||||||
Token string
|
|
||||||
Status string
|
|
||||||
Error *wireError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *wireChallenge) challenge() *Challenge {
|
|
||||||
v := &Challenge{
|
|
||||||
URI: c.URI,
|
|
||||||
Type: c.Type,
|
|
||||||
Token: c.Token,
|
|
||||||
Status: c.Status,
|
|
||||||
}
|
|
||||||
if v.Status == "" {
|
|
||||||
v.Status = StatusPending
|
|
||||||
}
|
|
||||||
if c.Error != nil {
|
|
||||||
v.Error = c.Error.error(nil)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// wireError is a subset of fields of the Problem Details object
|
|
||||||
// as described in https://tools.ietf.org/html/rfc7807#section-3.1.
|
|
||||||
type wireError struct {
|
|
||||||
Status int
|
|
||||||
Type string
|
|
||||||
Detail string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *wireError) error(h http.Header) *Error {
|
|
||||||
return &Error{
|
|
||||||
StatusCode: e.Status,
|
|
||||||
ProblemType: e.Type,
|
|
||||||
Detail: e.Detail,
|
|
||||||
Header: h,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CertOption is an optional argument type for the TLS ChallengeCert methods for
|
|
||||||
// customizing a temporary certificate for TLS-based challenges.
|
|
||||||
type CertOption interface {
|
|
||||||
privateCertOpt()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithKey creates an option holding a private/public key pair.
|
|
||||||
// The private part signs a certificate, and the public part represents the signee.
|
|
||||||
func WithKey(key crypto.Signer) CertOption {
|
|
||||||
return &certOptKey{key}
|
|
||||||
}
|
|
||||||
|
|
||||||
type certOptKey struct {
|
|
||||||
key crypto.Signer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*certOptKey) privateCertOpt() {}
|
|
||||||
|
|
||||||
// WithTemplate creates an option for specifying a certificate template.
|
|
||||||
// See x509.CreateCertificate for template usage details.
|
|
||||||
//
|
|
||||||
// In TLS ChallengeCert methods, the template is also used as parent,
|
|
||||||
// resulting in a self-signed certificate.
|
|
||||||
// The DNSNames field of t is always overwritten for tls-sni challenge certs.
|
|
||||||
func WithTemplate(t *x509.Certificate) CertOption {
|
|
||||||
return (*certOptTemplate)(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
type certOptTemplate x509.Certificate
|
|
||||||
|
|
||||||
func (*certOptTemplate) privateCertOpt() {}
|
|
|
@ -1,63 +0,0 @@
|
||||||
// Copyright 2017 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package acme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRateLimit(t *testing.T) {
|
|
||||||
now := time.Date(2017, 04, 27, 10, 0, 0, 0, time.UTC)
|
|
||||||
f := timeNow
|
|
||||||
defer func() { timeNow = f }()
|
|
||||||
timeNow = func() time.Time { return now }
|
|
||||||
|
|
||||||
h120, hTime := http.Header{}, http.Header{}
|
|
||||||
h120.Set("Retry-After", "120")
|
|
||||||
hTime.Set("Retry-After", "Tue Apr 27 11:00:00 2017")
|
|
||||||
|
|
||||||
err1 := &Error{
|
|
||||||
ProblemType: "urn:ietf:params:acme:error:nolimit",
|
|
||||||
Header: h120,
|
|
||||||
}
|
|
||||||
err2 := &Error{
|
|
||||||
ProblemType: "urn:ietf:params:acme:error:rateLimited",
|
|
||||||
Header: h120,
|
|
||||||
}
|
|
||||||
err3 := &Error{
|
|
||||||
ProblemType: "urn:ietf:params:acme:error:rateLimited",
|
|
||||||
Header: nil,
|
|
||||||
}
|
|
||||||
err4 := &Error{
|
|
||||||
ProblemType: "urn:ietf:params:acme:error:rateLimited",
|
|
||||||
Header: hTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
tt := []struct {
|
|
||||||
err error
|
|
||||||
res time.Duration
|
|
||||||
ok bool
|
|
||||||
}{
|
|
||||||
{nil, 0, false},
|
|
||||||
{errors.New("dummy"), 0, false},
|
|
||||||
{err1, 0, false},
|
|
||||||
{err2, 2 * time.Minute, true},
|
|
||||||
{err3, 0, true},
|
|
||||||
{err4, time.Hour, true},
|
|
||||||
}
|
|
||||||
for i, test := range tt {
|
|
||||||
res, ok := RateLimit(test.err)
|
|
||||||
if ok != test.ok {
|
|
||||||
t.Errorf("%d: RateLimit(%+v): ok = %v; want %v", i, test.err, ok, test.ok)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if res != test.res {
|
|
||||||
t.Errorf("%d: RateLimit(%+v) = %v; want %v", i, test.err, res, test.res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue