├── .github
└── workflows
│ └── release-please.yml
├── .gitignore
├── KittenDNS jMeter Test Plan.jmx
├── LICENSE
├── Makefile
├── README.md
├── assets
├── jmeter-kittendns.png
└── kittendns.png
├── builders
└── records.go
├── cache
└── rccache.go
├── config.toml.template
├── config
└── config.go
├── dns
├── .codecov.yml
├── .github
│ └── workflows
│ │ ├── codeql-analysis.yml
│ │ └── go.yml
├── .gitignore
├── AUTHORS
├── CODEOWNERS
├── CONTRIBUTORS
├── COPYRIGHT
├── LICENSE
├── Makefile.fuzz
├── Makefile.release
├── README.md
├── acceptfunc.go
├── acceptfunc_test.go
├── client.go
├── client_test.go
├── clientconfig.go
├── clientconfig_test.go
├── dane.go
├── defaults.go
├── dns.go
├── dns_bench_test.go
├── dns_test.go
├── dnssec.go
├── dnssec_keygen.go
├── dnssec_keyscan.go
├── dnssec_privkey.go
├── dnssec_test.go
├── dnsutil
│ ├── util.go
│ └── util_test.go
├── doc.go
├── duplicate.go
├── duplicate_generate.go
├── duplicate_test.go
├── dyn_test.go
├── edns.go
├── edns_test.go
├── example_test.go
├── format.go
├── format_test.go
├── fuzz.go
├── fuzz_test.go
├── generate.go
├── generate_test.go
├── go.mod
├── go.sum
├── hash.go
├── issue_test.go
├── labels.go
├── labels_test.go
├── leak_test.go
├── length_test.go
├── listen_no_reuseport.go
├── listen_reuseport.go
├── msg.go
├── msg_generate.go
├── msg_helpers.go
├── msg_helpers_test.go
├── msg_test.go
├── msg_truncate.go
├── msg_truncate_test.go
├── nsecx.go
├── nsecx_test.go
├── parse_test.go
├── privaterr.go
├── privaterr_test.go
├── reverse.go
├── rr_test.go
├── sanitize.go
├── sanitize_test.go
├── scan.go
├── scan_rr.go
├── scan_test.go
├── serve_mux.go
├── serve_mux_test.go
├── server.go
├── server_test.go
├── sig0.go
├── sig0_test.go
├── singleinflight.go
├── smimea.go
├── svcb.go
├── svcb_test.go
├── tlsa.go
├── tools.go
├── tsig.go
├── tsig_test.go
├── types.go
├── types_generate.go
├── types_test.go
├── udp.go
├── udp_test.go
├── udp_windows.go
├── update.go
├── update_test.go
├── version.go
├── version_test.go
├── xfr.go
├── xfr_test.go
├── zduplicate.go
├── zmsg.go
└── ztypes.go
├── go.mod
├── go.sum
├── main.go
├── main_test.go
├── plugins
├── Makefile
├── config.toml.template
├── example
│ ├── Makefile
│ └── example.go
├── handler.go
├── jsscript
│ ├── Makefile
│ ├── example.js
│ ├── extended_example.js
│ ├── fetch
│ │ └── fetcher.go
│ ├── jsscript.go
│ └── secrets.js.template
└── plugins.go
├── secret.toml.template
├── secret
└── secret.go
└── version
├── const.go
└── vcs.go
/.github/workflows/release-please.yml:
--------------------------------------------------------------------------------
1 | on:
2 | pull_request:
3 | push:
4 | branches:
5 | - master
6 |
7 | permissions:
8 | contents: write
9 | pull-requests: write
10 |
11 | name: release-please
12 |
13 | jobs:
14 | release-please:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: google-github-actions/release-please-action@v3
19 | with:
20 | release-type: go
21 | package-name: KittenDNS
22 | default-branch: master
23 | pull-request-title-pattern: "ci: release ${version}"
24 | token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
25 | extra-files: |
26 | version/const.go
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | config.toml
2 | secret.toml
3 | plugins/jsscript/secrets.js
4 | thirdparty
5 | kittendns
6 | sourceme
7 | dist/
8 | bin/
9 | .vscode/
10 | id_rsa*
11 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL := /bin/bash
2 | BUILD_FLAGS?=-s -w
3 | TRIM_FLAGS=
4 | MAIN_TARGETS?=linux/amd64,linux/arm64,darwin/amd64,darwin/arm64
5 | PLUGIN_TARGETS?=linux/amd64,linux/arm64,darwin/amd64,darwin/arm64
6 | GO_RELEASE_V=$(shell go version | { read _ _ v _; echo $${v#go}; })
7 |
8 | #include plugins/Makefile
9 |
10 | build:
11 | @mkdir -p bin && go build ${TRIM_FLAGS} -ldflags "${BUILD_FLAGS}" -o bin/kittendns main.go
12 |
13 | test:
14 | @go test
15 |
16 | linuxamd64:
17 | @mkdir -p dist/$@ && GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o dist/$@/kittendns main.go
18 |
19 | linuxarm64:
20 | @mkdir -p dist/$@ && GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o dist/$@/kittendns main.go
21 |
22 | darwinamd64:
23 | @mkdir -p dist/$@ && GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o dist/$@/kittendns main.go
24 |
25 | darwinarm64:
26 | @mkdir -p dist/$@ && GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o dist/$@/kittendns main.go
27 |
28 | winamd64:
29 | @mkdir -p dist/$@ && GOOS=windows GOARCH=amd64 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -o dist/$@/kittendns main.go
30 |
31 | plugins:
32 | @PLUGIN_OS=linux PLUGIN_ARCH=amd64 make plugin_example plugin_jsscript
33 |
34 | plugins_darwin:
35 | @PLUGIN_OS=darwin PLUGIN_ARCH=amd64 make plugin_example plugin_jsscript
36 | @PLUGIN_OS=darwin PLUGIN_ARCH=arm64 make plugin_example plugin_jsscript
37 |
38 | fullrelease:
39 | @cd scripts && ./release.sh main
40 |
41 | release: linuxamd64 linuxarm64 winamd64
42 |
43 | release_darwin: darwinamd64 darwinarm64
44 |
45 | releasemain:
46 | @xgo -v -ldflags="${BUILD_FLAGS}" -trimpath -go ${GO_RELEASE_V} -out kittendns -dest bin -buildvcs=false --targets="${MAIN_TARGETS}" .
47 |
48 | releaseplugin:
49 | @echo "Building $P plugin $M"; \
50 | xgo -v -ldflags="${BUILD_FLAGS}" -trimpath -go ${GO_RELEASE_V} -out $P -dest bin -buildvcs=false -buildmode=plugin --targets="${PLUGIN_TARGETS}" --pkg $M/$P.go . && \
51 | (cd bin && for lib in $$(ls $$P-*); do sudo mv $$lib $$lib.so; done); \
52 |
53 | releaseplugins:
54 | @for pkg in $$(find plugins/* -depth -maxdepth 0 -type d); do \
55 | P=$$(echo $$pkg | cut -d'/' -f 2) M=$$pkg make releaseplugin; \
56 | done
57 |
58 | .PHONY: build release release_darwin test linuxamd64 linuxarm64 darwinamd64 darwinarm64 winamd64 plugins plugins_darwin
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 | # What is this?
24 |
25 | A toy DNS for hobbyists and worried people.
26 |
27 | Mission Statement:
28 |
29 | - No fat. Fast.
30 |
31 | Features:
32 |
33 | - Really easy to configure (toml syntax)
34 | - Rule engine to rewrite/deny queries
35 | - Plugins support
36 |
37 | But also:
38 |
39 | - RFC2136 and LetsEncrypt compatibility, use as a DNS endpoint to obtain certificates
40 | - Configuration auto-update
41 |
42 |
43 | # Configuration, Documentation
44 |
45 |
46 |
47 | Take a look at the content of the `config.toml.template` file. Copy it to `config.toml` and run.
48 |
49 | Read the [CONCISE DOCUMENTATION](https://github.com/Fusion/kittendns/wiki) 📖
50 |
51 |
52 |
53 | # DNS Synchronization
54 |
55 | There is currently no notion of primary and secondary DNS. All your DNS instances are equal. It would be fairly easy to implement `IXFR/AXFR` but unless it becomes a mandatory feature, this seems to go against my "no fat/easy to configure" goals. With this being said, you could use something like [Syncthing](https://syncthing.net/) to keep `config.toml` current.
56 |
57 | # Tell me more about the DNS repository
58 |
59 | In the `github.com/miekg/dns` repository, there was a pull request allowing code using that library to retrieve additional information about the requesting socket. This includes source IP, which can be convenient in a split horizon environment. It lives in this directory (slightly adapted)
60 |
61 | # Performance testing
62 |
63 | The tests below are performed using authoritative (local) records as my main goal is to offer a server that can survive a brutal assault serving cloud endpoints. Performing the same test against recursed hosts offers similar performance, simply because I am not querying 1M different hosts and the server efficiently`*` caches responses (while respecting their TTL)
64 |
65 | These tests are run locally on a 2020 Macbook M1 Pro and jMeter is using as much CPU as it dares to, while kittendns doesn't even appear in my top output.
66 |
67 | `*` dumbly
68 |
69 | ## jMeter stress testing
70 |
71 | 1. Run Wireshark to capture a DNS query. In the details window, select the Domain Name System layer, right-click, copy as a hex stream.
72 | 2. In jMeter, paste in the "Request Data" area
73 |
74 | The jMeter test plan is stored in `KittenDNS jMeter Test Plan.jmx`
75 |
76 | Since we are testing DDoS-type scenarios, we are not going to allow any ramp-up. All clients will be hitting the servers from the beginning.
77 |
78 | Results:
79 |
80 | |Scenario|Queries/Minute|Queries/Second|
81 | |-|-|-|
82 | |1M queued queries for locally resolved hosts|1.3M|21,666|
83 | |1M queued queries for locally resolved, CNAME'd hosts|1.276M|21,417|
84 | |1M queries, but by 100 users, no ramp-up|4.599M|76,650|
85 | |1M queries, 100 users, flattening enabled|4.623M|77,050|
86 | |1M queries, bump to 1,000 users|3.2M|53,333|
87 |
88 | Observations:
89 | - If we distribute across 1000 users rather than 100, threading starts degrading.
90 | - Flattening doesn't provide the expected level of improvement.
91 |
92 | 
93 |
94 | Latency is pretty good, too.
95 |
96 | ## Mig testing
97 |
98 | https://github.com/infobloxopen/dnstools/tree/master/mig
99 |
100 | ```
101 | ./mig -s 192.168.1.189 -n 1000000 -d domains.lst -o perf.json
102 | python2 ../analyser/fit.py results/perf.json
103 | ```
104 |
105 | Results:
106 |
107 | |Rule Engine|Queries/Minute|Queries/Second|
108 | |-|-|-|
109 | |Enabled|6.7M|111,677|
110 | |Disabled|6.79M|113,181|
111 |
112 | Again, a somewhat unexpected result: a lightly loaded rule engine has almost no impact on the server's performance.
113 |
114 |
115 | # Todo
116 |
117 | ## Cache improvements
118 |
119 | - If flattening is enabled, we should cache the flattened version.
120 | - When flattening, what about recursed and fragmented answers?
121 |
122 | ## Circuit Breaker (when recursing)/Rate Limiter
123 |
124 | Because, realistically, it is better to fail some queries if this will allow them to succeed later.
125 |
126 | Rate Limiter: should be limiting some misbehaving clients. Problem: how do we identify a "Client?"
127 | - Is a client a single IP address? If it's a site DNS proxying to us, then it may be allowed higher traffic levels
128 | - Should we throttle a combination of source + queries?
129 |
130 | # FAQ
131 |
132 | Q: I noticed that you are storing similar records in separate structures. For instance, there is one entry for a A (v4) record,
133 | and another entry for its AAAA (v6) counterpart. This is wasteful!
134 |
135 | A: You are correct. However, I should not store both entries using the same key because they can both be capitalized differently.
136 | And, little known fact, capitalization in DNS can be a security feature.
137 |
138 | Q: What's that about capitalization?
139 |
140 | A: KittenDNS makes sure that the response to a query returns the host capitalized exactly as it was in the query.
141 | This is a protection scheme against DNS poisoning, known as the '0x20' trick.
142 |
143 | # Misc
144 |
145 |
146 |
--------------------------------------------------------------------------------
/assets/jmeter-kittendns.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fusion/kittendns/f0f533a7ee78cf37ebedcbde101e77cd600d9861/assets/jmeter-kittendns.png
--------------------------------------------------------------------------------
/assets/kittendns.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fusion/kittendns/f0f533a7ee78cf37ebedcbde101e77cd600d9861/assets/kittendns.png
--------------------------------------------------------------------------------
/builders/records.go:
--------------------------------------------------------------------------------
1 | package builders
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/miekg/dns"
7 | )
8 |
9 | func NewRR(recordType uint16, query string, host string, ip string, ttl uint32) (dns.RR, error) {
10 | textType := "A"
11 | if recordType == dns.TypeAAAA {
12 | textType = "AAAA"
13 | }
14 | rr, err := dns.NewRR(
15 | fmt.Sprintf(
16 | "%s %d %s %s",
17 | query,
18 | ttl,
19 | textType,
20 | ip))
21 | return rr, err
22 | }
23 |
24 | func NewCNAME(query string, target string, ttl uint32) dns.RR {
25 | alias := new(dns.CNAME)
26 | alias.Hdr = dns.RR_Header{
27 | Name: query,
28 | Rrtype: dns.TypeCNAME,
29 | Class: dns.ClassINET,
30 | Ttl: ttl}
31 | alias.Target = target
32 | return alias
33 | }
34 |
35 | func NewSRV(query string, target string, port uint16, priority uint16, weight uint16, ttl uint32) dns.RR {
36 | srv := new(dns.SRV)
37 | srv.Hdr = dns.RR_Header{
38 | Name: query,
39 | Rrtype: dns.TypeSRV,
40 | Class: dns.ClassINET,
41 | Ttl: ttl,
42 | Rdlength: 0}
43 | srv.Port = port
44 | srv.Priority = priority
45 | srv.Weight = weight
46 | srv.Target = target
47 | return srv
48 | }
49 |
50 | func NewTXT(query string, target string, ttl uint32) dns.RR {
51 | srv := new(dns.TXT)
52 | srv.Hdr = dns.RR_Header{
53 | Name: query,
54 | Rrtype: dns.TypeTXT,
55 | Class: dns.ClassINET,
56 | Ttl: ttl,
57 | Rdlength: 0}
58 | srv.Txt = []string{target}
59 | return srv
60 | }
61 |
62 | func NewMX(query string, host string, priority uint16, ttl uint32) dns.RR {
63 | mailer := new(dns.MX)
64 | mailer.Hdr = dns.RR_Header{
65 | Name: query,
66 | Rrtype: dns.TypeMX,
67 | Class: dns.ClassINET,
68 | Ttl: ttl}
69 | mailer.Mx = host
70 | mailer.Preference = priority
71 | return mailer
72 | }
73 |
74 | func NewNS(query string, host string, ttl uint32) dns.RR {
75 | nameserver := new(dns.NS)
76 | nameserver.Hdr = dns.RR_Header{
77 | Name: query,
78 | Rrtype: dns.TypeNS,
79 | Class: dns.ClassINET,
80 | Ttl: ttl}
81 | nameserver.Ns = host
82 | return nameserver
83 | }
84 |
85 | func NewSOA(origin string, ns string, mbox string, serial uint32) dns.RR {
86 | soa := new(dns.SOA)
87 | soa.Hdr = dns.RR_Header{
88 | Name: origin,
89 | Rrtype: dns.TypeSOA,
90 | Class: dns.ClassINET,
91 | Ttl: 14400,
92 | Rdlength: 0}
93 | soa.Ns = fmt.Sprintf("%s.", ns)
94 | soa.Mbox = fmt.Sprintf("%s.", mbox)
95 | soa.Serial = serial
96 | soa.Refresh = 86400
97 | soa.Retry = 7200
98 | soa.Expire = (86400 + 7200*2)
99 | soa.Minttl = 7200
100 | return soa
101 | }
102 |
--------------------------------------------------------------------------------
/cache/rccache.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/miekg/dns"
7 | )
8 |
9 | type RcCacheEntry struct {
10 | Type uint16
11 | ExpireTS int64
12 | Targets []dns.RR
13 | }
14 |
15 | type RcCache struct {
16 | Entries map[string]RcCacheEntry
17 | BackRef map[string]string
18 | }
19 |
20 | func (c *RcCache) Get(name string) ([]dns.RR, bool, uint32) {
21 | if entry, ok := c.Entries[name]; ok {
22 | remaining := uint32(entry.ExpireTS - time.Now().Unix())
23 | if remaining > 0 {
24 | return entry.Targets, true, remaining
25 | }
26 | delete(c.Entries, name)
27 | }
28 | return nil, false, 0
29 | }
30 |
31 | type MaybeFlatten int
32 |
33 | const (
34 | Flatten MaybeFlatten = iota
35 | DoNotFlatten
36 | )
37 |
38 | func (c *RcCache) Set(flatten MaybeFlatten, name string, dnsType uint16, targets []dns.RR, ttl uint32) {
39 | if c.Entries == nil {
40 | c.Entries = make(map[string]RcCacheEntry)
41 | }
42 | if c.BackRef == nil {
43 | c.BackRef = make(map[string]string)
44 | }
45 |
46 | expireTs := time.Now().Unix() + int64(ttl)
47 |
48 | c.Entries[name] = RcCacheEntry{
49 | Type: dnsType,
50 | ExpireTS: expireTs,
51 | Targets: targets,
52 | }
53 | if flatten == Flatten {
54 | switch dnsType {
55 | case dns.TypeCNAME:
56 | if len(targets) > 0 {
57 | c.BackRef[targets[0].(*dns.CNAME).Target] = name
58 | }
59 | case dns.TypeA:
60 | for {
61 | backRefName, ok := c.BackRef[name]
62 | if !ok {
63 | break
64 | }
65 | backRefEntry := c.Entries[backRefName]
66 | c.Entries[backRefName] = RcCacheEntry{
67 | Type: dns.TypeA,
68 | ExpireTS: backRefEntry.ExpireTS,
69 | Targets: targets,
70 | }
71 | name = backRefName
72 | }
73 | }
74 | }
75 | }
76 |
77 | // TODO Create ageing function that will clean up both entries and backrefs
78 |
--------------------------------------------------------------------------------
/config.toml.template:
--------------------------------------------------------------------------------
1 | [settings]
2 | debuglevel = 1
3 | autoreload = true
4 | listen = 53
5 | # Cache recursive queries.
6 | cache = true
7 | # Flatten CNAME chains down to A records. Not fully functional yet.
8 | flatten = false
9 | # Will return a single record, round-robin, when multiple records are available.
10 | loadbalance = true
11 |
12 | # A parent DNS to recurse non authoritative queries to
13 | [settings.parent]
14 | address = "192.168.1.254"
15 |
16 | # A few rules. Use a natural language engine similar to the one I included
17 | # in https://github.com/fusion/mailbiter
18 |
19 | [[rule]]
20 | condition = "remoteip != '192.168.1.19' and host startsWith 'google.'"
21 | action = "rewrite '142.250.189.14'"
22 |
23 | [[rule]]
24 | condition = "remoteip != '192.168.1.19'"
25 | action = "drop"
26 |
27 | [[rule]]
28 | condition = "not (remoteip startsWith '192.168.1')"
29 | action = "inspect"
30 |
31 | # Zone definitions
32 |
33 | [[zone]]
34 | origin = "example.com."
35 | TTL = 60
36 |
37 | # SOA information
38 | [zone.auth]
39 | ns = "dns1.example.com"
40 | email = "chris.example.com"
41 | serial = 1
42 |
43 | # Top-level record
44 | [[zone.record]]
45 | host = "@"
46 | ipv4 = "192.168.1.1"
47 |
48 | # An A record, with multiple replies
49 | # (can be load balanced, either in server or in client)
50 | [[zone.record]]
51 | host = "test"
52 | ipv4 = "192.168.1.2"
53 | ipv4s = ["192.168.2.2", "192.168.3.2"]
54 |
55 | # An SRV record
56 | [[zone.record]]
57 | Service = "sip"
58 | Proto = "tcp"
59 | Priority = 10
60 | Weight = 5
61 | Target = "test"
62 |
63 | # A CNAME record
64 | [[zone.record]]
65 | host = "bogus2"
66 | aliased = "test"
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "strings"
7 |
8 | "github.com/fusion/kittendns/secret"
9 | "github.com/hydronica/toml"
10 | )
11 |
12 | type Parent struct {
13 | // May be suffixed with [:port]
14 | Address string
15 | }
16 | type Settings struct {
17 | DebugLevel uint8
18 | AutoReload bool
19 | // ["ip:port", ...]
20 | // Each instance should identify a working ip address from this list
21 | Listeners []string
22 | // If true, when multiple records are found for a domain, only one is returned
23 | // A different one every time.
24 | // If false, all records are returned.
25 | LoadBalance bool
26 | // In lazy mode, when a CNAME is found, the CNAME is returned and the
27 | // target is not resolved.
28 | Lazy bool
29 | // A caching DNS will not refresh its knowledge until Ttl value expires
30 | Cache bool
31 | // If true, CNAME chains will be merged into a single record
32 | Flatten bool
33 | // Disabling the rule engine speeds up simple DNS lookups
34 | DisableRuleEngine bool
35 |
36 | // DNS to recurse to when an authoritative answer does not exist.
37 | Parent Parent
38 | }
39 |
40 | type Auth struct {
41 | Ns string
42 | Email string
43 | Serial uint32
44 | }
45 |
46 | type Mailer struct {
47 | Host string
48 | Priority uint16
49 | TTL uint32
50 | NoMailer bool
51 | }
52 |
53 | type NameServer struct {
54 | Host string
55 | Target string
56 | TTL uint32
57 | }
58 |
59 | type Record struct {
60 | Type uint16
61 |
62 | Host string
63 |
64 | // A
65 | IPv4 string
66 | IPv4s []string
67 |
68 | // AAAA
69 | IPv6 string
70 | IPv6s []string
71 |
72 | // CNAME
73 | Aliased string
74 |
75 | // SRV
76 | Service string
77 | Priority uint16
78 | Proto string
79 | Weight uint16
80 | Port uint16
81 | Target string
82 | NoService bool
83 |
84 | // TXT
85 | Text string
86 |
87 | Origin string
88 | TTL uint32
89 | Auth Auth
90 | }
91 |
92 | type Zone struct {
93 | Origin string
94 | TTL uint32
95 | Auth Auth
96 | Record []Record
97 | Mailer []Mailer
98 | NameServer []NameServer
99 | }
100 |
101 | type Rule struct {
102 | Condition string
103 | Action string
104 | }
105 |
106 | type Plugin struct {
107 | Enabled bool
108 | Path string
109 | PreHandler string
110 | PostHandler string
111 | Arguments []string
112 | Monitor []string
113 | }
114 |
115 | type Config struct {
116 | Settings Settings
117 | Zone []Zone
118 | Records map[string]Record
119 | Rule []Rule
120 | Plugin []Plugin
121 | Monitor []string
122 | Secret secret.Secret
123 | }
124 |
125 | func GetConfig() *Config {
126 | var config Config
127 | if _, err := toml.DecodeFile("config.toml", &config); err != nil {
128 | log.Fatal(err)
129 | }
130 | var secret secret.Secret
131 | // TODO Place secret in another, convenient location!
132 | if _, err := toml.DecodeFile("secret.toml", &secret); err != nil {
133 | log.Fatal(err)
134 | }
135 | config.Secret = secret
136 |
137 | // Default parent dns to port 53 is not set, but parent _is_ set
138 | if config.Settings.Parent.Address != "" && !strings.Contains(config.Settings.Parent.Address, ":") {
139 | config.Settings.Parent.Address = fmt.Sprintf("%s:%d", config.Settings.Parent.Address, 53)
140 | }
141 | return &config
142 | }
143 |
--------------------------------------------------------------------------------
/dns/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | target: 40%
6 | threshold: null
7 | patch: false
8 | changes: false
9 |
--------------------------------------------------------------------------------
/dns/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "Code scanning - action"
2 |
3 | on:
4 | push:
5 | branches: [master, ]
6 | pull_request:
7 | branches: [master]
8 | schedule:
9 | - cron: '0 23 * * 5'
10 |
11 | jobs:
12 | CodeQL-Build:
13 |
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout repository
18 | uses: actions/checkout@v2
19 | with:
20 | fetch-depth: 2
21 |
22 | - run: git checkout HEAD^2
23 | if: ${{ github.event_name == 'pull_request' }}
24 |
25 | - name: Initialize CodeQL
26 | uses: github/codeql-action/init@v1
27 |
28 | - name: Autobuild
29 | uses: github/codeql-action/autobuild@v1
30 |
31 | - name: Perform CodeQL Analysis
32 | uses: github/codeql-action/analyze@v1
33 |
--------------------------------------------------------------------------------
/dns/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 | on: [push, pull_request]
3 | jobs:
4 |
5 | build:
6 | name: Build and Test
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | go: [ 1.15.x, 1.16.x ]
11 | steps:
12 |
13 | - name: Set up Go
14 | uses: actions/setup-go@v2
15 | with:
16 | go-version: ${{ matrix.go }}
17 |
18 | - name: Check out code
19 | uses: actions/checkout@v2
20 |
21 | - name: Build
22 | run: go build -v ./...
23 |
24 | - name: Test
25 | run: go test -v ./...
26 |
--------------------------------------------------------------------------------
/dns/.gitignore:
--------------------------------------------------------------------------------
1 | *.6
2 | tags
3 | test.out
4 | a.out
5 |
--------------------------------------------------------------------------------
/dns/AUTHORS:
--------------------------------------------------------------------------------
1 | Miek Gieben
2 |
--------------------------------------------------------------------------------
/dns/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @miekg @tmthrgd
2 |
--------------------------------------------------------------------------------
/dns/CONTRIBUTORS:
--------------------------------------------------------------------------------
1 | Alex A. Skinner
2 | Andrew Tunnell-Jones
3 | Ask Bjørn Hansen
4 | Dave Cheney
5 | Dusty Wilson
6 | Marek Majkowski
7 | Peter van Dijk
8 | Omri Bahumi
9 | Alex Sergeyev
10 | James Hartig
11 |
--------------------------------------------------------------------------------
/dns/COPYRIGHT:
--------------------------------------------------------------------------------
1 | Copyright 2009 The Go Authors. All rights reserved. Use of this source code
2 | is governed by a BSD-style license that can be found in the LICENSE file.
3 | Extensions of the original work are copyright (c) 2011 Miek Gieben
4 |
5 | Copyright 2011 Miek Gieben. All rights reserved. Use of this source code is
6 | governed by a BSD-style license that can be found in the LICENSE file.
7 |
8 | Copyright 2014 CloudFlare. All rights reserved. Use of this source code is
9 | governed by a BSD-style license that can be found in the LICENSE file.
10 |
--------------------------------------------------------------------------------
/dns/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009 The Go Authors. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following disclaimer
11 | in the documentation and/or other materials provided with the
12 | distribution.
13 | * Neither the name of Google Inc. nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 | As this is fork of the official Go code the same license applies.
30 | Extensions of the original work are copyright (c) 2011 Miek Gieben
31 |
--------------------------------------------------------------------------------
/dns/Makefile.fuzz:
--------------------------------------------------------------------------------
1 | # Makefile for fuzzing
2 | #
3 | # Use go-fuzz and needs the tools installed.
4 | # See https://blog.cloudflare.com/dns-parser-meet-go-fuzzer/
5 | #
6 | # Installing go-fuzz:
7 | # $ make -f Makefile.fuzz get
8 | # Installs:
9 | # * github.com/dvyukov/go-fuzz/go-fuzz
10 | # * get github.com/dvyukov/go-fuzz/go-fuzz-build
11 |
12 | all: build
13 |
14 | .PHONY: build
15 | build:
16 | go-fuzz-build -tags fuzz github.com/miekg/dns
17 |
18 | .PHONY: build-newrr
19 | build-newrr:
20 | go-fuzz-build -func FuzzNewRR -tags fuzz github.com/miekg/dns
21 |
22 | .PHONY: fuzz
23 | fuzz:
24 | go-fuzz -bin=dns-fuzz.zip -workdir=fuzz
25 |
26 | .PHONY: get
27 | get:
28 | go get github.com/dvyukov/go-fuzz/go-fuzz
29 | go get github.com/dvyukov/go-fuzz/go-fuzz-build
30 |
31 | .PHONY: clean
32 | clean:
33 | rm *-fuzz.zip
34 |
--------------------------------------------------------------------------------
/dns/Makefile.release:
--------------------------------------------------------------------------------
1 | # Makefile for releasing.
2 | #
3 | # The release is controlled from version.go. The version found there is
4 | # used to tag the git repo, we're not building any artifacts so there is nothing
5 | # to upload to github.
6 | #
7 | # * Up the version in version.go
8 | # * Run: make -f Makefile.release release
9 | # * will *commit* your change with 'Release $VERSION'
10 | # * push to github
11 | #
12 |
13 | define GO
14 | //+build ignore
15 |
16 | package main
17 |
18 | import (
19 | "fmt"
20 |
21 | "github.com/miekg/dns"
22 | )
23 |
24 | func main() {
25 | fmt.Println(dns.Version.String())
26 | }
27 | endef
28 |
29 | $(file > version_release.go,$(GO))
30 | VERSION:=$(shell go run version_release.go)
31 | TAG="v$(VERSION)"
32 |
33 | all:
34 | @echo Use the \'release\' target to start a release $(VERSION)
35 | rm -f version_release.go
36 |
37 | .PHONY: release
38 | release: commit push
39 | @echo Released $(VERSION)
40 | rm -f version_release.go
41 |
42 | .PHONY: commit
43 | commit:
44 | @echo Committing release $(VERSION)
45 | git commit -am"Release $(VERSION)"
46 | git tag $(TAG)
47 |
48 | .PHONY: push
49 | push:
50 | @echo Pushing release $(VERSION) to master
51 | git push --tags
52 | git push
53 |
--------------------------------------------------------------------------------
/dns/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/miekg/dns)
2 | [](https://codecov.io/github/miekg/dns?branch=master)
3 | [](https://goreportcard.com/report/miekg/dns)
4 | [](https://godoc.org/github.com/miekg/dns)
5 |
6 | # Alternative (more granular) approach to a DNS library
7 |
8 | > Less is more.
9 |
10 | Complete and usable DNS library. All Resource Records are supported, including the DNSSEC types.
11 | It follows a lean and mean philosophy. If there is stuff you should know as a DNS programmer there
12 | isn't a convenience function for it. Server side and client side programming is supported, i.e. you
13 | can build servers and resolvers with it.
14 |
15 | We try to keep the "master" branch as sane as possible and at the bleeding edge of standards,
16 | avoiding breaking changes wherever reasonable. We support the last two versions of Go.
17 |
18 | # Goals
19 |
20 | * KISS;
21 | * Fast;
22 | * Small API. If it's easy to code in Go, don't make a function for it.
23 |
24 | # Users
25 |
26 | A not-so-up-to-date-list-that-may-be-actually-current:
27 |
28 | * https://github.com/coredns/coredns
29 | * https://github.com/abh/geodns
30 | * https://github.com/baidu/bfe
31 | * http://www.statdns.com/
32 | * http://www.dnsinspect.com/
33 | * https://github.com/chuangbo/jianbing-dictionary-dns
34 | * http://www.dns-lg.com/
35 | * https://github.com/fcambus/rrda
36 | * https://github.com/kenshinx/godns
37 | * https://github.com/skynetservices/skydns
38 | * https://github.com/hashicorp/consul
39 | * https://github.com/DevelopersPL/godnsagent
40 | * https://github.com/duedil-ltd/discodns
41 | * https://github.com/StalkR/dns-reverse-proxy
42 | * https://github.com/tianon/rawdns
43 | * https://mesosphere.github.io/mesos-dns/
44 | * https://github.com/fcambus/statzone
45 | * https://github.com/benschw/dns-clb-go
46 | * https://github.com/corny/dnscheck for
47 | * https://github.com/miekg/unbound
48 | * https://github.com/miekg/exdns
49 | * https://dnslookup.org
50 | * https://github.com/looterz/grimd
51 | * https://github.com/phamhongviet/serf-dns
52 | * https://github.com/mehrdadrad/mylg
53 | * https://github.com/bamarni/dockness
54 | * https://github.com/fffaraz/microdns
55 | * https://github.com/ipdcode/hades
56 | * https://github.com/StackExchange/dnscontrol/
57 | * https://www.dnsperf.com/
58 | * https://dnssectest.net/
59 | * https://github.com/oif/apex
60 | * https://github.com/jedisct1/dnscrypt-proxy
61 | * https://github.com/jedisct1/rpdns
62 | * https://github.com/xor-gate/sshfp
63 | * https://github.com/rs/dnstrace
64 | * https://blitiri.com.ar/p/dnss ([github mirror](https://github.com/albertito/dnss))
65 | * https://render.com
66 | * https://github.com/peterzen/goresolver
67 | * https://github.com/folbricht/routedns
68 | * https://domainr.com/
69 | * https://zonedb.org/
70 | * https://router7.org/
71 | * https://github.com/fortio/dnsping
72 | * https://github.com/Luzilla/dnsbl_exporter
73 | * https://github.com/bodgit/tsig
74 | * https://github.com/v2fly/v2ray-core (test only)
75 | * https://kuma.io/
76 | * https://www.misaka.io/services/dns
77 | * https://ping.sx/dig
78 | * https://fleetdeck.io/
79 | * https://github.com/markdingo/autoreverse
80 |
81 |
82 | Send pull request if you want to be listed here.
83 |
84 | # Features
85 |
86 | * UDP/TCP queries, IPv4 and IPv6
87 | * RFC 1035 zone file parsing ($INCLUDE, $ORIGIN, $TTL and $GENERATE (for all record types) are supported
88 | * Fast
89 | * Server side programming (mimicking the net/http package)
90 | * Client side programming
91 | * DNSSEC: signing, validating and key generation for DSA, RSA, ECDSA and Ed25519
92 | * EDNS0, NSID, Cookies
93 | * AXFR/IXFR
94 | * TSIG, SIG(0)
95 | * DNS over TLS (DoT): encrypted connection between client and server over TCP
96 | * DNS name compression
97 |
98 | Have fun!
99 |
100 | Miek Gieben - 2010-2012 -
101 | DNS Authors 2012-
102 |
103 | # Building
104 |
105 | This library uses Go modules and uses semantic versioning. Building is done with the `go` tool, so
106 | the following should work:
107 |
108 | go get github.com/miekg/dns
109 | go build github.com/miekg/dns
110 |
111 | ## Examples
112 |
113 | A short "how to use the API" is at the beginning of doc.go (this also will show when you call `godoc
114 | github.com/miekg/dns`).
115 |
116 | Example programs can be found in the `github.com/miekg/exdns` repository.
117 |
118 | ## Supported RFCs
119 |
120 | *all of them*
121 |
122 | * 103{4,5} - DNS standard
123 | * 1348 - NSAP record (removed the record)
124 | * 1982 - Serial Arithmetic
125 | * 1876 - LOC record
126 | * 1995 - IXFR
127 | * 1996 - DNS notify
128 | * 2136 - DNS Update (dynamic updates)
129 | * 2181 - RRset definition - there is no RRset type though, just []RR
130 | * 2537 - RSAMD5 DNS keys
131 | * 2065 - DNSSEC (updated in later RFCs)
132 | * 2671 - EDNS record
133 | * 2782 - SRV record
134 | * 2845 - TSIG record
135 | * 2915 - NAPTR record
136 | * 2929 - DNS IANA Considerations
137 | * 3110 - RSASHA1 DNS keys
138 | * 3123 - APL record
139 | * 3225 - DO bit (DNSSEC OK)
140 | * 340{1,2,3} - NAPTR record
141 | * 3445 - Limiting the scope of (DNS)KEY
142 | * 3597 - Unknown RRs
143 | * 403{3,4,5} - DNSSEC + validation functions
144 | * 4255 - SSHFP record
145 | * 4343 - Case insensitivity
146 | * 4408 - SPF record
147 | * 4509 - SHA256 Hash in DS
148 | * 4592 - Wildcards in the DNS
149 | * 4635 - HMAC SHA TSIG
150 | * 4701 - DHCID
151 | * 4892 - id.server
152 | * 5001 - NSID
153 | * 5155 - NSEC3 record
154 | * 5205 - HIP record
155 | * 5702 - SHA2 in the DNS
156 | * 5936 - AXFR
157 | * 5966 - TCP implementation recommendations
158 | * 6605 - ECDSA
159 | * 6725 - IANA Registry Update
160 | * 6742 - ILNP DNS
161 | * 6840 - Clarifications and Implementation Notes for DNS Security
162 | * 6844 - CAA record
163 | * 6891 - EDNS0 update
164 | * 6895 - DNS IANA considerations
165 | * 6944 - DNSSEC DNSKEY Algorithm Status
166 | * 6975 - Algorithm Understanding in DNSSEC
167 | * 7043 - EUI48/EUI64 records
168 | * 7314 - DNS (EDNS) EXPIRE Option
169 | * 7477 - CSYNC RR
170 | * 7828 - edns-tcp-keepalive EDNS0 Option
171 | * 7553 - URI record
172 | * 7858 - DNS over TLS: Initiation and Performance Considerations
173 | * 7871 - EDNS0 Client Subnet
174 | * 7873 - Domain Name System (DNS) Cookies
175 | * 8080 - EdDSA for DNSSEC
176 | * 8499 - DNS Terminology
177 | * 8659 - DNS Certification Authority Authorization (CAA) Resource Record
178 | * 8914 - Extended DNS Errors
179 | * 8976 - Message Digest for DNS Zones (ZONEMD RR)
180 |
181 | ## Loosely Based Upon
182 |
183 | * ldns -
184 | * NSD -
185 | * Net::DNS -
186 | * GRONG -
187 |
--------------------------------------------------------------------------------
/dns/acceptfunc.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // MsgAcceptFunc is used early in the server code to accept or reject a message with RcodeFormatError.
4 | // It returns a MsgAcceptAction to indicate what should happen with the message.
5 | type MsgAcceptFunc func(dh Header) MsgAcceptAction
6 |
7 | // DefaultMsgAcceptFunc checks the request and will reject if:
8 | //
9 | // * isn't a request (don't respond in that case)
10 | //
11 | // * opcode isn't OpcodeQuery or OpcodeNotify
12 | //
13 | // * Zero bit isn't zero
14 | //
15 | // * does not have exactly 1 question in the question section
16 | //
17 | // * has more than 1 RR in the Answer section
18 | //
19 | // * has more than 0 RRs in the Authority section
20 | //
21 | // * has more than 2 RRs in the Additional section
22 | //
23 | var DefaultMsgAcceptFunc MsgAcceptFunc = defaultMsgAcceptFunc
24 |
25 | // MsgAcceptAction represents the action to be taken.
26 | type MsgAcceptAction int
27 |
28 | // Allowed returned values from a MsgAcceptFunc.
29 | const (
30 | MsgAccept MsgAcceptAction = iota // Accept the message
31 | MsgReject // Reject the message with a RcodeFormatError
32 | MsgIgnore // Ignore the error and send nothing back.
33 | MsgRejectNotImplemented // Reject the message with a RcodeNotImplemented
34 | )
35 |
36 | func defaultMsgAcceptFunc(dh Header) MsgAcceptAction {
37 | if isResponse := dh.Bits&_QR != 0; isResponse {
38 | return MsgIgnore
39 | }
40 |
41 | // Don't allow dynamic updates, because then the sections can contain a whole bunch of RRs.
42 | opcode := int(dh.Bits>>11) & 0xF
43 | if opcode != OpcodeQuery && opcode != OpcodeNotify {
44 | return MsgRejectNotImplemented
45 | }
46 |
47 | if dh.Qdcount != 1 {
48 | return MsgReject
49 | }
50 | // NOTIFY requests can have a SOA in the ANSWER section. See RFC 1996 Section 3.7 and 3.11.
51 | if dh.Ancount > 1 {
52 | return MsgReject
53 | }
54 | // IXFR request could have one SOA RR in the NS section. See RFC 1995, section 3.
55 | if dh.Nscount > 1 {
56 | return MsgReject
57 | }
58 | if dh.Arcount > 2 {
59 | return MsgReject
60 | }
61 | return MsgAccept
62 | }
63 |
--------------------------------------------------------------------------------
/dns/acceptfunc_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "context"
5 | "testing"
6 | )
7 |
8 | func TestAcceptNotify(t *testing.T) {
9 | HandleFunc("example.org.", handleNotify)
10 | s, addrstr, _, err := RunLocalUDPServer(":0")
11 | if err != nil {
12 | t.Fatalf("unable to run test server: %v", err)
13 | }
14 | defer s.Shutdown()
15 |
16 | m := new(Msg)
17 | m.SetNotify("example.org.")
18 | // Set a SOA hint in the answer section, this is allowed according to RFC 1996.
19 | soa, _ := NewRR("example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2018112827 7200 3600 1209600 3600")
20 | m.Answer = []RR{soa}
21 |
22 | c := new(Client)
23 | resp, _, err := c.Exchange(m, addrstr)
24 | if err != nil {
25 | t.Errorf("failed to exchange: %v", err)
26 | }
27 | if resp.Rcode != RcodeSuccess {
28 | t.Errorf("expected %s, got %s", RcodeToString[RcodeSuccess], RcodeToString[resp.Rcode])
29 | }
30 | }
31 |
32 | func handleNotify(ctx context.Context, w ResponseWriter, req *Msg) {
33 | m := new(Msg)
34 | m.SetReply(req)
35 | w.WriteMsg(m)
36 | }
37 |
--------------------------------------------------------------------------------
/dns/clientconfig.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "bufio"
5 | "io"
6 | "os"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | // ClientConfig wraps the contents of the /etc/resolv.conf file.
12 | type ClientConfig struct {
13 | Servers []string // servers to use
14 | Search []string // suffixes to append to local name
15 | Port string // what port to use
16 | Ndots int // number of dots in name to trigger absolute lookup
17 | Timeout int // seconds before giving up on packet
18 | Attempts int // lost packets before giving up on server, not used in the package dns
19 | }
20 |
21 | // ClientConfigFromFile parses a resolv.conf(5) like file and returns
22 | // a *ClientConfig.
23 | func ClientConfigFromFile(resolvconf string) (*ClientConfig, error) {
24 | file, err := os.Open(resolvconf)
25 | if err != nil {
26 | return nil, err
27 | }
28 | defer file.Close()
29 | return ClientConfigFromReader(file)
30 | }
31 |
32 | // ClientConfigFromReader works like ClientConfigFromFile but takes an io.Reader as argument
33 | func ClientConfigFromReader(resolvconf io.Reader) (*ClientConfig, error) {
34 | c := new(ClientConfig)
35 | scanner := bufio.NewScanner(resolvconf)
36 | c.Servers = make([]string, 0)
37 | c.Search = make([]string, 0)
38 | c.Port = "53"
39 | c.Ndots = 1
40 | c.Timeout = 5
41 | c.Attempts = 2
42 |
43 | for scanner.Scan() {
44 | if err := scanner.Err(); err != nil {
45 | return nil, err
46 | }
47 | line := scanner.Text()
48 | f := strings.Fields(line)
49 | if len(f) < 1 {
50 | continue
51 | }
52 | switch f[0] {
53 | case "nameserver": // add one name server
54 | if len(f) > 1 {
55 | // One more check: make sure server name is
56 | // just an IP address. Otherwise we need DNS
57 | // to look it up.
58 | name := f[1]
59 | c.Servers = append(c.Servers, name)
60 | }
61 |
62 | case "domain": // set search path to just this domain
63 | if len(f) > 1 {
64 | c.Search = make([]string, 1)
65 | c.Search[0] = f[1]
66 | } else {
67 | c.Search = make([]string, 0)
68 | }
69 |
70 | case "search": // set search path to given servers
71 | c.Search = append([]string(nil), f[1:]...)
72 |
73 | case "options": // magic options
74 | for _, s := range f[1:] {
75 | switch {
76 | case len(s) >= 6 && s[:6] == "ndots:":
77 | n, _ := strconv.Atoi(s[6:])
78 | if n < 0 {
79 | n = 0
80 | } else if n > 15 {
81 | n = 15
82 | }
83 | c.Ndots = n
84 | case len(s) >= 8 && s[:8] == "timeout:":
85 | n, _ := strconv.Atoi(s[8:])
86 | if n < 1 {
87 | n = 1
88 | }
89 | c.Timeout = n
90 | case len(s) >= 9 && s[:9] == "attempts:":
91 | n, _ := strconv.Atoi(s[9:])
92 | if n < 1 {
93 | n = 1
94 | }
95 | c.Attempts = n
96 | case s == "rotate":
97 | /* not imp */
98 | }
99 | }
100 | }
101 | }
102 | return c, nil
103 | }
104 |
105 | // NameList returns all of the names that should be queried based on the
106 | // config. It is based off of go's net/dns name building, but it does not
107 | // check the length of the resulting names.
108 | func (c *ClientConfig) NameList(name string) []string {
109 | // if this domain is already fully qualified, no append needed.
110 | if IsFqdn(name) {
111 | return []string{name}
112 | }
113 |
114 | // Check to see if the name has more labels than Ndots. Do this before making
115 | // the domain fully qualified.
116 | hasNdots := CountLabel(name) > c.Ndots
117 | // Make the domain fully qualified.
118 | name = Fqdn(name)
119 |
120 | // Make a list of names based off search.
121 | names := []string{}
122 |
123 | // If name has enough dots, try that first.
124 | if hasNdots {
125 | names = append(names, name)
126 | }
127 | for _, s := range c.Search {
128 | names = append(names, Fqdn(name+s))
129 | }
130 | // If we didn't have enough dots, try after suffixes.
131 | if !hasNdots {
132 | names = append(names, name)
133 | }
134 | return names
135 | }
136 |
--------------------------------------------------------------------------------
/dns/clientconfig_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 | "testing"
9 | )
10 |
11 | const normal string = `
12 | # Comment
13 | domain somedomain.com
14 | nameserver 10.28.10.2
15 | nameserver 11.28.10.1
16 | `
17 |
18 | const missingNewline string = `
19 | domain somedomain.com
20 | nameserver 10.28.10.2
21 | nameserver 11.28.10.1` // <- NOTE: NO newline.
22 |
23 | func testConfig(t *testing.T, data string) {
24 | cc, err := ClientConfigFromReader(strings.NewReader(data))
25 | if err != nil {
26 | t.Errorf("error parsing resolv.conf: %v", err)
27 | }
28 | if l := len(cc.Servers); l != 2 {
29 | t.Errorf("incorrect number of nameservers detected: %d", l)
30 | }
31 | if l := len(cc.Search); l != 1 {
32 | t.Errorf("domain directive not parsed correctly: %v", cc.Search)
33 | } else {
34 | if cc.Search[0] != "somedomain.com" {
35 | t.Errorf("domain is unexpected: %v", cc.Search[0])
36 | }
37 | }
38 | }
39 |
40 | func TestNameserver(t *testing.T) { testConfig(t, normal) }
41 | func TestMissingFinalNewLine(t *testing.T) { testConfig(t, missingNewline) }
42 |
43 | func TestNdots(t *testing.T) {
44 | ndotsVariants := map[string]int{
45 | "options ndots:0": 0,
46 | "options ndots:1": 1,
47 | "options ndots:15": 15,
48 | "options ndots:16": 15,
49 | "options ndots:-1": 0,
50 | "": 1,
51 | }
52 |
53 | for data := range ndotsVariants {
54 | cc, err := ClientConfigFromReader(strings.NewReader(data))
55 | if err != nil {
56 | t.Errorf("error parsing resolv.conf: %v", err)
57 | }
58 | if cc.Ndots != ndotsVariants[data] {
59 | t.Errorf("Ndots not properly parsed: (Expected: %d / Was: %d)", ndotsVariants[data], cc.Ndots)
60 | }
61 | }
62 | }
63 |
64 | func TestClientConfigFromReaderAttempts(t *testing.T) {
65 | testCases := []struct {
66 | data string
67 | expected int
68 | }{
69 | {data: "options attempts:0", expected: 1},
70 | {data: "options attempts:1", expected: 1},
71 | {data: "options attempts:15", expected: 15},
72 | {data: "options attempts:16", expected: 16},
73 | {data: "options attempts:-1", expected: 1},
74 | {data: "options attempt:", expected: 2},
75 | }
76 |
77 | for _, test := range testCases {
78 | test := test
79 | t.Run(strings.Replace(test.data, ":", " ", -1), func(t *testing.T) {
80 | t.Parallel()
81 |
82 | cc, err := ClientConfigFromReader(strings.NewReader(test.data))
83 | if err != nil {
84 | t.Errorf("error parsing resolv.conf: %v", err)
85 | }
86 | if cc.Attempts != test.expected {
87 | t.Errorf("A attempts not properly parsed: (Expected: %d / Was: %d)", test.expected, cc.Attempts)
88 | }
89 | })
90 | }
91 | }
92 |
93 | func TestReadFromFile(t *testing.T) {
94 | tempDir, err := ioutil.TempDir("", "")
95 | if err != nil {
96 | t.Fatalf("tempDir: %v", err)
97 | }
98 | defer os.RemoveAll(tempDir)
99 |
100 | path := filepath.Join(tempDir, "resolv.conf")
101 | if err := ioutil.WriteFile(path, []byte(normal), 0644); err != nil {
102 | t.Fatalf("writeFile: %v", err)
103 | }
104 | cc, err := ClientConfigFromFile(path)
105 | if err != nil {
106 | t.Errorf("error parsing resolv.conf: %v", err)
107 | }
108 | if l := len(cc.Servers); l != 2 {
109 | t.Errorf("incorrect number of nameservers detected: %d", l)
110 | }
111 | if l := len(cc.Search); l != 1 {
112 | t.Errorf("domain directive not parsed correctly: %v", cc.Search)
113 | } else {
114 | if cc.Search[0] != "somedomain.com" {
115 | t.Errorf("domain is unexpected: %v", cc.Search[0])
116 | }
117 | }
118 | }
119 |
120 | func TestNameListNdots1(t *testing.T) {
121 | cfg := ClientConfig{
122 | Ndots: 1,
123 | }
124 | // fqdn should be only result returned
125 | names := cfg.NameList("miek.nl.")
126 | if len(names) != 1 {
127 | t.Errorf("NameList returned != 1 names: %v", names)
128 | } else if names[0] != "miek.nl." {
129 | t.Errorf("NameList didn't return sent fqdn domain: %v", names[0])
130 | }
131 |
132 | cfg.Search = []string{
133 | "test",
134 | }
135 | // Sent domain has NDots and search
136 | names = cfg.NameList("miek.nl")
137 | if len(names) != 2 {
138 | t.Errorf("NameList returned != 2 names: %v", names)
139 | } else if names[0] != "miek.nl." {
140 | t.Errorf("NameList didn't return sent domain first: %v", names[0])
141 | } else if names[1] != "miek.nl.test." {
142 | t.Errorf("NameList didn't return search last: %v", names[1])
143 | }
144 | }
145 | func TestNameListNdots2(t *testing.T) {
146 | cfg := ClientConfig{
147 | Ndots: 2,
148 | }
149 |
150 | // Sent domain has less than NDots and search
151 | cfg.Search = []string{
152 | "test",
153 | }
154 | names := cfg.NameList("miek.nl")
155 |
156 | if len(names) != 2 {
157 | t.Errorf("NameList returned != 2 names: %v", names)
158 | } else if names[0] != "miek.nl.test." {
159 | t.Errorf("NameList didn't return search first: %v", names[0])
160 | } else if names[1] != "miek.nl." {
161 | t.Errorf("NameList didn't return sent domain last: %v", names[1])
162 | }
163 | }
164 |
165 | func TestNameListNdots0(t *testing.T) {
166 | cfg := ClientConfig{
167 | Ndots: 0,
168 | }
169 | cfg.Search = []string{
170 | "test",
171 | }
172 | // Sent domain has less than NDots and search
173 | names := cfg.NameList("miek")
174 | if len(names) != 2 {
175 | t.Errorf("NameList returned != 2 names: %v", names)
176 | } else if names[0] != "miek." {
177 | t.Errorf("NameList didn't return search first: %v", names[0])
178 | } else if names[1] != "miek.test." {
179 | t.Errorf("NameList didn't return sent domain last: %v", names[1])
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/dns/dane.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "crypto/sha256"
5 | "crypto/sha512"
6 | "crypto/x509"
7 | "encoding/hex"
8 | "errors"
9 | )
10 |
11 | // CertificateToDANE converts a certificate to a hex string as used in the TLSA or SMIMEA records.
12 | func CertificateToDANE(selector, matchingType uint8, cert *x509.Certificate) (string, error) {
13 | switch matchingType {
14 | case 0:
15 | switch selector {
16 | case 0:
17 | return hex.EncodeToString(cert.Raw), nil
18 | case 1:
19 | return hex.EncodeToString(cert.RawSubjectPublicKeyInfo), nil
20 | }
21 | case 1:
22 | h := sha256.New()
23 | switch selector {
24 | case 0:
25 | h.Write(cert.Raw)
26 | return hex.EncodeToString(h.Sum(nil)), nil
27 | case 1:
28 | h.Write(cert.RawSubjectPublicKeyInfo)
29 | return hex.EncodeToString(h.Sum(nil)), nil
30 | }
31 | case 2:
32 | h := sha512.New()
33 | switch selector {
34 | case 0:
35 | h.Write(cert.Raw)
36 | return hex.EncodeToString(h.Sum(nil)), nil
37 | case 1:
38 | h.Write(cert.RawSubjectPublicKeyInfo)
39 | return hex.EncodeToString(h.Sum(nil)), nil
40 | }
41 | }
42 | return "", errors.New("dns: bad MatchingType or Selector")
43 | }
44 |
--------------------------------------------------------------------------------
/dns/dns.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "encoding/hex"
5 | "strconv"
6 | )
7 |
8 | const (
9 | year68 = 1 << 31 // For RFC1982 (Serial Arithmetic) calculations in 32 bits.
10 | defaultTtl = 3600 // Default internal TTL.
11 |
12 | // DefaultMsgSize is the standard default for messages larger than 512 bytes.
13 | DefaultMsgSize = 4096
14 | // MinMsgSize is the minimal size of a DNS packet.
15 | MinMsgSize = 512
16 | // MaxMsgSize is the largest possible DNS packet.
17 | MaxMsgSize = 65535
18 | )
19 |
20 | // Error represents a DNS error.
21 | type Error struct{ err string }
22 |
23 | func (e *Error) Error() string {
24 | if e == nil {
25 | return "dns: "
26 | }
27 | return "dns: " + e.err
28 | }
29 |
30 | // An RR represents a resource record.
31 | type RR interface {
32 | // Header returns the header of an resource record. The header contains
33 | // everything up to the rdata.
34 | Header() *RR_Header
35 | // String returns the text representation of the resource record.
36 | String() string
37 |
38 | // copy returns a copy of the RR
39 | copy() RR
40 |
41 | // len returns the length (in octets) of the compressed or uncompressed RR in wire format.
42 | //
43 | // If compression is nil, the uncompressed size will be returned, otherwise the compressed
44 | // size will be returned and domain names will be added to the map for future compression.
45 | len(off int, compression map[string]struct{}) int
46 |
47 | // pack packs the records RDATA into wire format. The header will
48 | // already have been packed into msg.
49 | pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error)
50 |
51 | // unpack unpacks an RR from wire format.
52 | //
53 | // This will only be called on a new and empty RR type with only the header populated. It
54 | // will only be called if the record's RDATA is non-empty.
55 | unpack(msg []byte, off int) (off1 int, err error)
56 |
57 | // parse parses an RR from zone file format.
58 | //
59 | // This will only be called on a new and empty RR type with only the header populated.
60 | parse(c *zlexer, origin string) *ParseError
61 |
62 | // isDuplicate returns whether the two RRs are duplicates.
63 | isDuplicate(r2 RR) bool
64 | }
65 |
66 | // RR_Header is the header all DNS resource records share.
67 | type RR_Header struct {
68 | Name string `dns:"cdomain-name"`
69 | Rrtype uint16
70 | Class uint16
71 | Ttl uint32
72 | Rdlength uint16 // Length of data after header.
73 | }
74 |
75 | // Header returns itself. This is here to make RR_Header implements the RR interface.
76 | func (h *RR_Header) Header() *RR_Header { return h }
77 |
78 | // Just to implement the RR interface.
79 | func (h *RR_Header) copy() RR { return nil }
80 |
81 | func (h *RR_Header) String() string {
82 | var s string
83 |
84 | if h.Rrtype == TypeOPT {
85 | s = ";"
86 | // and maybe other things
87 | }
88 |
89 | s += sprintName(h.Name) + "\t"
90 | s += strconv.FormatInt(int64(h.Ttl), 10) + "\t"
91 | s += Class(h.Class).String() + "\t"
92 | s += Type(h.Rrtype).String() + "\t"
93 | return s
94 | }
95 |
96 | func (h *RR_Header) len(off int, compression map[string]struct{}) int {
97 | l := domainNameLen(h.Name, off, compression, true)
98 | l += 10 // rrtype(2) + class(2) + ttl(4) + rdlength(2)
99 | return l
100 | }
101 |
102 | func (h *RR_Header) pack(msg []byte, off int, compression compressionMap, compress bool) (off1 int, err error) {
103 | // RR_Header has no RDATA to pack.
104 | return off, nil
105 | }
106 |
107 | func (h *RR_Header) unpack(msg []byte, off int) (int, error) {
108 | panic("dns: internal error: unpack should never be called on RR_Header")
109 | }
110 |
111 | func (h *RR_Header) parse(c *zlexer, origin string) *ParseError {
112 | panic("dns: internal error: parse should never be called on RR_Header")
113 | }
114 |
115 | // ToRFC3597 converts a known RR to the unknown RR representation from RFC 3597.
116 | func (rr *RFC3597) ToRFC3597(r RR) error {
117 | buf := make([]byte, Len(r))
118 | headerEnd, off, err := packRR(r, buf, 0, compressionMap{}, false)
119 | if err != nil {
120 | return err
121 | }
122 | buf = buf[:off]
123 |
124 | *rr = RFC3597{Hdr: *r.Header()}
125 | rr.Hdr.Rdlength = uint16(off - headerEnd)
126 |
127 | if noRdata(rr.Hdr) {
128 | return nil
129 | }
130 |
131 | _, err = rr.unpack(buf, headerEnd)
132 | return err
133 | }
134 |
135 | // fromRFC3597 converts an unknown RR representation from RFC 3597 to the known RR type.
136 | func (rr *RFC3597) fromRFC3597(r RR) error {
137 | hdr := r.Header()
138 | *hdr = rr.Hdr
139 |
140 | // Can't overflow uint16 as the length of Rdata is validated in (*RFC3597).parse.
141 | // We can only get here when rr was constructed with that method.
142 | hdr.Rdlength = uint16(hex.DecodedLen(len(rr.Rdata)))
143 |
144 | if noRdata(*hdr) {
145 | // Dynamic update.
146 | return nil
147 | }
148 |
149 | // rr.pack requires an extra allocation and a copy so we just decode Rdata
150 | // manually, it's simpler anyway.
151 | msg, err := hex.DecodeString(rr.Rdata)
152 | if err != nil {
153 | return err
154 | }
155 |
156 | _, err = r.unpack(msg, 0)
157 | return err
158 | }
159 |
--------------------------------------------------------------------------------
/dns/dnssec_keygen.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/ed25519"
7 | "crypto/elliptic"
8 | "crypto/rand"
9 | "crypto/rsa"
10 | "math/big"
11 | )
12 |
13 | // Generate generates a DNSKEY of the given bit size.
14 | // The public part is put inside the DNSKEY record.
15 | // The Algorithm in the key must be set as this will define
16 | // what kind of DNSKEY will be generated.
17 | // The ECDSA algorithms imply a fixed keysize, in that case
18 | // bits should be set to the size of the algorithm.
19 | func (k *DNSKEY) Generate(bits int) (crypto.PrivateKey, error) {
20 | switch k.Algorithm {
21 | case RSASHA1, RSASHA256, RSASHA1NSEC3SHA1:
22 | if bits < 512 || bits > 4096 {
23 | return nil, ErrKeySize
24 | }
25 | case RSASHA512:
26 | if bits < 1024 || bits > 4096 {
27 | return nil, ErrKeySize
28 | }
29 | case ECDSAP256SHA256:
30 | if bits != 256 {
31 | return nil, ErrKeySize
32 | }
33 | case ECDSAP384SHA384:
34 | if bits != 384 {
35 | return nil, ErrKeySize
36 | }
37 | case ED25519:
38 | if bits != 256 {
39 | return nil, ErrKeySize
40 | }
41 | default:
42 | return nil, ErrAlg
43 | }
44 |
45 | switch k.Algorithm {
46 | case RSASHA1, RSASHA256, RSASHA512, RSASHA1NSEC3SHA1:
47 | priv, err := rsa.GenerateKey(rand.Reader, bits)
48 | if err != nil {
49 | return nil, err
50 | }
51 | k.setPublicKeyRSA(priv.PublicKey.E, priv.PublicKey.N)
52 | return priv, nil
53 | case ECDSAP256SHA256, ECDSAP384SHA384:
54 | var c elliptic.Curve
55 | switch k.Algorithm {
56 | case ECDSAP256SHA256:
57 | c = elliptic.P256()
58 | case ECDSAP384SHA384:
59 | c = elliptic.P384()
60 | }
61 | priv, err := ecdsa.GenerateKey(c, rand.Reader)
62 | if err != nil {
63 | return nil, err
64 | }
65 | k.setPublicKeyECDSA(priv.PublicKey.X, priv.PublicKey.Y)
66 | return priv, nil
67 | case ED25519:
68 | pub, priv, err := ed25519.GenerateKey(rand.Reader)
69 | if err != nil {
70 | return nil, err
71 | }
72 | k.setPublicKeyED25519(pub)
73 | return priv, nil
74 | default:
75 | return nil, ErrAlg
76 | }
77 | }
78 |
79 | // Set the public key (the value E and N)
80 | func (k *DNSKEY) setPublicKeyRSA(_E int, _N *big.Int) bool {
81 | if _E == 0 || _N == nil {
82 | return false
83 | }
84 | buf := exponentToBuf(_E)
85 | buf = append(buf, _N.Bytes()...)
86 | k.PublicKey = toBase64(buf)
87 | return true
88 | }
89 |
90 | // Set the public key for Elliptic Curves
91 | func (k *DNSKEY) setPublicKeyECDSA(_X, _Y *big.Int) bool {
92 | if _X == nil || _Y == nil {
93 | return false
94 | }
95 | var intlen int
96 | switch k.Algorithm {
97 | case ECDSAP256SHA256:
98 | intlen = 32
99 | case ECDSAP384SHA384:
100 | intlen = 48
101 | }
102 | k.PublicKey = toBase64(curveToBuf(_X, _Y, intlen))
103 | return true
104 | }
105 |
106 | // Set the public key for Ed25519
107 | func (k *DNSKEY) setPublicKeyED25519(_K ed25519.PublicKey) bool {
108 | if _K == nil {
109 | return false
110 | }
111 | k.PublicKey = toBase64(_K)
112 | return true
113 | }
114 |
115 | // Set the public key (the values E and N) for RSA
116 | // RFC 3110: Section 2. RSA Public KEY Resource Records
117 | func exponentToBuf(_E int) []byte {
118 | var buf []byte
119 | i := big.NewInt(int64(_E)).Bytes()
120 | if len(i) < 256 {
121 | buf = make([]byte, 1, 1+len(i))
122 | buf[0] = uint8(len(i))
123 | } else {
124 | buf = make([]byte, 3, 3+len(i))
125 | buf[0] = 0
126 | buf[1] = uint8(len(i) >> 8)
127 | buf[2] = uint8(len(i))
128 | }
129 | buf = append(buf, i...)
130 | return buf
131 | }
132 |
133 | // Set the public key for X and Y for Curve. The two
134 | // values are just concatenated.
135 | func curveToBuf(_X, _Y *big.Int, intlen int) []byte {
136 | buf := intToBytes(_X, intlen)
137 | buf = append(buf, intToBytes(_Y, intlen)...)
138 | return buf
139 | }
140 |
--------------------------------------------------------------------------------
/dns/dnssec_privkey.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/ed25519"
7 | "crypto/rsa"
8 | "math/big"
9 | "strconv"
10 | )
11 |
12 | const format = "Private-key-format: v1.3\n"
13 |
14 | var bigIntOne = big.NewInt(1)
15 |
16 | // PrivateKeyString converts a PrivateKey to a string. This string has the same
17 | // format as the private-key-file of BIND9 (Private-key-format: v1.3).
18 | // It needs some info from the key (the algorithm), so its a method of the DNSKEY.
19 | // It supports *rsa.PrivateKey, *ecdsa.PrivateKey and ed25519.PrivateKey.
20 | func (r *DNSKEY) PrivateKeyString(p crypto.PrivateKey) string {
21 | algorithm := strconv.Itoa(int(r.Algorithm))
22 | algorithm += " (" + AlgorithmToString[r.Algorithm] + ")"
23 |
24 | switch p := p.(type) {
25 | case *rsa.PrivateKey:
26 | modulus := toBase64(p.PublicKey.N.Bytes())
27 | e := big.NewInt(int64(p.PublicKey.E))
28 | publicExponent := toBase64(e.Bytes())
29 | privateExponent := toBase64(p.D.Bytes())
30 | prime1 := toBase64(p.Primes[0].Bytes())
31 | prime2 := toBase64(p.Primes[1].Bytes())
32 | // Calculate Exponent1/2 and Coefficient as per: http://en.wikipedia.org/wiki/RSA#Using_the_Chinese_remainder_algorithm
33 | // and from: http://code.google.com/p/go/issues/detail?id=987
34 | p1 := new(big.Int).Sub(p.Primes[0], bigIntOne)
35 | q1 := new(big.Int).Sub(p.Primes[1], bigIntOne)
36 | exp1 := new(big.Int).Mod(p.D, p1)
37 | exp2 := new(big.Int).Mod(p.D, q1)
38 | coeff := new(big.Int).ModInverse(p.Primes[1], p.Primes[0])
39 |
40 | exponent1 := toBase64(exp1.Bytes())
41 | exponent2 := toBase64(exp2.Bytes())
42 | coefficient := toBase64(coeff.Bytes())
43 |
44 | return format +
45 | "Algorithm: " + algorithm + "\n" +
46 | "Modulus: " + modulus + "\n" +
47 | "PublicExponent: " + publicExponent + "\n" +
48 | "PrivateExponent: " + privateExponent + "\n" +
49 | "Prime1: " + prime1 + "\n" +
50 | "Prime2: " + prime2 + "\n" +
51 | "Exponent1: " + exponent1 + "\n" +
52 | "Exponent2: " + exponent2 + "\n" +
53 | "Coefficient: " + coefficient + "\n"
54 |
55 | case *ecdsa.PrivateKey:
56 | var intlen int
57 | switch r.Algorithm {
58 | case ECDSAP256SHA256:
59 | intlen = 32
60 | case ECDSAP384SHA384:
61 | intlen = 48
62 | }
63 | private := toBase64(intToBytes(p.D, intlen))
64 | return format +
65 | "Algorithm: " + algorithm + "\n" +
66 | "PrivateKey: " + private + "\n"
67 |
68 | case ed25519.PrivateKey:
69 | private := toBase64(p.Seed())
70 | return format +
71 | "Algorithm: " + algorithm + "\n" +
72 | "PrivateKey: " + private + "\n"
73 |
74 | default:
75 | return ""
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/dns/dnsutil/util.go:
--------------------------------------------------------------------------------
1 | // Package dnsutil contains higher-level methods useful with the dns
2 | // package. While package dns implements the DNS protocols itself,
3 | // these functions are related but not directly required for protocol
4 | // processing. They are often useful in preparing input/output of the
5 | // functions in package dns.
6 | package dnsutil
7 |
8 | import (
9 | "strings"
10 |
11 | "github.com/miekg/dns"
12 | )
13 |
14 | // AddOrigin adds origin to s if s is not already a FQDN.
15 | // Note that the result may not be a FQDN. If origin does not end
16 | // with a ".", the result won't either.
17 | // This implements the zonefile convention (specified in RFC 1035,
18 | // Section "5.1. Format") that "@" represents the
19 | // apex (bare) domain. i.e. AddOrigin("@", "foo.com.") returns "foo.com.".
20 | func AddOrigin(s, origin string) string {
21 | // ("foo.", "origin.") -> "foo." (already a FQDN)
22 | // ("foo", "origin.") -> "foo.origin."
23 | // ("foo", "origin") -> "foo.origin"
24 | // ("foo", ".") -> "foo." (Same as dns.Fqdn())
25 | // ("foo.", ".") -> "foo." (Same as dns.Fqdn())
26 | // ("@", "origin.") -> "origin." (@ represents the apex (bare) domain)
27 | // ("", "origin.") -> "origin." (not obvious)
28 | // ("foo", "") -> "foo" (not obvious)
29 |
30 | if dns.IsFqdn(s) {
31 | return s // s is already a FQDN, no need to mess with it.
32 | }
33 | if origin == "" {
34 | return s // Nothing to append.
35 | }
36 | if s == "@" || s == "" {
37 | return origin // Expand apex.
38 | }
39 | if origin == "." {
40 | return dns.Fqdn(s)
41 | }
42 |
43 | return s + "." + origin // The simple case.
44 | }
45 |
46 | // TrimDomainName trims origin from s if s is a subdomain.
47 | // This function will never return "", but returns "@" instead (@ represents the apex domain).
48 | func TrimDomainName(s, origin string) string {
49 | // An apex (bare) domain is always returned as "@".
50 | // If the return value ends in a ".", the domain was not the suffix.
51 | // origin can end in "." or not. Either way the results should be the same.
52 |
53 | if s == "" {
54 | return "@"
55 | }
56 | // Someone is using TrimDomainName(s, ".") to remove a dot if it exists.
57 | if origin == "." {
58 | return strings.TrimSuffix(s, origin)
59 | }
60 |
61 | original := s
62 | s = dns.Fqdn(s)
63 | origin = dns.Fqdn(origin)
64 |
65 | if !dns.IsSubDomain(origin, s) {
66 | return original
67 | }
68 |
69 | slabels := dns.Split(s)
70 | olabels := dns.Split(origin)
71 | m := dns.CompareDomainName(s, origin)
72 | if len(olabels) == m {
73 | if len(olabels) == len(slabels) {
74 | return "@" // origin == s
75 | }
76 | if (s[0] == '.') && (len(slabels) == (len(olabels) + 1)) {
77 | return "@" // TrimDomainName(".foo.", "foo.")
78 | }
79 | }
80 |
81 | // Return the first (len-m) labels:
82 | return s[:slabels[len(slabels)-m]-1]
83 | }
84 |
--------------------------------------------------------------------------------
/dns/dnsutil/util_test.go:
--------------------------------------------------------------------------------
1 | package dnsutil
2 |
3 | import "testing"
4 |
5 | func TestAddOrigin(t *testing.T) {
6 | var tests = []struct{ e1, e2, expected string }{
7 | {"@", "example.com", "example.com"},
8 | {"foo", "example.com", "foo.example.com"},
9 | {"foo.", "example.com", "foo."},
10 | {"@", "example.com.", "example.com."},
11 | {"foo", "example.com.", "foo.example.com."},
12 | {"foo.", "example.com.", "foo."},
13 | {"example.com", ".", "example.com."},
14 | {"example.com.", ".", "example.com."},
15 | // Oddball tests:
16 | // In general origin should not be "" or "." but at least
17 | // these tests verify we don't crash and will keep results
18 | // from changing unexpectedly.
19 | {"*.", "", "*."},
20 | {"@", "", "@"},
21 | {"foobar", "", "foobar"},
22 | {"foobar.", "", "foobar."},
23 | {"*.", ".", "*."},
24 | {"@", ".", "."},
25 | {"foobar", ".", "foobar."},
26 | {"foobar.", ".", "foobar."},
27 | }
28 | for _, test := range tests {
29 | actual := AddOrigin(test.e1, test.e2)
30 | if test.expected != actual {
31 | t.Errorf("AddOrigin(%#v, %#v) expected %#v, got %#v\n", test.e1, test.e2, test.expected, actual)
32 | }
33 | }
34 | }
35 |
36 | func TestTrimDomainName(t *testing.T) {
37 | // Basic tests.
38 | // Try trimming "example.com" and "example.com." from typical use cases.
39 | testsEx := []struct{ experiment, expected string }{
40 | {"foo.example.com", "foo"},
41 | {"foo.example.com.", "foo"},
42 | {".foo.example.com", ".foo"},
43 | {".foo.example.com.", ".foo"},
44 | {"*.example.com", "*"},
45 | {"example.com", "@"},
46 | {"example.com.", "@"},
47 | {"com.", "com."},
48 | {"foo.", "foo."},
49 | {"serverfault.com.", "serverfault.com."},
50 | {"serverfault.com", "serverfault.com"},
51 | {".foo.ronco.com", ".foo.ronco.com"},
52 | {".foo.ronco.com.", ".foo.ronco.com."},
53 | }
54 | for _, dom := range []string{"example.com", "example.com."} {
55 | for i, test := range testsEx {
56 | actual := TrimDomainName(test.experiment, dom)
57 | if test.expected != actual {
58 | t.Errorf("%d TrimDomainName(%#v, %#v): expected %v, got %v\n", i, test.experiment, dom, test.expected, actual)
59 | }
60 | }
61 | }
62 |
63 | // Paranoid tests.
64 | // These test shouldn't be needed but I was weary of off-by-one errors.
65 | // In theory, these can't happen because there are no single-letter TLDs,
66 | // but it is good to exercize the code this way.
67 | tests := []struct{ experiment, expected string }{
68 | {"", "@"},
69 | {".", "."},
70 | {"a.b.c.d.e.f.", "a.b.c.d.e"},
71 | {"b.c.d.e.f.", "b.c.d.e"},
72 | {"c.d.e.f.", "c.d.e"},
73 | {"d.e.f.", "d.e"},
74 | {"e.f.", "e"},
75 | {"f.", "@"},
76 | {".a.b.c.d.e.f.", ".a.b.c.d.e"},
77 | {".b.c.d.e.f.", ".b.c.d.e"},
78 | {".c.d.e.f.", ".c.d.e"},
79 | {".d.e.f.", ".d.e"},
80 | {".e.f.", ".e"},
81 | {".f.", "@"},
82 | {"a.b.c.d.e.f", "a.b.c.d.e"},
83 | {"a.b.c.d.e.", "a.b.c.d.e."},
84 | {"a.b.c.d.e", "a.b.c.d.e"},
85 | {"a.b.c.d.", "a.b.c.d."},
86 | {"a.b.c.d", "a.b.c.d"},
87 | {"a.b.c.", "a.b.c."},
88 | {"a.b.c", "a.b.c"},
89 | {"a.b.", "a.b."},
90 | {"a.b", "a.b"},
91 | {"a.", "a."},
92 | {"a", "a"},
93 | {".a.b.c.d.e.f", ".a.b.c.d.e"},
94 | {".a.b.c.d.e.", ".a.b.c.d.e."},
95 | {".a.b.c.d.e", ".a.b.c.d.e"},
96 | {".a.b.c.d.", ".a.b.c.d."},
97 | {".a.b.c.d", ".a.b.c.d"},
98 | {".a.b.c.", ".a.b.c."},
99 | {".a.b.c", ".a.b.c"},
100 | {".a.b.", ".a.b."},
101 | {".a.b", ".a.b"},
102 | {".a.", ".a."},
103 | {".a", ".a"},
104 | }
105 | for _, dom := range []string{"f", "f."} {
106 | for i, test := range tests {
107 | actual := TrimDomainName(test.experiment, dom)
108 | if test.expected != actual {
109 | t.Errorf("%d TrimDomainName(%#v, %#v): expected %v, got %v\n", i, test.experiment, dom, test.expected, actual)
110 | }
111 | }
112 | }
113 |
114 | // Test cases for bugs found in the wild.
115 | // These test cases provide both origin, s, and the expected result.
116 | // If you find a bug in the while, this is probably the easiest place
117 | // to add it as a test case.
118 | var testsWild = []struct{ e1, e2, expected string }{
119 | {"mathoverflow.net.", ".", "mathoverflow.net"},
120 | {"mathoverflow.net", ".", "mathoverflow.net"},
121 | {"", ".", "@"},
122 | {"@", ".", "@"},
123 | }
124 | for i, test := range testsWild {
125 | actual := TrimDomainName(test.e1, test.e2)
126 | if test.expected != actual {
127 | t.Errorf("%d TrimDomainName(%#v, %#v): expected %v, got %v\n", i, test.e1, test.e2, test.expected, actual)
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/dns/duplicate.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | //go:generate go run duplicate_generate.go
4 |
5 | // IsDuplicate checks of r1 and r2 are duplicates of each other, excluding the TTL.
6 | // So this means the header data is equal *and* the RDATA is the same. Returns true
7 | // if so, otherwise false. It's a protocol violation to have identical RRs in a message.
8 | func IsDuplicate(r1, r2 RR) bool {
9 | // Check whether the record header is identical.
10 | if !r1.Header().isDuplicate(r2.Header()) {
11 | return false
12 | }
13 |
14 | // Check whether the RDATA is identical.
15 | return r1.isDuplicate(r2)
16 | }
17 |
18 | func (r1 *RR_Header) isDuplicate(_r2 RR) bool {
19 | r2, ok := _r2.(*RR_Header)
20 | if !ok {
21 | return false
22 | }
23 | if r1.Class != r2.Class {
24 | return false
25 | }
26 | if r1.Rrtype != r2.Rrtype {
27 | return false
28 | }
29 | if !isDuplicateName(r1.Name, r2.Name) {
30 | return false
31 | }
32 | // ignore TTL
33 | return true
34 | }
35 |
36 | // isDuplicateName checks if the domain names s1 and s2 are equal.
37 | func isDuplicateName(s1, s2 string) bool { return equal(s1, s2) }
38 |
--------------------------------------------------------------------------------
/dns/duplicate_generate.go:
--------------------------------------------------------------------------------
1 | //+build ignore
2 |
3 | // types_generate.go is meant to run with go generate. It will use
4 | // go/{importer,types} to track down all the RR struct types. Then for each type
5 | // it will generate conversion tables (TypeToRR and TypeToString) and banal
6 | // methods (len, Header, copy) based on the struct tags. The generated source is
7 | // written to ztypes.go, and is meant to be checked into git.
8 | package main
9 |
10 | import (
11 | "bytes"
12 | "fmt"
13 | "go/format"
14 | "go/types"
15 | "log"
16 | "os"
17 |
18 | "golang.org/x/tools/go/packages"
19 | )
20 |
21 | var packageHdr = `
22 | // Code generated by "go run duplicate_generate.go"; DO NOT EDIT.
23 |
24 | package dns
25 |
26 | `
27 |
28 | func getTypeStruct(t types.Type, scope *types.Scope) (*types.Struct, bool) {
29 | st, ok := t.Underlying().(*types.Struct)
30 | if !ok {
31 | return nil, false
32 | }
33 | if st.NumFields() == 0 {
34 | return nil, false
35 | }
36 | if st.Field(0).Type() == scope.Lookup("RR_Header").Type() {
37 | return st, false
38 | }
39 | if st.Field(0).Anonymous() {
40 | st, _ := getTypeStruct(st.Field(0).Type(), scope)
41 | return st, true
42 | }
43 | return nil, false
44 | }
45 |
46 | // loadModule retrieves package description for a given module.
47 | func loadModule(name string) (*types.Package, error) {
48 | conf := packages.Config{Mode: packages.NeedTypes | packages.NeedTypesInfo}
49 | pkgs, err := packages.Load(&conf, name)
50 | if err != nil {
51 | return nil, err
52 | }
53 | return pkgs[0].Types, nil
54 | }
55 |
56 | func main() {
57 | // Import and type-check the package
58 | pkg, err := loadModule("github.com/miekg/dns")
59 | fatalIfErr(err)
60 | scope := pkg.Scope()
61 |
62 | // Collect actual types (*X)
63 | var namedTypes []string
64 | for _, name := range scope.Names() {
65 | o := scope.Lookup(name)
66 | if o == nil || !o.Exported() {
67 | continue
68 | }
69 |
70 | if st, _ := getTypeStruct(o.Type(), scope); st == nil {
71 | continue
72 | }
73 |
74 | if name == "PrivateRR" || name == "OPT" {
75 | continue
76 | }
77 |
78 | namedTypes = append(namedTypes, o.Name())
79 | }
80 |
81 | b := &bytes.Buffer{}
82 | b.WriteString(packageHdr)
83 |
84 | // Generate the duplicate check for each type.
85 | fmt.Fprint(b, "// isDuplicate() functions\n\n")
86 | for _, name := range namedTypes {
87 |
88 | o := scope.Lookup(name)
89 | st, _ := getTypeStruct(o.Type(), scope)
90 | fmt.Fprintf(b, "func (r1 *%s) isDuplicate(_r2 RR) bool {\n", name)
91 | fmt.Fprintf(b, "r2, ok := _r2.(*%s)\n", name)
92 | fmt.Fprint(b, "if !ok { return false }\n")
93 | fmt.Fprint(b, "_ = r2\n")
94 | for i := 1; i < st.NumFields(); i++ {
95 | field := st.Field(i).Name()
96 | o2 := func(s string) { fmt.Fprintf(b, s+"\n", field, field) }
97 | o3 := func(s string) { fmt.Fprintf(b, s+"\n", field, field, field) }
98 |
99 | // For some reason, a and aaaa don't pop up as *types.Slice here (mostly like because the are
100 | // *indirectly* defined as a slice in the net package).
101 | if _, ok := st.Field(i).Type().(*types.Slice); ok {
102 | o2("if len(r1.%s) != len(r2.%s) {\nreturn false\n}")
103 |
104 | if st.Tag(i) == `dns:"cdomain-name"` || st.Tag(i) == `dns:"domain-name"` {
105 | o3(`for i := 0; i < len(r1.%s); i++ {
106 | if !isDuplicateName(r1.%s[i], r2.%s[i]) {
107 | return false
108 | }
109 | }`)
110 |
111 | continue
112 | }
113 |
114 | if st.Tag(i) == `dns:"apl"` {
115 | o3(`for i := 0; i < len(r1.%s); i++ {
116 | if !r1.%s[i].equals(&r2.%s[i]) {
117 | return false
118 | }
119 | }`)
120 |
121 | continue
122 | }
123 |
124 | if st.Tag(i) == `dns:"pairs"` {
125 | o2(`if !areSVCBPairArraysEqual(r1.%s, r2.%s) {
126 | return false
127 | }`)
128 |
129 | continue
130 | }
131 |
132 | o3(`for i := 0; i < len(r1.%s); i++ {
133 | if r1.%s[i] != r2.%s[i] {
134 | return false
135 | }
136 | }`)
137 |
138 | continue
139 | }
140 |
141 | switch st.Tag(i) {
142 | case `dns:"-"`:
143 | // ignored
144 | case `dns:"a"`, `dns:"aaaa"`:
145 | o2("if !r1.%s.Equal(r2.%s) {\nreturn false\n}")
146 | case `dns:"cdomain-name"`, `dns:"domain-name"`:
147 | o2("if !isDuplicateName(r1.%s, r2.%s) {\nreturn false\n}")
148 | default:
149 | o2("if r1.%s != r2.%s {\nreturn false\n}")
150 | }
151 | }
152 | fmt.Fprintf(b, "return true\n}\n\n")
153 | }
154 |
155 | // gofmt
156 | res, err := format.Source(b.Bytes())
157 | if err != nil {
158 | b.WriteTo(os.Stderr)
159 | log.Fatal(err)
160 | }
161 |
162 | // write result
163 | f, err := os.Create("zduplicate.go")
164 | fatalIfErr(err)
165 | defer f.Close()
166 | f.Write(res)
167 | }
168 |
169 | func fatalIfErr(err error) {
170 | if err != nil {
171 | log.Fatal(err)
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/dns/duplicate_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "testing"
4 |
5 | func TestDuplicateA(t *testing.T) {
6 | a1, _ := NewRR("www.example.org. 2700 IN A 127.0.0.1")
7 | a2, _ := NewRR("www.example.org. IN A 127.0.0.1")
8 | if !IsDuplicate(a1, a2) {
9 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
10 | }
11 |
12 | a2, _ = NewRR("www.example.org. IN A 127.0.0.2")
13 | if IsDuplicate(a1, a2) {
14 | t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
15 | }
16 | }
17 |
18 | func TestDuplicateTXT(t *testing.T) {
19 | a1, _ := NewRR("www.example.org. IN TXT \"aa\"")
20 | a2, _ := NewRR("www.example.org. IN TXT \"aa\"")
21 |
22 | if !IsDuplicate(a1, a2) {
23 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
24 | }
25 |
26 | a2, _ = NewRR("www.example.org. IN TXT \"aa\" \"bb\"")
27 | if IsDuplicate(a1, a2) {
28 | t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
29 | }
30 |
31 | a1, _ = NewRR("www.example.org. IN TXT \"aa\" \"bc\"")
32 | if IsDuplicate(a1, a2) {
33 | t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
34 | }
35 | }
36 |
37 | func TestDuplicateSVCB(t *testing.T) {
38 | a1, _ := NewRR(`example.com. 3600 IN SVCB 1 . ipv6hint=1::3:3:3:3 key65300=\254\032\030\000\ \043,\;`)
39 | a2, _ := NewRR(`example.com. 3600 IN SVCB 1 . ipv6hint=1:0::3:3:3:3 key65300="\254\ \030\000 +\,;"`)
40 |
41 | if !IsDuplicate(a1, a2) {
42 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
43 | }
44 |
45 | a2, _ = NewRR(`example.com. 3600 IN SVCB 1 . ipv6hint=1::3:3:3:3 key65300="\255\ \030\000 +\,;"`)
46 |
47 | if IsDuplicate(a1, a2) {
48 | t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
49 | }
50 |
51 | a1, _ = NewRR(`example.com. 3600 IN SVCB 1 . ipv6hint=1::3:3:3:3`)
52 |
53 | if IsDuplicate(a1, a2) {
54 | t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
55 | }
56 |
57 | a2, _ = NewRR(`example.com. 3600 IN SVCB 1 . ipv4hint=1.1.1.1`)
58 |
59 | if IsDuplicate(a1, a2) {
60 | t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
61 | }
62 |
63 | a1, _ = NewRR(`example.com. 3600 IN SVCB 1 . ipv4hint=1.1.1.1,1.0.2.1`)
64 |
65 | if IsDuplicate(a1, a2) {
66 | t.Errorf("expected %s/%s not to be duplicates, but got true", a1.String(), a2.String())
67 | }
68 | }
69 |
70 | func TestDuplicateOwner(t *testing.T) {
71 | a1, _ := NewRR("www.example.org. IN A 127.0.0.1")
72 | a2, _ := NewRR("www.example.org. IN A 127.0.0.1")
73 | if !IsDuplicate(a1, a2) {
74 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
75 | }
76 |
77 | a2, _ = NewRR("WWw.exaMPle.org. IN A 127.0.0.2")
78 | if IsDuplicate(a1, a2) {
79 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
80 | }
81 | }
82 |
83 | func TestDuplicateDomain(t *testing.T) {
84 | a1, _ := NewRR("www.example.org. IN CNAME example.org.")
85 | a2, _ := NewRR("www.example.org. IN CNAME example.org.")
86 | if !IsDuplicate(a1, a2) {
87 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
88 | }
89 |
90 | a2, _ = NewRR("www.example.org. IN CNAME exAMPLe.oRG.")
91 | if !IsDuplicate(a1, a2) {
92 | t.Errorf("expected %s/%s to be duplicates, but got false", a1.String(), a2.String())
93 | }
94 | }
95 |
96 | func TestDuplicateWrongRrtype(t *testing.T) {
97 | // Test that IsDuplicate won't panic for a record that's lying about
98 | // it's Rrtype.
99 |
100 | r1 := &A{Hdr: RR_Header{Rrtype: TypeA}}
101 | r2 := &AAAA{Hdr: RR_Header{Rrtype: TypeA}}
102 | if IsDuplicate(r1, r2) {
103 | t.Errorf("expected %s/%s not to be duplicates, but got true", r1.String(), r2.String())
104 | }
105 |
106 | r3 := &AAAA{Hdr: RR_Header{Rrtype: TypeA}}
107 | r4 := &A{Hdr: RR_Header{Rrtype: TypeA}}
108 | if IsDuplicate(r3, r4) {
109 | t.Errorf("expected %s/%s not to be duplicates, but got true", r3.String(), r4.String())
110 | }
111 |
112 | r5 := &AAAA{Hdr: RR_Header{Rrtype: TypeA}}
113 | r6 := &AAAA{Hdr: RR_Header{Rrtype: TypeA}}
114 | if !IsDuplicate(r5, r6) {
115 | t.Errorf("expected %s/%s to be duplicates, but got false", r5.String(), r6.String())
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/dns/dyn_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // Find better solution
4 |
--------------------------------------------------------------------------------
/dns/edns_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "bytes"
5 | "net"
6 | "testing"
7 | )
8 |
9 | func TestOPTTtl(t *testing.T) {
10 | e := &OPT{}
11 | e.Hdr.Name = "."
12 | e.Hdr.Rrtype = TypeOPT
13 |
14 | // verify the default setting of DO=0
15 | if e.Do() {
16 | t.Errorf("DO bit should be zero")
17 | }
18 |
19 | // There are 6 possible invocations of SetDo():
20 | //
21 | // 1. Starting with DO=0, using SetDo()
22 | // 2. Starting with DO=0, using SetDo(true)
23 | // 3. Starting with DO=0, using SetDo(false)
24 | // 4. Starting with DO=1, using SetDo()
25 | // 5. Starting with DO=1, using SetDo(true)
26 | // 6. Starting with DO=1, using SetDo(false)
27 |
28 | // verify that invoking SetDo() sets DO=1 (TEST #1)
29 | e.SetDo()
30 | if !e.Do() {
31 | t.Errorf("DO bit should be non-zero")
32 | }
33 | // verify that using SetDo(true) works when DO=1 (TEST #5)
34 | e.SetDo(true)
35 | if !e.Do() {
36 | t.Errorf("DO bit should still be non-zero")
37 | }
38 | // verify that we can use SetDo(false) to set DO=0 (TEST #6)
39 | e.SetDo(false)
40 | if e.Do() {
41 | t.Errorf("DO bit should be zero")
42 | }
43 | // verify that if we call SetDo(false) when DO=0 that it is unchanged (TEST #3)
44 | e.SetDo(false)
45 | if e.Do() {
46 | t.Errorf("DO bit should still be zero")
47 | }
48 | // verify that using SetDo(true) works for DO=0 (TEST #2)
49 | e.SetDo(true)
50 | if !e.Do() {
51 | t.Errorf("DO bit should be non-zero")
52 | }
53 | // verify that using SetDo() works for DO=1 (TEST #4)
54 | e.SetDo()
55 | if !e.Do() {
56 | t.Errorf("DO bit should be non-zero")
57 | }
58 |
59 | if e.Version() != 0 {
60 | t.Errorf("version should be non-zero")
61 | }
62 |
63 | e.SetVersion(42)
64 | if e.Version() != 42 {
65 | t.Errorf("set 42, expected %d, got %d", 42, e.Version())
66 | }
67 |
68 | e.SetExtendedRcode(42)
69 | // ExtendedRcode has the last 4 bits set to 0.
70 | if e.ExtendedRcode() != 42&0xFFFFFFF0 {
71 | t.Errorf("set 42, expected %d, got %d", 42&0xFFFFFFF0, e.ExtendedRcode())
72 | }
73 |
74 | // This will reset the 8 upper bits of the extended rcode
75 | e.SetExtendedRcode(RcodeNotAuth)
76 | if e.ExtendedRcode() != 0 {
77 | t.Errorf("Setting a non-extended rcode is expected to set extended rcode to 0, got: %d", e.ExtendedRcode())
78 | }
79 | }
80 |
81 | func TestEDNS0_SUBNETUnpack(t *testing.T) {
82 | for _, ip := range []net.IP{
83 | net.IPv4(0xde, 0xad, 0xbe, 0xef),
84 | net.ParseIP("192.0.2.1"),
85 | net.ParseIP("2001:db8::68"),
86 | } {
87 | var s1 EDNS0_SUBNET
88 | s1.Address = ip
89 |
90 | if ip.To4() == nil {
91 | s1.Family = 2
92 | s1.SourceNetmask = net.IPv6len * 8
93 | } else {
94 | s1.Family = 1
95 | s1.SourceNetmask = net.IPv4len * 8
96 | }
97 |
98 | b, err := s1.pack()
99 | if err != nil {
100 | t.Fatalf("failed to pack: %v", err)
101 | }
102 |
103 | var s2 EDNS0_SUBNET
104 | if err := s2.unpack(b); err != nil {
105 | t.Fatalf("failed to unpack: %v", err)
106 | }
107 |
108 | if !ip.Equal(s2.Address) {
109 | t.Errorf("address different after unpacking; expected %s, got %s", ip, s2.Address)
110 | }
111 | }
112 | }
113 |
114 | func TestEDNS0_UL(t *testing.T) {
115 | cases := []struct {
116 | l uint32
117 | kl uint32
118 | }{
119 | {0x01234567, 0},
120 | {0x76543210, 0xFEDCBA98},
121 | }
122 | for _, c := range cases {
123 | expect := EDNS0_UL{EDNS0UL, c.l, c.kl}
124 | b, err := expect.pack()
125 | if err != nil {
126 | t.Fatalf("failed to pack: %v", err)
127 | }
128 | actual := EDNS0_UL{EDNS0UL, ^uint32(0), ^uint32(0)}
129 | if err := actual.unpack(b); err != nil {
130 | t.Fatalf("failed to unpack: %v", err)
131 | }
132 | if expect != actual {
133 | t.Errorf("unpacked option is different; expected %v, got %v", expect, actual)
134 | }
135 | }
136 | }
137 |
138 | func TestZ(t *testing.T) {
139 | e := &OPT{}
140 | e.Hdr.Name = "."
141 | e.Hdr.Rrtype = TypeOPT
142 | e.SetVersion(8)
143 | e.SetDo()
144 | if e.Z() != 0 {
145 | t.Errorf("expected Z of 0, got %d", e.Z())
146 | }
147 | e.SetZ(5)
148 | if e.Z() != 5 {
149 | t.Errorf("expected Z of 5, got %d", e.Z())
150 | }
151 | e.SetZ(0xFFFF)
152 | if e.Z() != 0x7FFF {
153 | t.Errorf("expected Z of 0x7FFFF, got %d", e.Z())
154 | }
155 | if e.Version() != 8 {
156 | t.Errorf("expected version to still be 8, got %d", e.Version())
157 | }
158 | if !e.Do() {
159 | t.Error("expected DO to be set")
160 | }
161 | }
162 |
163 | func TestEDNS0_ESU(t *testing.T) {
164 | p := []byte{
165 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
166 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x29, 0x04,
167 | 0xC4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00,
168 | 0x04, 0x00, 0x24, 0x73, 0x69, 0x70, 0x3A, 0x2B,
169 | 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
170 | 0x39, 0x40, 0x74, 0x65, 0x73, 0x74, 0x2E, 0x63,
171 | 0x6F, 0x6D, 0x3B, 0x75, 0x73, 0x65, 0x72, 0x3D,
172 | 0x63, 0x67, 0x72, 0x61, 0x74, 0x65, 0x73,
173 | }
174 |
175 | m := new(Msg)
176 | if err := m.Unpack(p); err != nil {
177 | t.Fatalf("failed to unpack: %v", err)
178 | }
179 | opt := m.IsEdns0()
180 | if opt == nil {
181 | t.Fatalf("expected edns0 option")
182 | }
183 | if len(opt.Option) != 1 {
184 | t.Fatalf("expected only one option: %v", opt.Option)
185 | }
186 | edns0 := opt.Option[0]
187 | esu, ok := edns0.(*EDNS0_ESU)
188 | if !ok {
189 | t.Fatalf("expected option of type EDNS0_ESU, got %t", edns0)
190 | }
191 | expect := "sip:+123456789@test.com;user=cgrates"
192 | if esu.Uri != expect {
193 | t.Errorf("unpacked option is different; expected %v, got %v", expect, esu.Uri)
194 | }
195 | }
196 |
197 | func TestEDNS0_TCP_KEEPALIVE_unpack(t *testing.T) {
198 | cases := []struct {
199 | name string
200 | b []byte
201 | expected uint16
202 | expectedErr bool
203 | }{
204 | {
205 | name: "empty",
206 | b: []byte{},
207 | expected: 0,
208 | },
209 | {
210 | name: "timeout 1",
211 | b: []byte{0, 1},
212 | expected: 1,
213 | },
214 | {
215 | name: "invalid",
216 | b: []byte{0, 1, 3},
217 | expectedErr: true,
218 | },
219 | }
220 |
221 | for _, tc := range cases {
222 | t.Run(tc.name, func(t *testing.T) {
223 | e := &EDNS0_TCP_KEEPALIVE{}
224 | err := e.unpack(tc.b)
225 | if err != nil && !tc.expectedErr {
226 | t.Error("failed to unpack, expected no error")
227 | }
228 | if err == nil && tc.expectedErr {
229 | t.Error("unpacked, but expected an error")
230 | }
231 | if e.Timeout != tc.expected {
232 | t.Errorf("invalid timeout, actual: %d, expected: %d", e.Timeout, tc.expected)
233 | }
234 | })
235 | }
236 | }
237 |
238 | func TestEDNS0_TCP_KEEPALIVE_pack(t *testing.T) {
239 | cases := []struct {
240 | name string
241 | edns *EDNS0_TCP_KEEPALIVE
242 | expected []byte
243 | }{
244 | {
245 | name: "empty",
246 | edns: &EDNS0_TCP_KEEPALIVE{
247 | Code: EDNS0TCPKEEPALIVE,
248 | Timeout: 0,
249 | },
250 | expected: nil,
251 | },
252 | {
253 | name: "timeout 1",
254 | edns: &EDNS0_TCP_KEEPALIVE{
255 | Code: EDNS0TCPKEEPALIVE,
256 | Timeout: 1,
257 | },
258 | expected: []byte{0, 1},
259 | },
260 | }
261 |
262 | for _, tc := range cases {
263 | t.Run(tc.name, func(t *testing.T) {
264 | b, err := tc.edns.pack()
265 | if err != nil {
266 | t.Error("expected no error")
267 | }
268 |
269 | if tc.expected == nil && b != nil {
270 | t.Errorf("invalid result, expected nil")
271 | }
272 |
273 | res := bytes.Compare(b, tc.expected)
274 | if res != 0 {
275 | t.Errorf("invalid result, expected: %v, actual: %v", tc.expected, b)
276 | }
277 | })
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/dns/example_test.go:
--------------------------------------------------------------------------------
1 | package dns_test
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 | "net"
8 |
9 | "github.com/miekg/dns"
10 | )
11 |
12 | // Retrieve the MX records for miek.nl.
13 | func ExampleMX() {
14 | config, _ := dns.ClientConfigFromFile("/etc/resolv.conf")
15 | c := new(dns.Client)
16 | m := new(dns.Msg)
17 | m.SetQuestion("miek.nl.", dns.TypeMX)
18 | m.RecursionDesired = true
19 | r, _, err := c.Exchange(m, config.Servers[0]+":"+config.Port)
20 | if err != nil {
21 | return
22 | }
23 | if r.Rcode != dns.RcodeSuccess {
24 | return
25 | }
26 | for _, a := range r.Answer {
27 | if mx, ok := a.(*dns.MX); ok {
28 | fmt.Printf("%s\n", mx.String())
29 | }
30 | }
31 | }
32 |
33 | // Retrieve the DNSKEY records of a zone and convert them
34 | // to DS records for SHA1, SHA256 and SHA384.
35 | func ExampleDS() {
36 | config, _ := dns.ClientConfigFromFile("/etc/resolv.conf")
37 | c := new(dns.Client)
38 | m := new(dns.Msg)
39 | zone := "miek.nl"
40 | m.SetQuestion(dns.Fqdn(zone), dns.TypeDNSKEY)
41 | m.SetEdns0(4096, true)
42 | r, _, err := c.Exchange(m, config.Servers[0]+":"+config.Port)
43 | if err != nil {
44 | return
45 | }
46 | if r.Rcode != dns.RcodeSuccess {
47 | return
48 | }
49 | for _, k := range r.Answer {
50 | if key, ok := k.(*dns.DNSKEY); ok {
51 | for _, alg := range []uint8{dns.SHA1, dns.SHA256, dns.SHA384} {
52 | fmt.Printf("%s; %d\n", key.ToDS(alg).String(), key.Flags)
53 | }
54 | }
55 | }
56 | }
57 |
58 | const TypeAPAIR = 0x0F99
59 |
60 | type APAIR struct {
61 | addr [2]net.IP
62 | }
63 |
64 | func NewAPAIR() dns.PrivateRdata { return new(APAIR) }
65 |
66 | func (rd *APAIR) String() string { return rd.addr[0].String() + " " + rd.addr[1].String() }
67 | func (rd *APAIR) Parse(txt []string) error {
68 | if len(txt) != 2 {
69 | return errors.New("two addresses required for APAIR")
70 | }
71 | for i, s := range txt {
72 | ip := net.ParseIP(s)
73 | if ip == nil {
74 | return errors.New("invalid IP in APAIR text representation")
75 | }
76 | rd.addr[i] = ip
77 | }
78 | return nil
79 | }
80 |
81 | func (rd *APAIR) Pack(buf []byte) (int, error) {
82 | b := append([]byte(rd.addr[0]), []byte(rd.addr[1])...)
83 | n := copy(buf, b)
84 | if n != len(b) {
85 | return n, dns.ErrBuf
86 | }
87 | return n, nil
88 | }
89 |
90 | func (rd *APAIR) Unpack(buf []byte) (int, error) {
91 | ln := net.IPv4len * 2
92 | if len(buf) != ln {
93 | return 0, errors.New("invalid length of APAIR rdata")
94 | }
95 | cp := make([]byte, ln)
96 | copy(cp, buf) // clone bytes to use them in IPs
97 |
98 | rd.addr[0] = net.IP(cp[:3])
99 | rd.addr[1] = net.IP(cp[4:])
100 |
101 | return len(buf), nil
102 | }
103 |
104 | func (rd *APAIR) Copy(dest dns.PrivateRdata) error {
105 | cp := make([]byte, rd.Len())
106 | _, err := rd.Pack(cp)
107 | if err != nil {
108 | return err
109 | }
110 |
111 | d := dest.(*APAIR)
112 | d.addr[0] = net.IP(cp[:3])
113 | d.addr[1] = net.IP(cp[4:])
114 | return nil
115 | }
116 |
117 | func (rd *APAIR) Len() int {
118 | return net.IPv4len * 2
119 | }
120 |
121 | func ExamplePrivateHandle() {
122 | dns.PrivateHandle("APAIR", TypeAPAIR, NewAPAIR)
123 | defer dns.PrivateHandleRemove(TypeAPAIR)
124 |
125 | rr, err := dns.NewRR("miek.nl. APAIR (1.2.3.4 1.2.3.5)")
126 | if err != nil {
127 | log.Fatal("could not parse APAIR record: ", err)
128 | }
129 | fmt.Println(rr)
130 | // Output: miek.nl. 3600 IN APAIR 1.2.3.4 1.2.3.5
131 |
132 | m := new(dns.Msg)
133 | m.Id = 12345
134 | m.SetQuestion("miek.nl.", TypeAPAIR)
135 | m.Answer = append(m.Answer, rr)
136 |
137 | fmt.Println(m)
138 | // ;; opcode: QUERY, status: NOERROR, id: 12345
139 | // ;; flags: rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
140 | //
141 | // ;; QUESTION SECTION:
142 | // ;miek.nl. IN APAIR
143 | //
144 | // ;; ANSWER SECTION:
145 | // miek.nl. 3600 IN APAIR 1.2.3.4 1.2.3.5
146 | }
147 |
--------------------------------------------------------------------------------
/dns/format.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "net"
5 | "reflect"
6 | "strconv"
7 | )
8 |
9 | // NumField returns the number of rdata fields r has.
10 | func NumField(r RR) int {
11 | return reflect.ValueOf(r).Elem().NumField() - 1 // Remove RR_Header
12 | }
13 |
14 | // Field returns the rdata field i as a string. Fields are indexed starting from 1.
15 | // RR types that holds slice data, for instance the NSEC type bitmap will return a single
16 | // string where the types are concatenated using a space.
17 | // Accessing non existing fields will cause a panic.
18 | func Field(r RR, i int) string {
19 | if i == 0 {
20 | return ""
21 | }
22 | d := reflect.ValueOf(r).Elem().Field(i)
23 | switch d.Kind() {
24 | case reflect.String:
25 | return d.String()
26 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
27 | return strconv.FormatInt(d.Int(), 10)
28 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
29 | return strconv.FormatUint(d.Uint(), 10)
30 | case reflect.Slice:
31 | switch reflect.ValueOf(r).Elem().Type().Field(i).Tag {
32 | case `dns:"a"`:
33 | // TODO(miek): Hmm store this as 16 bytes
34 | if d.Len() < net.IPv4len {
35 | return ""
36 | }
37 | if d.Len() < net.IPv6len {
38 | return net.IPv4(byte(d.Index(0).Uint()),
39 | byte(d.Index(1).Uint()),
40 | byte(d.Index(2).Uint()),
41 | byte(d.Index(3).Uint())).String()
42 | }
43 | return net.IPv4(byte(d.Index(12).Uint()),
44 | byte(d.Index(13).Uint()),
45 | byte(d.Index(14).Uint()),
46 | byte(d.Index(15).Uint())).String()
47 | case `dns:"aaaa"`:
48 | if d.Len() < net.IPv6len {
49 | return ""
50 | }
51 | return net.IP{
52 | byte(d.Index(0).Uint()),
53 | byte(d.Index(1).Uint()),
54 | byte(d.Index(2).Uint()),
55 | byte(d.Index(3).Uint()),
56 | byte(d.Index(4).Uint()),
57 | byte(d.Index(5).Uint()),
58 | byte(d.Index(6).Uint()),
59 | byte(d.Index(7).Uint()),
60 | byte(d.Index(8).Uint()),
61 | byte(d.Index(9).Uint()),
62 | byte(d.Index(10).Uint()),
63 | byte(d.Index(11).Uint()),
64 | byte(d.Index(12).Uint()),
65 | byte(d.Index(13).Uint()),
66 | byte(d.Index(14).Uint()),
67 | byte(d.Index(15).Uint()),
68 | }.String()
69 | case `dns:"nsec"`:
70 | if d.Len() == 0 {
71 | return ""
72 | }
73 | s := Type(d.Index(0).Uint()).String()
74 | for i := 1; i < d.Len(); i++ {
75 | s += " " + Type(d.Index(i).Uint()).String()
76 | }
77 | return s
78 | default:
79 | // if it does not have a tag its a string slice
80 | fallthrough
81 | case `dns:"txt"`:
82 | if d.Len() == 0 {
83 | return ""
84 | }
85 | s := d.Index(0).String()
86 | for i := 1; i < d.Len(); i++ {
87 | s += " " + d.Index(i).String()
88 | }
89 | return s
90 | }
91 | }
92 | return ""
93 | }
94 |
--------------------------------------------------------------------------------
/dns/format_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestFieldEmptyAOrAAAAData(t *testing.T) {
8 | res := Field(new(A), 1)
9 | if res != "" {
10 | t.Errorf("expected empty string but got %v", res)
11 | }
12 | res = Field(new(AAAA), 1)
13 | if res != "" {
14 | t.Errorf("expected empty string but got %v", res)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/dns/fuzz.go:
--------------------------------------------------------------------------------
1 | // +build fuzz
2 |
3 | package dns
4 |
5 | import "strings"
6 |
7 | func Fuzz(data []byte) int {
8 | msg := new(Msg)
9 |
10 | if err := msg.Unpack(data); err != nil {
11 | return 0
12 | }
13 | if _, err := msg.Pack(); err != nil {
14 | return 0
15 | }
16 |
17 | return 1
18 | }
19 |
20 | func FuzzNewRR(data []byte) int {
21 | str := string(data)
22 | // Do not fuzz lines that include the $INCLUDE keyword and hint the fuzzer
23 | // at avoiding them.
24 | // See GH#1025 for context.
25 | if strings.Contains(strings.ToUpper(str), "$INCLUDE") {
26 | return -1
27 | }
28 | if _, err := NewRR(str); err != nil {
29 | return 0
30 | }
31 | return 1
32 | }
33 |
--------------------------------------------------------------------------------
/dns/generate.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | // Parse the $GENERATE statement as used in BIND9 zones.
12 | // See http://www.zytrax.com/books/dns/ch8/generate.html for instance.
13 | // We are called after '$GENERATE '. After which we expect:
14 | // * the range (12-24/2)
15 | // * lhs (ownername)
16 | // * [[ttl][class]]
17 | // * type
18 | // * rhs (rdata)
19 | // But we are lazy here, only the range is parsed *all* occurrences
20 | // of $ after that are interpreted.
21 | func (zp *ZoneParser) generate(l lex) (RR, bool) {
22 | token := l.token
23 | step := int64(1)
24 | if i := strings.IndexByte(token, '/'); i >= 0 {
25 | if i+1 == len(token) {
26 | return zp.setParseError("bad step in $GENERATE range", l)
27 | }
28 |
29 | s, err := strconv.ParseInt(token[i+1:], 10, 64)
30 | if err != nil || s <= 0 {
31 | return zp.setParseError("bad step in $GENERATE range", l)
32 | }
33 |
34 | step = s
35 | token = token[:i]
36 | }
37 |
38 | sx := strings.SplitN(token, "-", 2)
39 | if len(sx) != 2 {
40 | return zp.setParseError("bad start-stop in $GENERATE range", l)
41 | }
42 |
43 | start, err := strconv.ParseInt(sx[0], 10, 64)
44 | if err != nil {
45 | return zp.setParseError("bad start in $GENERATE range", l)
46 | }
47 |
48 | end, err := strconv.ParseInt(sx[1], 10, 64)
49 | if err != nil {
50 | return zp.setParseError("bad stop in $GENERATE range", l)
51 | }
52 | if end < 0 || start < 0 || end < start || (end-start)/step > 65535 {
53 | return zp.setParseError("bad range in $GENERATE range", l)
54 | }
55 |
56 | // _BLANK
57 | l, ok := zp.c.Next()
58 | if !ok || l.value != zBlank {
59 | return zp.setParseError("garbage after $GENERATE range", l)
60 | }
61 |
62 | // Create a complete new string, which we then parse again.
63 | var s string
64 | for l, ok := zp.c.Next(); ok; l, ok = zp.c.Next() {
65 | if l.err {
66 | return zp.setParseError("bad data in $GENERATE directive", l)
67 | }
68 | if l.value == zNewline {
69 | break
70 | }
71 |
72 | s += l.token
73 | }
74 |
75 | r := &generateReader{
76 | s: s,
77 |
78 | cur: start,
79 | start: start,
80 | end: end,
81 | step: step,
82 |
83 | file: zp.file,
84 | lex: &l,
85 | }
86 | zp.sub = NewZoneParser(r, zp.origin, zp.file)
87 | zp.sub.includeDepth, zp.sub.includeAllowed = zp.includeDepth, zp.includeAllowed
88 | zp.sub.generateDisallowed = true
89 | zp.sub.SetDefaultTTL(defaultTtl)
90 | return zp.subNext()
91 | }
92 |
93 | type generateReader struct {
94 | s string
95 | si int
96 |
97 | cur int64
98 | start int64
99 | end int64
100 | step int64
101 |
102 | mod bytes.Buffer
103 |
104 | escape bool
105 |
106 | eof bool
107 |
108 | file string
109 | lex *lex
110 | }
111 |
112 | func (r *generateReader) parseError(msg string, end int) *ParseError {
113 | r.eof = true // Make errors sticky.
114 |
115 | l := *r.lex
116 | l.token = r.s[r.si-1 : end]
117 | l.column += r.si // l.column starts one zBLANK before r.s
118 |
119 | return &ParseError{r.file, msg, l}
120 | }
121 |
122 | func (r *generateReader) Read(p []byte) (int, error) {
123 | // NewZLexer, through NewZoneParser, should use ReadByte and
124 | // not end up here.
125 |
126 | panic("not implemented")
127 | }
128 |
129 | func (r *generateReader) ReadByte() (byte, error) {
130 | if r.eof {
131 | return 0, io.EOF
132 | }
133 | if r.mod.Len() > 0 {
134 | return r.mod.ReadByte()
135 | }
136 |
137 | if r.si >= len(r.s) {
138 | r.si = 0
139 | r.cur += r.step
140 |
141 | r.eof = r.cur > r.end || r.cur < 0
142 | return '\n', nil
143 | }
144 |
145 | si := r.si
146 | r.si++
147 |
148 | switch r.s[si] {
149 | case '\\':
150 | if r.escape {
151 | r.escape = false
152 | return '\\', nil
153 | }
154 |
155 | r.escape = true
156 | return r.ReadByte()
157 | case '$':
158 | if r.escape {
159 | r.escape = false
160 | return '$', nil
161 | }
162 |
163 | mod := "%d"
164 |
165 | if si >= len(r.s)-1 {
166 | // End of the string
167 | fmt.Fprintf(&r.mod, mod, r.cur)
168 | return r.mod.ReadByte()
169 | }
170 |
171 | if r.s[si+1] == '$' {
172 | r.si++
173 | return '$', nil
174 | }
175 |
176 | var offset int64
177 |
178 | // Search for { and }
179 | if r.s[si+1] == '{' {
180 | // Modifier block
181 | sep := strings.Index(r.s[si+2:], "}")
182 | if sep < 0 {
183 | return 0, r.parseError("bad modifier in $GENERATE", len(r.s))
184 | }
185 |
186 | var errMsg string
187 | mod, offset, errMsg = modToPrintf(r.s[si+2 : si+2+sep])
188 | if errMsg != "" {
189 | return 0, r.parseError(errMsg, si+3+sep)
190 | }
191 | if r.start+offset < 0 || r.end+offset > 1<<31-1 {
192 | return 0, r.parseError("bad offset in $GENERATE", si+3+sep)
193 | }
194 |
195 | r.si += 2 + sep // Jump to it
196 | }
197 |
198 | fmt.Fprintf(&r.mod, mod, r.cur+offset)
199 | return r.mod.ReadByte()
200 | default:
201 | if r.escape { // Pretty useless here
202 | r.escape = false
203 | return r.ReadByte()
204 | }
205 |
206 | return r.s[si], nil
207 | }
208 | }
209 |
210 | // Convert a $GENERATE modifier 0,0,d to something Printf can deal with.
211 | func modToPrintf(s string) (string, int64, string) {
212 | // Modifier is { offset [ ,width [ ,base ] ] } - provide default
213 | // values for optional width and type, if necessary.
214 | var offStr, widthStr, base string
215 | switch xs := strings.Split(s, ","); len(xs) {
216 | case 1:
217 | offStr, widthStr, base = xs[0], "0", "d"
218 | case 2:
219 | offStr, widthStr, base = xs[0], xs[1], "d"
220 | case 3:
221 | offStr, widthStr, base = xs[0], xs[1], xs[2]
222 | default:
223 | return "", 0, "bad modifier in $GENERATE"
224 | }
225 |
226 | switch base {
227 | case "o", "d", "x", "X":
228 | default:
229 | return "", 0, "bad base in $GENERATE"
230 | }
231 |
232 | offset, err := strconv.ParseInt(offStr, 10, 64)
233 | if err != nil {
234 | return "", 0, "bad offset in $GENERATE"
235 | }
236 |
237 | width, err := strconv.ParseInt(widthStr, 10, 64)
238 | if err != nil || width < 0 || width > 255 {
239 | return "", 0, "bad width in $GENERATE"
240 | }
241 |
242 | if width == 0 {
243 | return "%" + base, offset, ""
244 | }
245 |
246 | return "%0" + widthStr + base, offset, ""
247 | }
248 |
--------------------------------------------------------------------------------
/dns/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/miekg/dns
2 |
3 | go 1.14
4 |
5 | require (
6 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985
7 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
8 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
9 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2
10 | )
11 |
--------------------------------------------------------------------------------
/dns/go.sum:
--------------------------------------------------------------------------------
1 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
3 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
4 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
5 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
6 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
7 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
8 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
9 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8=
10 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
11 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
12 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
13 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
15 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
16 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
17 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
18 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
19 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
20 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
21 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
22 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
23 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
24 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
25 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
26 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
27 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
28 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
29 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
30 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
31 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
32 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
33 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
34 |
--------------------------------------------------------------------------------
/dns/hash.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "bytes"
5 | "crypto"
6 | "hash"
7 | )
8 |
9 | // identityHash will not hash, it only buffers the data written into it and returns it as-is.
10 | type identityHash struct {
11 | b *bytes.Buffer
12 | }
13 |
14 | // Implement the hash.Hash interface.
15 |
16 | func (i identityHash) Write(b []byte) (int, error) { return i.b.Write(b) }
17 | func (i identityHash) Size() int { return i.b.Len() }
18 | func (i identityHash) BlockSize() int { return 1024 }
19 | func (i identityHash) Reset() { i.b.Reset() }
20 | func (i identityHash) Sum(b []byte) []byte { return append(b, i.b.Bytes()...) }
21 |
22 | func hashFromAlgorithm(alg uint8) (hash.Hash, crypto.Hash, error) {
23 | hashnumber, ok := AlgorithmToHash[alg]
24 | if !ok {
25 | return nil, 0, ErrAlg
26 | }
27 | if hashnumber == 0 {
28 | return identityHash{b: &bytes.Buffer{}}, hashnumber, nil
29 | }
30 | return hashnumber.New(), hashnumber, nil
31 | }
32 |
--------------------------------------------------------------------------------
/dns/issue_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // Tests that solve that an specific issue.
4 |
5 | import (
6 | "strings"
7 | "testing"
8 | )
9 |
10 | func TestNSEC3MissingSalt(t *testing.T) {
11 | rr := testRR("ji6neoaepv8b5o6k4ev33abha8ht9fgc.example. NSEC3 1 1 12 aabbccdd K8UDEMVP1J2F7EG6JEBPS17VP3N8I58H")
12 | m := new(Msg)
13 | m.Answer = []RR{rr}
14 | mb, err := m.Pack()
15 | if err != nil {
16 | t.Fatalf("expected to pack message. err: %s", err)
17 | }
18 | if err := m.Unpack(mb); err != nil {
19 | t.Fatalf("expected to unpack message. missing salt? err: %s", err)
20 | }
21 | in := rr.(*NSEC3).Salt
22 | out := m.Answer[0].(*NSEC3).Salt
23 | if in != out {
24 | t.Fatalf("expected salts to match. packed: `%s`. returned: `%s`", in, out)
25 | }
26 | }
27 |
28 | func TestNSEC3MixedNextDomain(t *testing.T) {
29 | rr := testRR("ji6neoaepv8b5o6k4ev33abha8ht9fgc.example. NSEC3 1 1 12 - k8udemvp1j2f7eg6jebps17vp3n8i58h")
30 | m := new(Msg)
31 | m.Answer = []RR{rr}
32 | mb, err := m.Pack()
33 | if err != nil {
34 | t.Fatalf("expected to pack message. err: %s", err)
35 | }
36 | if err := m.Unpack(mb); err != nil {
37 | t.Fatalf("expected to unpack message. err: %s", err)
38 | }
39 | in := strings.ToUpper(rr.(*NSEC3).NextDomain)
40 | out := m.Answer[0].(*NSEC3).NextDomain
41 | if in != out {
42 | t.Fatalf("expected round trip to produce NextDomain `%s`, instead `%s`", in, out)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/dns/labels.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // Holds a bunch of helper functions for dealing with labels.
4 |
5 | // SplitDomainName splits a name string into it's labels.
6 | // www.miek.nl. returns []string{"www", "miek", "nl"}
7 | // .www.miek.nl. returns []string{"", "www", "miek", "nl"},
8 | // The root label (.) returns nil. Note that using
9 | // strings.Split(s) will work in most cases, but does not handle
10 | // escaped dots (\.) for instance.
11 | // s must be a syntactically valid domain name, see IsDomainName.
12 | func SplitDomainName(s string) (labels []string) {
13 | if s == "" {
14 | return nil
15 | }
16 | fqdnEnd := 0 // offset of the final '.' or the length of the name
17 | idx := Split(s)
18 | begin := 0
19 | if IsFqdn(s) {
20 | fqdnEnd = len(s) - 1
21 | } else {
22 | fqdnEnd = len(s)
23 | }
24 |
25 | switch len(idx) {
26 | case 0:
27 | return nil
28 | case 1:
29 | // no-op
30 | default:
31 | for _, end := range idx[1:] {
32 | labels = append(labels, s[begin:end-1])
33 | begin = end
34 | }
35 | }
36 |
37 | return append(labels, s[begin:fqdnEnd])
38 | }
39 |
40 | // CompareDomainName compares the names s1 and s2 and
41 | // returns how many labels they have in common starting from the *right*.
42 | // The comparison stops at the first inequality. The names are downcased
43 | // before the comparison.
44 | //
45 | // www.miek.nl. and miek.nl. have two labels in common: miek and nl
46 | // www.miek.nl. and www.bla.nl. have one label in common: nl
47 | //
48 | // s1 and s2 must be syntactically valid domain names.
49 | func CompareDomainName(s1, s2 string) (n int) {
50 | // the first check: root label
51 | if s1 == "." || s2 == "." {
52 | return 0
53 | }
54 |
55 | l1 := Split(s1)
56 | l2 := Split(s2)
57 |
58 | j1 := len(l1) - 1 // end
59 | i1 := len(l1) - 2 // start
60 | j2 := len(l2) - 1
61 | i2 := len(l2) - 2
62 | // the second check can be done here: last/only label
63 | // before we fall through into the for-loop below
64 | if equal(s1[l1[j1]:], s2[l2[j2]:]) {
65 | n++
66 | } else {
67 | return
68 | }
69 | for {
70 | if i1 < 0 || i2 < 0 {
71 | break
72 | }
73 | if equal(s1[l1[i1]:l1[j1]], s2[l2[i2]:l2[j2]]) {
74 | n++
75 | } else {
76 | break
77 | }
78 | j1--
79 | i1--
80 | j2--
81 | i2--
82 | }
83 | return
84 | }
85 |
86 | // CountLabel counts the number of labels in the string s.
87 | // s must be a syntactically valid domain name.
88 | func CountLabel(s string) (labels int) {
89 | if s == "." {
90 | return
91 | }
92 | off := 0
93 | end := false
94 | for {
95 | off, end = NextLabel(s, off)
96 | labels++
97 | if end {
98 | return
99 | }
100 | }
101 | }
102 |
103 | // Split splits a name s into its label indexes.
104 | // www.miek.nl. returns []int{0, 4, 9}, www.miek.nl also returns []int{0, 4, 9}.
105 | // The root name (.) returns nil. Also see SplitDomainName.
106 | // s must be a syntactically valid domain name.
107 | func Split(s string) []int {
108 | if s == "." {
109 | return nil
110 | }
111 | idx := make([]int, 1, 3)
112 | off := 0
113 | end := false
114 |
115 | for {
116 | off, end = NextLabel(s, off)
117 | if end {
118 | return idx
119 | }
120 | idx = append(idx, off)
121 | }
122 | }
123 |
124 | // NextLabel returns the index of the start of the next label in the
125 | // string s starting at offset.
126 | // The bool end is true when the end of the string has been reached.
127 | // Also see PrevLabel.
128 | func NextLabel(s string, offset int) (i int, end bool) {
129 | if s == "" {
130 | return 0, true
131 | }
132 | for i = offset; i < len(s)-1; i++ {
133 | if s[i] != '.' {
134 | continue
135 | }
136 | j := i - 1
137 | for j >= 0 && s[j] == '\\' {
138 | j--
139 | }
140 |
141 | if (j-i)%2 == 0 {
142 | continue
143 | }
144 |
145 | return i + 1, false
146 | }
147 | return i + 1, true
148 | }
149 |
150 | // PrevLabel returns the index of the label when starting from the right and
151 | // jumping n labels to the left.
152 | // The bool start is true when the start of the string has been overshot.
153 | // Also see NextLabel.
154 | func PrevLabel(s string, n int) (i int, start bool) {
155 | if s == "" {
156 | return 0, true
157 | }
158 | if n == 0 {
159 | return len(s), false
160 | }
161 |
162 | l := len(s) - 1
163 | if s[l] == '.' {
164 | l--
165 | }
166 |
167 | for ; l >= 0 && n > 0; l-- {
168 | if s[l] != '.' {
169 | continue
170 | }
171 | j := l - 1
172 | for j >= 0 && s[j] == '\\' {
173 | j--
174 | }
175 |
176 | if (j-l)%2 == 0 {
177 | continue
178 | }
179 |
180 | n--
181 | if n == 0 {
182 | return l + 1, false
183 | }
184 | }
185 |
186 | return 0, n > 1
187 | }
188 |
189 | // equal compares a and b while ignoring case. It returns true when equal otherwise false.
190 | func equal(a, b string) bool {
191 | // might be lifted into API function.
192 | la := len(a)
193 | lb := len(b)
194 | if la != lb {
195 | return false
196 | }
197 |
198 | for i := la - 1; i >= 0; i-- {
199 | ai := a[i]
200 | bi := b[i]
201 | if ai >= 'A' && ai <= 'Z' {
202 | ai |= 'a' - 'A'
203 | }
204 | if bi >= 'A' && bi <= 'Z' {
205 | bi |= 'a' - 'A'
206 | }
207 | if ai != bi {
208 | return false
209 | }
210 | }
211 | return true
212 | }
213 |
--------------------------------------------------------------------------------
/dns/leak_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "runtime"
7 | "sort"
8 | "strings"
9 | "testing"
10 | "time"
11 | )
12 |
13 | // copied from net/http/main_test.go
14 |
15 | func interestingGoroutines() (gs []string) {
16 | buf := make([]byte, 2<<20)
17 | buf = buf[:runtime.Stack(buf, true)]
18 | for _, g := range strings.Split(string(buf), "\n\n") {
19 | sl := strings.SplitN(g, "\n", 2)
20 | if len(sl) != 2 {
21 | continue
22 | }
23 | stack := strings.TrimSpace(sl[1])
24 | if stack == "" ||
25 | strings.Contains(stack, "testing.(*M).before.func1") ||
26 | strings.Contains(stack, "os/signal.signal_recv") ||
27 | strings.Contains(stack, "created by net.startServer") ||
28 | strings.Contains(stack, "created by testing.RunTests") ||
29 | strings.Contains(stack, "closeWriteAndWait") ||
30 | strings.Contains(stack, "testing.Main(") ||
31 | strings.Contains(stack, "testing.(*T).Run(") ||
32 | // These only show up with GOTRACEBACK=2; Issue 5005 (comment 28)
33 | strings.Contains(stack, "runtime.goexit") ||
34 | strings.Contains(stack, "created by runtime.gc") ||
35 | strings.Contains(stack, "dns.interestingGoroutines") ||
36 | strings.Contains(stack, "runtime.MHeap_Scavenger") {
37 | continue
38 | }
39 | gs = append(gs, stack)
40 | }
41 | sort.Strings(gs)
42 | return
43 | }
44 |
45 | func goroutineLeaked() error {
46 | if testing.Short() {
47 | // Don't worry about goroutine leaks in -short mode or in
48 | // benchmark mode. Too distracting when there are false positives.
49 | return nil
50 | }
51 |
52 | var stackCount map[string]int
53 | for i := 0; i < 5; i++ {
54 | n := 0
55 | stackCount = make(map[string]int)
56 | gs := interestingGoroutines()
57 | for _, g := range gs {
58 | stackCount[g]++
59 | n++
60 | }
61 | if n == 0 {
62 | return nil
63 | }
64 | // Wait for goroutines to schedule and die off:
65 | time.Sleep(100 * time.Millisecond)
66 | }
67 | for stack, count := range stackCount {
68 | fmt.Fprintf(os.Stderr, "%d instances of:\n%s\n", count, stack)
69 | }
70 | return fmt.Errorf("too many goroutines running after dns test(s)")
71 | }
72 |
--------------------------------------------------------------------------------
/dns/listen_no_reuseport.go:
--------------------------------------------------------------------------------
1 | // +build !go1.11 !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd
2 |
3 | package dns
4 |
5 | import "net"
6 |
7 | const supportsReusePort = false
8 |
9 | func listenTCP(network, addr string, reuseport bool) (net.Listener, error) {
10 | if reuseport {
11 | // TODO(tmthrgd): return an error?
12 | }
13 |
14 | return net.Listen(network, addr)
15 | }
16 |
17 | func listenUDP(network, addr string, reuseport bool) (net.PacketConn, error) {
18 | if reuseport {
19 | // TODO(tmthrgd): return an error?
20 | }
21 |
22 | return net.ListenPacket(network, addr)
23 | }
24 |
--------------------------------------------------------------------------------
/dns/listen_reuseport.go:
--------------------------------------------------------------------------------
1 | // +build go1.11
2 | // +build aix darwin dragonfly freebsd linux netbsd openbsd
3 |
4 | package dns
5 |
6 | import (
7 | "context"
8 | "net"
9 | "syscall"
10 |
11 | "golang.org/x/sys/unix"
12 | )
13 |
14 | const supportsReusePort = true
15 |
16 | func reuseportControl(network, address string, c syscall.RawConn) error {
17 | var opErr error
18 | err := c.Control(func(fd uintptr) {
19 | opErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
20 | })
21 | if err != nil {
22 | return err
23 | }
24 |
25 | return opErr
26 | }
27 |
28 | func listenTCP(network, addr string, reuseport bool) (net.Listener, error) {
29 | var lc net.ListenConfig
30 | if reuseport {
31 | lc.Control = reuseportControl
32 | }
33 |
34 | return lc.Listen(context.Background(), network, addr)
35 | }
36 |
37 | func listenUDP(network, addr string, reuseport bool) (net.PacketConn, error) {
38 | var lc net.ListenConfig
39 | if reuseport {
40 | lc.Control = reuseportControl
41 | }
42 |
43 | return lc.ListenPacket(context.Background(), network, addr)
44 | }
45 |
--------------------------------------------------------------------------------
/dns/msg_truncate.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // Truncate ensures the reply message will fit into the requested buffer
4 | // size by removing records that exceed the requested size.
5 | //
6 | // It will first check if the reply fits without compression and then with
7 | // compression. If it won't fit with compression, Truncate then walks the
8 | // record adding as many records as possible without exceeding the
9 | // requested buffer size.
10 | //
11 | // If the message fits within the requested size without compression,
12 | // Truncate will set the message's Compress attribute to false. It is
13 | // the caller's responsibility to set it back to true if they wish to
14 | // compress the payload regardless of size.
15 | //
16 | // The TC bit will be set if any records were excluded from the message.
17 | // If the TC bit is already set on the message it will be retained.
18 | // TC indicates that the client should retry over TCP.
19 | //
20 | // According to RFC 2181, the TC bit should only be set if not all of the
21 | // "required" RRs can be included in the response. Unfortunately, we have
22 | // no way of knowing which RRs are required so we set the TC bit if any RR
23 | // had to be omitted from the response.
24 | //
25 | // The appropriate buffer size can be retrieved from the requests OPT
26 | // record, if present, and is transport specific otherwise. dns.MinMsgSize
27 | // should be used for UDP requests without an OPT record, and
28 | // dns.MaxMsgSize for TCP requests without an OPT record.
29 | func (dns *Msg) Truncate(size int) {
30 | if dns.IsTsig() != nil {
31 | // To simplify this implementation, we don't perform
32 | // truncation on responses with a TSIG record.
33 | return
34 | }
35 |
36 | // RFC 6891 mandates that the payload size in an OPT record
37 | // less than 512 (MinMsgSize) bytes must be treated as equal to 512 bytes.
38 | //
39 | // For ease of use, we impose that restriction here.
40 | if size < MinMsgSize {
41 | size = MinMsgSize
42 | }
43 |
44 | l := msgLenWithCompressionMap(dns, nil) // uncompressed length
45 | if l <= size {
46 | // Don't waste effort compressing this message.
47 | dns.Compress = false
48 | return
49 | }
50 |
51 | dns.Compress = true
52 |
53 | edns0 := dns.popEdns0()
54 | if edns0 != nil {
55 | // Account for the OPT record that gets added at the end,
56 | // by subtracting that length from our budget.
57 | //
58 | // The EDNS(0) OPT record must have the root domain and
59 | // it's length is thus unaffected by compression.
60 | size -= Len(edns0)
61 | }
62 |
63 | compression := make(map[string]struct{})
64 |
65 | l = headerSize
66 | for _, r := range dns.Question {
67 | l += r.len(l, compression)
68 | }
69 |
70 | var numAnswer int
71 | if l < size {
72 | l, numAnswer = truncateLoop(dns.Answer, size, l, compression)
73 | }
74 |
75 | var numNS int
76 | if l < size {
77 | l, numNS = truncateLoop(dns.Ns, size, l, compression)
78 | }
79 |
80 | var numExtra int
81 | if l < size {
82 | _, numExtra = truncateLoop(dns.Extra, size, l, compression)
83 | }
84 |
85 | // See the function documentation for when we set this.
86 | dns.Truncated = dns.Truncated || len(dns.Answer) > numAnswer ||
87 | len(dns.Ns) > numNS || len(dns.Extra) > numExtra
88 |
89 | dns.Answer = dns.Answer[:numAnswer]
90 | dns.Ns = dns.Ns[:numNS]
91 | dns.Extra = dns.Extra[:numExtra]
92 |
93 | if edns0 != nil {
94 | // Add the OPT record back onto the additional section.
95 | dns.Extra = append(dns.Extra, edns0)
96 | }
97 | }
98 |
99 | func truncateLoop(rrs []RR, size, l int, compression map[string]struct{}) (int, int) {
100 | for i, r := range rrs {
101 | if r == nil {
102 | continue
103 | }
104 |
105 | l += r.len(l, compression)
106 | if l > size {
107 | // Return size, rather than l prior to this record,
108 | // to prevent any further records being added.
109 | return size, i
110 | }
111 | if l == size {
112 | return l, i + 1
113 | }
114 | }
115 |
116 | return l, len(rrs)
117 | }
118 |
--------------------------------------------------------------------------------
/dns/msg_truncate_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestRequestTruncateAnswer(t *testing.T) {
9 | m := new(Msg)
10 | m.SetQuestion("large.example.com.", TypeSRV)
11 |
12 | reply := new(Msg)
13 | reply.SetReply(m)
14 | for i := 1; i < 200; i++ {
15 | reply.Answer = append(reply.Answer, testRR(
16 | fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
17 | }
18 |
19 | reply.Truncate(MinMsgSize)
20 | if want, got := MinMsgSize, reply.Len(); want < got {
21 | t.Errorf("message length should be bellow %d bytes, got %d bytes", want, got)
22 | }
23 | if !reply.Truncated {
24 | t.Errorf("truncated bit should be set")
25 | }
26 | }
27 |
28 | func TestRequestTruncateExtra(t *testing.T) {
29 | m := new(Msg)
30 | m.SetQuestion("large.example.com.", TypeSRV)
31 |
32 | reply := new(Msg)
33 | reply.SetReply(m)
34 | for i := 1; i < 200; i++ {
35 | reply.Extra = append(reply.Extra, testRR(
36 | fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
37 | }
38 |
39 | reply.Truncate(MinMsgSize)
40 | if want, got := MinMsgSize, reply.Len(); want < got {
41 | t.Errorf("message length should be bellow %d bytes, got %d bytes", want, got)
42 | }
43 | if !reply.Truncated {
44 | t.Errorf("truncated bit should be set")
45 | }
46 | }
47 |
48 | func TestRequestTruncateExtraEdns0(t *testing.T) {
49 | const size = 4096
50 |
51 | m := new(Msg)
52 | m.SetQuestion("large.example.com.", TypeSRV)
53 | m.SetEdns0(size, true)
54 |
55 | reply := new(Msg)
56 | reply.SetReply(m)
57 | for i := 1; i < 200; i++ {
58 | reply.Extra = append(reply.Extra, testRR(
59 | fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
60 | }
61 | reply.SetEdns0(size, true)
62 |
63 | reply.Truncate(size)
64 | if want, got := size, reply.Len(); want < got {
65 | t.Errorf("message length should be bellow %d bytes, got %d bytes", want, got)
66 | }
67 | if !reply.Truncated {
68 | t.Errorf("truncated bit should be set")
69 | }
70 | opt := reply.Extra[len(reply.Extra)-1]
71 | if opt.Header().Rrtype != TypeOPT {
72 | t.Errorf("expected last RR to be OPT")
73 | }
74 | }
75 |
76 | func TestRequestTruncateExtraRegression(t *testing.T) {
77 | const size = 2048
78 |
79 | m := new(Msg)
80 | m.SetQuestion("large.example.com.", TypeSRV)
81 | m.SetEdns0(size, true)
82 |
83 | reply := new(Msg)
84 | reply.SetReply(m)
85 | for i := 1; i < 33; i++ {
86 | reply.Answer = append(reply.Answer, testRR(
87 | fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
88 | }
89 | for i := 1; i < 33; i++ {
90 | reply.Extra = append(reply.Extra, testRR(
91 | fmt.Sprintf("10-0-0-%d.default.pod.k8s.example.com. 10 IN A 10.0.0.%d", i, i)))
92 | }
93 | reply.SetEdns0(size, true)
94 |
95 | reply.Truncate(size)
96 | if want, got := size, reply.Len(); want < got {
97 | t.Errorf("message length should be bellow %d bytes, got %d bytes", want, got)
98 | }
99 | if !reply.Truncated {
100 | t.Errorf("truncated bit should be set")
101 | }
102 | opt := reply.Extra[len(reply.Extra)-1]
103 | if opt.Header().Rrtype != TypeOPT {
104 | t.Errorf("expected last RR to be OPT")
105 | }
106 | }
107 |
108 | func TestTruncation(t *testing.T) {
109 | reply := new(Msg)
110 |
111 | for i := 0; i < 61; i++ {
112 | 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)))
113 | }
114 |
115 | for i := 0; i < 5; i++ {
116 | 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)))
117 | }
118 |
119 | for i := 0; i < 5; i++ {
120 | 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)))
121 | }
122 |
123 | for bufsize := 1024; bufsize <= 4096; bufsize += 12 {
124 | m := new(Msg)
125 | m.SetQuestion("http.service.tcp.srv.k8s.example.org.", TypeSRV)
126 | m.SetEdns0(uint16(bufsize), true)
127 |
128 | copy := reply.Copy()
129 | copy.SetReply(m)
130 |
131 | copy.Truncate(bufsize)
132 | if want, got := bufsize, copy.Len(); want < got {
133 | t.Errorf("message length should be bellow %d bytes, got %d bytes", want, got)
134 | }
135 | }
136 | }
137 |
138 | func TestRequestTruncateAnswerExact(t *testing.T) {
139 | const size = 867 // Bit fiddly, but this hits the rl == size break clause in Truncate, 52 RRs should remain.
140 |
141 | m := new(Msg)
142 | m.SetQuestion("large.example.com.", TypeSRV)
143 | m.SetEdns0(size, false)
144 |
145 | reply := new(Msg)
146 | reply.SetReply(m)
147 | for i := 1; i < 200; i++ {
148 | reply.Answer = append(reply.Answer, testRR(fmt.Sprintf("large.example.com. 10 IN A 127.0.0.%d", i)))
149 | }
150 |
151 | reply.Truncate(size)
152 | if want, got := size, reply.Len(); want < got {
153 | t.Errorf("message length should be bellow %d bytes, got %d bytes", want, got)
154 | }
155 | if expected := 52; len(reply.Answer) != expected {
156 | t.Errorf("wrong number of answers; expected %d, got %d", expected, len(reply.Answer))
157 | }
158 | }
159 |
160 | func BenchmarkMsgTruncate(b *testing.B) {
161 | const size = 2048
162 |
163 | m := new(Msg)
164 | m.SetQuestion("example.com.", TypeA)
165 | m.SetEdns0(size, true)
166 |
167 | reply := new(Msg)
168 | reply.SetReply(m)
169 | for i := 1; i < 33; i++ {
170 | reply.Answer = append(reply.Answer, testRR(
171 | fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
172 | }
173 | for i := 1; i < 33; i++ {
174 | reply.Extra = append(reply.Extra, testRR(
175 | fmt.Sprintf("10-0-0-%d.default.pod.k8s.example.com. 10 IN A 10.0.0.%d", i, i)))
176 | }
177 |
178 | b.ResetTimer()
179 |
180 | for i := 0; i < b.N; i++ {
181 | b.StopTimer()
182 | copy := reply.Copy()
183 | b.StartTimer()
184 |
185 | copy.Truncate(size)
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/dns/nsecx.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "crypto/sha1"
5 | "encoding/hex"
6 | "strings"
7 | )
8 |
9 | // HashName hashes a string (label) according to RFC 5155. It returns the hashed string in uppercase.
10 | func HashName(label string, ha uint8, iter uint16, salt string) string {
11 | if ha != SHA1 {
12 | return ""
13 | }
14 |
15 | wireSalt := make([]byte, hex.DecodedLen(len(salt)))
16 | n, err := packStringHex(salt, wireSalt, 0)
17 | if err != nil {
18 | return ""
19 | }
20 | wireSalt = wireSalt[:n]
21 |
22 | name := make([]byte, 255)
23 | off, err := PackDomainName(strings.ToLower(label), name, 0, nil, false)
24 | if err != nil {
25 | return ""
26 | }
27 | name = name[:off]
28 |
29 | s := sha1.New()
30 | // k = 0
31 | s.Write(name)
32 | s.Write(wireSalt)
33 | nsec3 := s.Sum(nil)
34 |
35 | // k > 0
36 | for k := uint16(0); k < iter; k++ {
37 | s.Reset()
38 | s.Write(nsec3)
39 | s.Write(wireSalt)
40 | nsec3 = s.Sum(nsec3[:0])
41 | }
42 |
43 | return toBase32(nsec3)
44 | }
45 |
46 | // Cover returns true if a name is covered by the NSEC3 record.
47 | func (rr *NSEC3) Cover(name string) bool {
48 | nameHash := HashName(name, rr.Hash, rr.Iterations, rr.Salt)
49 | owner := strings.ToUpper(rr.Hdr.Name)
50 | labelIndices := Split(owner)
51 | if len(labelIndices) < 2 {
52 | return false
53 | }
54 | ownerHash := owner[:labelIndices[1]-1]
55 | ownerZone := owner[labelIndices[1]:]
56 | if !IsSubDomain(ownerZone, strings.ToUpper(name)) { // name is outside owner zone
57 | return false
58 | }
59 |
60 | nextHash := rr.NextDomain
61 |
62 | // if empty interval found, try cover wildcard hashes so nameHash shouldn't match with ownerHash
63 | if ownerHash == nextHash && nameHash != ownerHash { // empty interval
64 | return true
65 | }
66 | if ownerHash > nextHash { // end of zone
67 | if nameHash > ownerHash { // covered since there is nothing after ownerHash
68 | return true
69 | }
70 | return nameHash < nextHash // if nameHash is before beginning of zone it is covered
71 | }
72 | if nameHash < ownerHash { // nameHash is before ownerHash, not covered
73 | return false
74 | }
75 | return nameHash < nextHash // if nameHash is before nextHash is it covered (between ownerHash and nextHash)
76 | }
77 |
78 | // Match returns true if a name matches the NSEC3 record
79 | func (rr *NSEC3) Match(name string) bool {
80 | nameHash := HashName(name, rr.Hash, rr.Iterations, rr.Salt)
81 | owner := strings.ToUpper(rr.Hdr.Name)
82 | labelIndices := Split(owner)
83 | if len(labelIndices) < 2 {
84 | return false
85 | }
86 | ownerHash := owner[:labelIndices[1]-1]
87 | ownerZone := owner[labelIndices[1]:]
88 | if !IsSubDomain(ownerZone, strings.ToUpper(name)) { // name is outside owner zone
89 | return false
90 | }
91 | if ownerHash == nameHash {
92 | return true
93 | }
94 | return false
95 | }
96 |
--------------------------------------------------------------------------------
/dns/nsecx_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 | )
7 |
8 | func TestPackNsec3(t *testing.T) {
9 | nsec3 := HashName("dnsex.nl.", SHA1, 0, "DEAD")
10 | if nsec3 != "ROCCJAE8BJJU7HN6T7NG3TNM8ACRS87J" {
11 | t.Error(nsec3)
12 | }
13 |
14 | nsec3 = HashName("a.b.c.example.org.", SHA1, 2, "DEAD")
15 | if nsec3 != "6LQ07OAHBTOOEU2R9ANI2AT70K5O0RCG" {
16 | t.Error(nsec3)
17 | }
18 | }
19 |
20 | func TestNsec3(t *testing.T) {
21 | nsec3 := testRR("sk4e8fj94u78smusb40o1n0oltbblu2r.nl. IN NSEC3 1 1 5 F10E9F7EA83FC8F3 SK4F38CQ0ATIEI8MH3RGD0P5I4II6QAN NS SOA TXT RRSIG DNSKEY NSEC3PARAM")
22 | if !nsec3.(*NSEC3).Match("nl.") { // name hash = sk4e8fj94u78smusb40o1n0oltbblu2r
23 | t.Fatal("sk4e8fj94u78smusb40o1n0oltbblu2r.nl. should match sk4e8fj94u78smusb40o1n0oltbblu2r.nl.")
24 | }
25 | if !nsec3.(*NSEC3).Match("NL.") { // name hash = sk4e8fj94u78smusb40o1n0oltbblu2r
26 | t.Fatal("sk4e8fj94u78smusb40o1n0oltbblu2r.NL. should match sk4e8fj94u78smusb40o1n0oltbblu2r.nl.")
27 | }
28 | if nsec3.(*NSEC3).Match("com.") { //
29 | t.Fatal("com. is not in the zone nl.")
30 | }
31 | if nsec3.(*NSEC3).Match("test.nl.") { // name hash = gd0ptr5bnfpimpu2d3v6gd4n0bai7s0q
32 | t.Fatal("gd0ptr5bnfpimpu2d3v6gd4n0bai7s0q.nl. should not match sk4e8fj94u78smusb40o1n0oltbblu2r.nl.")
33 | }
34 | nsec3 = testRR("nl. IN NSEC3 1 1 5 F10E9F7EA83FC8F3 SK4F38CQ0ATIEI8MH3RGD0P5I4II6QAN NS SOA TXT RRSIG DNSKEY NSEC3PARAM")
35 | if nsec3.(*NSEC3).Match("nl.") {
36 | t.Fatal("sk4e8fj94u78smusb40o1n0oltbblu2r.nl. should not match a record without a owner hash")
37 | }
38 |
39 | for _, tc := range []struct {
40 | rr *NSEC3
41 | name string
42 | covers bool
43 | }{
44 | // positive tests
45 | { // name hash between owner hash and next hash
46 | rr: &NSEC3{
47 | Hdr: RR_Header{Name: "2N1TB3VAIRUOBL6RKDVII42N9TFMIALP.com."},
48 | Hash: 1,
49 | Flags: 1,
50 | Iterations: 5,
51 | Salt: "F10E9F7EA83FC8F3",
52 | NextDomain: "PT3RON8N7PM3A0OE989IB84OOSADP7O8",
53 | },
54 | name: "bsd.com.",
55 | covers: true,
56 | },
57 | { // end of zone, name hash is after owner hash
58 | rr: &NSEC3{
59 | Hdr: RR_Header{Name: "3v62ulr0nre83v0rja2vjgtlif9v6rab.com."},
60 | Hash: 1,
61 | Flags: 1,
62 | Iterations: 5,
63 | Salt: "F10E9F7EA83FC8F3",
64 | NextDomain: "2N1TB3VAIRUOBL6RKDVII42N9TFMIALP",
65 | },
66 | name: "csd.com.",
67 | covers: true,
68 | },
69 | { // end of zone, name hash is before beginning of zone
70 | rr: &NSEC3{
71 | Hdr: RR_Header{Name: "PT3RON8N7PM3A0OE989IB84OOSADP7O8.com."},
72 | Hash: 1,
73 | Flags: 1,
74 | Iterations: 5,
75 | Salt: "F10E9F7EA83FC8F3",
76 | NextDomain: "3V62ULR0NRE83V0RJA2VJGTLIF9V6RAB",
77 | },
78 | name: "asd.com.",
79 | covers: true,
80 | },
81 | // negative tests
82 | { // too short owner name
83 | rr: &NSEC3{
84 | Hdr: RR_Header{Name: "nl."},
85 | Hash: 1,
86 | Flags: 1,
87 | Iterations: 5,
88 | Salt: "F10E9F7EA83FC8F3",
89 | NextDomain: "39P99DCGG0MDLARTCRMCF6OFLLUL7PR6",
90 | },
91 | name: "asd.com.",
92 | covers: false,
93 | },
94 | { // outside of zone
95 | rr: &NSEC3{
96 | Hdr: RR_Header{Name: "39p91242oslggest5e6a7cci4iaeqvnk.nl."},
97 | Hash: 1,
98 | Flags: 1,
99 | Iterations: 5,
100 | Salt: "F10E9F7EA83FC8F3",
101 | NextDomain: "39P99DCGG0MDLARTCRMCF6OFLLUL7PR6",
102 | },
103 | name: "asd.com.",
104 | covers: false,
105 | },
106 | { // empty interval
107 | rr: &NSEC3{
108 | Hdr: RR_Header{Name: "2n1tb3vairuobl6rkdvii42n9tfmialp.com."},
109 | Hash: 1,
110 | Flags: 1,
111 | Iterations: 5,
112 | Salt: "F10E9F7EA83FC8F3",
113 | NextDomain: "2N1TB3VAIRUOBL6RKDVII42N9TFMIALP",
114 | },
115 | name: "asd.com.",
116 | covers: false,
117 | },
118 | { // empty interval wildcard
119 | rr: &NSEC3{
120 | Hdr: RR_Header{Name: "2n1tb3vairuobl6rkdvii42n9tfmialp.com."},
121 | Hash: 1,
122 | Flags: 1,
123 | Iterations: 5,
124 | Salt: "F10E9F7EA83FC8F3",
125 | NextDomain: "2N1TB3VAIRUOBL6RKDVII42N9TFMIALP",
126 | },
127 | name: "*.asd.com.",
128 | covers: true,
129 | },
130 | { // name hash is before owner hash, not covered
131 | rr: &NSEC3{
132 | Hdr: RR_Header{Name: "3V62ULR0NRE83V0RJA2VJGTLIF9V6RAB.com."},
133 | Hash: 1,
134 | Flags: 1,
135 | Iterations: 5,
136 | Salt: "F10E9F7EA83FC8F3",
137 | NextDomain: "PT3RON8N7PM3A0OE989IB84OOSADP7O8",
138 | },
139 | name: "asd.com.",
140 | covers: false,
141 | },
142 | } {
143 | covers := tc.rr.Cover(tc.name)
144 | if tc.covers != covers {
145 | t.Fatalf("cover failed for %s: expected %t, got %t [record: %s]", tc.name, tc.covers, covers, tc.rr)
146 | }
147 | }
148 | }
149 |
150 | func TestNsec3EmptySalt(t *testing.T) {
151 | rr, _ := NewRR("CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 86400 IN NSEC3 1 1 0 - CK0Q1GIN43N1ARRC9OSM6QPQR81H5M9A NS SOA RRSIG DNSKEY NSEC3PARAM")
152 |
153 | if !rr.(*NSEC3).Match("com.") {
154 | t.Fatalf("expected record to match com. label")
155 | }
156 | }
157 |
158 | func BenchmarkHashName(b *testing.B) {
159 | for _, iter := range []uint16{
160 | 150, 2500, 5000, 10000, ^uint16(0),
161 | } {
162 | b.Run(strconv.Itoa(int(iter)), func(b *testing.B) {
163 | for n := 0; n < b.N; n++ {
164 | if HashName("some.example.org.", SHA1, iter, "deadbeef") == "" {
165 | b.Fatalf("HashName failed")
166 | }
167 | }
168 | })
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/dns/privaterr.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "strings"
4 |
5 | // PrivateRdata is an interface used for implementing "Private Use" RR types, see
6 | // RFC 6895. This allows one to experiment with new RR types, without requesting an
7 | // official type code. Also see dns.PrivateHandle and dns.PrivateHandleRemove.
8 | type PrivateRdata interface {
9 | // String returns the text presentation of the Rdata of the Private RR.
10 | String() string
11 | // Parse parses the Rdata of the private RR.
12 | Parse([]string) error
13 | // Pack is used when packing a private RR into a buffer.
14 | Pack([]byte) (int, error)
15 | // Unpack is used when unpacking a private RR from a buffer.
16 | Unpack([]byte) (int, error)
17 | // Copy copies the Rdata into the PrivateRdata argument.
18 | Copy(PrivateRdata) error
19 | // Len returns the length in octets of the Rdata.
20 | Len() int
21 | }
22 |
23 | // PrivateRR represents an RR that uses a PrivateRdata user-defined type.
24 | // It mocks normal RRs and implements dns.RR interface.
25 | type PrivateRR struct {
26 | Hdr RR_Header
27 | Data PrivateRdata
28 |
29 | generator func() PrivateRdata // for copy
30 | }
31 |
32 | // Header return the RR header of r.
33 | func (r *PrivateRR) Header() *RR_Header { return &r.Hdr }
34 |
35 | func (r *PrivateRR) String() string { return r.Hdr.String() + r.Data.String() }
36 |
37 | // Private len and copy parts to satisfy RR interface.
38 | func (r *PrivateRR) len(off int, compression map[string]struct{}) int {
39 | l := r.Hdr.len(off, compression)
40 | l += r.Data.Len()
41 | return l
42 | }
43 |
44 | func (r *PrivateRR) copy() RR {
45 | // make new RR like this:
46 | rr := &PrivateRR{r.Hdr, r.generator(), r.generator}
47 |
48 | if err := r.Data.Copy(rr.Data); err != nil {
49 | panic("dns: got value that could not be used to copy Private rdata: " + err.Error())
50 | }
51 |
52 | return rr
53 | }
54 |
55 | func (r *PrivateRR) pack(msg []byte, off int, compression compressionMap, compress bool) (int, error) {
56 | n, err := r.Data.Pack(msg[off:])
57 | if err != nil {
58 | return len(msg), err
59 | }
60 | off += n
61 | return off, nil
62 | }
63 |
64 | func (r *PrivateRR) unpack(msg []byte, off int) (int, error) {
65 | off1, err := r.Data.Unpack(msg[off:])
66 | off += off1
67 | return off, err
68 | }
69 |
70 | func (r *PrivateRR) parse(c *zlexer, origin string) *ParseError {
71 | var l lex
72 | text := make([]string, 0, 2) // could be 0..N elements, median is probably 1
73 | Fetch:
74 | for {
75 | // TODO(miek): we could also be returning _QUOTE, this might or might not
76 | // be an issue (basically parsing TXT becomes hard)
77 | switch l, _ = c.Next(); l.value {
78 | case zNewline, zEOF:
79 | break Fetch
80 | case zString:
81 | text = append(text, l.token)
82 | }
83 | }
84 |
85 | err := r.Data.Parse(text)
86 | if err != nil {
87 | return &ParseError{"", err.Error(), l}
88 | }
89 |
90 | return nil
91 | }
92 |
93 | func (r *PrivateRR) isDuplicate(r2 RR) bool { return false }
94 |
95 | // PrivateHandle registers a private resource record type. It requires
96 | // string and numeric representation of private RR type and generator function as argument.
97 | func PrivateHandle(rtypestr string, rtype uint16, generator func() PrivateRdata) {
98 | rtypestr = strings.ToUpper(rtypestr)
99 |
100 | TypeToRR[rtype] = func() RR { return &PrivateRR{RR_Header{}, generator(), generator} }
101 | TypeToString[rtype] = rtypestr
102 | StringToType[rtypestr] = rtype
103 | }
104 |
105 | // PrivateHandleRemove removes definitions required to support private RR type.
106 | func PrivateHandleRemove(rtype uint16) {
107 | rtypestr, ok := TypeToString[rtype]
108 | if ok {
109 | delete(TypeToRR, rtype)
110 | delete(TypeToString, rtype)
111 | delete(StringToType, rtypestr)
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/dns/privaterr_test.go:
--------------------------------------------------------------------------------
1 | package dns_test
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "github.com/miekg/dns"
8 | )
9 |
10 | const TypeISBN uint16 = 0xFF00
11 |
12 | // A crazy new RR type :)
13 | type ISBN struct {
14 | x string // rdata with 10 or 13 numbers, dashes or spaces allowed
15 | }
16 |
17 | func NewISBN() dns.PrivateRdata { return &ISBN{""} }
18 |
19 | func (rd *ISBN) Len() int { return len([]byte(rd.x)) }
20 | func (rd *ISBN) String() string { return rd.x }
21 |
22 | func (rd *ISBN) Parse(txt []string) error {
23 | rd.x = strings.TrimSpace(strings.Join(txt, " "))
24 | return nil
25 | }
26 |
27 | func (rd *ISBN) Pack(buf []byte) (int, error) {
28 | b := []byte(rd.x)
29 | n := copy(buf, b)
30 | if n != len(b) {
31 | return n, dns.ErrBuf
32 | }
33 | return n, nil
34 | }
35 |
36 | func (rd *ISBN) Unpack(buf []byte) (int, error) {
37 | rd.x = string(buf)
38 | return len(buf), nil
39 | }
40 |
41 | func (rd *ISBN) Copy(dest dns.PrivateRdata) error {
42 | isbn, ok := dest.(*ISBN)
43 | if !ok {
44 | return dns.ErrRdata
45 | }
46 | isbn.x = rd.x
47 | return nil
48 | }
49 |
50 | var testrecord = strings.Join([]string{"example.org.", "3600", "IN", "ISBN", "12-3 456789-0-123"}, "\t")
51 |
52 | func TestPrivateText(t *testing.T) {
53 | dns.PrivateHandle("ISBN", TypeISBN, NewISBN)
54 | defer dns.PrivateHandleRemove(TypeISBN)
55 |
56 | rr, err := dns.NewRR(testrecord)
57 | if err != nil {
58 | t.Fatal(err)
59 | }
60 | if rr.String() != testrecord {
61 | t.Errorf("record string representation did not match original %#v != %#v", rr.String(), testrecord)
62 | }
63 | }
64 |
65 | func TestPrivateByteSlice(t *testing.T) {
66 | dns.PrivateHandle("ISBN", TypeISBN, NewISBN)
67 | defer dns.PrivateHandleRemove(TypeISBN)
68 |
69 | rr, err := dns.NewRR(testrecord)
70 | if err != nil {
71 | t.Fatal(err)
72 | }
73 |
74 | buf := make([]byte, 100)
75 | off, err := dns.PackRR(rr, buf, 0, nil, false)
76 | if err != nil {
77 | t.Errorf("got error packing ISBN: %v", err)
78 | }
79 |
80 | custrr := rr.(*dns.PrivateRR)
81 | if ln := custrr.Data.Len() + len(custrr.Header().Name) + 11; ln != off {
82 | t.Errorf("offset is not matching to length of Private RR: %d!=%d", off, ln)
83 | }
84 |
85 | rr1, off1, err := dns.UnpackRR(buf[:off], 0)
86 | if err != nil {
87 | t.Errorf("got error unpacking ISBN: %v", err)
88 | return
89 | }
90 |
91 | if off1 != off {
92 | t.Errorf("offset after unpacking differs: %d != %d", off1, off)
93 | }
94 |
95 | if rr1.String() != testrecord {
96 | t.Errorf("record string representation did not match original %#v != %#v", rr1.String(), testrecord)
97 | }
98 | }
99 |
100 | const TypeVERSION uint16 = 0xFF01
101 |
102 | type VERSION struct {
103 | x string
104 | }
105 |
106 | func NewVersion() dns.PrivateRdata { return &VERSION{""} }
107 |
108 | func (rd *VERSION) String() string { return rd.x }
109 | func (rd *VERSION) Parse(txt []string) error {
110 | rd.x = strings.TrimSpace(strings.Join(txt, " "))
111 | return nil
112 | }
113 |
114 | func (rd *VERSION) Pack(buf []byte) (int, error) {
115 | b := []byte(rd.x)
116 | n := copy(buf, b)
117 | if n != len(b) {
118 | return n, dns.ErrBuf
119 | }
120 | return n, nil
121 | }
122 |
123 | func (rd *VERSION) Unpack(buf []byte) (int, error) {
124 | rd.x = string(buf)
125 | return len(buf), nil
126 | }
127 |
128 | func (rd *VERSION) Copy(dest dns.PrivateRdata) error {
129 | isbn, ok := dest.(*VERSION)
130 | if !ok {
131 | return dns.ErrRdata
132 | }
133 | isbn.x = rd.x
134 | return nil
135 | }
136 |
137 | func (rd *VERSION) Len() int {
138 | return len([]byte(rd.x))
139 | }
140 |
141 | var smallzone = `$ORIGIN example.org.
142 | @ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. (
143 | 2014091518 7200 3600 1209600 3600
144 | )
145 | A 1.2.3.4
146 | ok ISBN 1231-92110-12
147 | go VERSION (
148 | 1.3.1 ; comment
149 | )
150 | www ISBN 1231-92110-16
151 | * CNAME @
152 | `
153 |
154 | func TestPrivateZoneParser(t *testing.T) {
155 | dns.PrivateHandle("ISBN", TypeISBN, NewISBN)
156 | dns.PrivateHandle("VERSION", TypeVERSION, NewVersion)
157 | defer dns.PrivateHandleRemove(TypeISBN)
158 | defer dns.PrivateHandleRemove(TypeVERSION)
159 |
160 | r := strings.NewReader(smallzone)
161 | z := dns.NewZoneParser(r, ".", "")
162 |
163 | for _, ok := z.Next(); ok; _, ok = z.Next() {
164 | }
165 | if err := z.Err(); err != nil {
166 | t.Fatal(err)
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/dns/reverse.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // StringToType is the reverse of TypeToString, needed for string parsing.
4 | var StringToType = reverseInt16(TypeToString)
5 |
6 | // StringToClass is the reverse of ClassToString, needed for string parsing.
7 | var StringToClass = reverseInt16(ClassToString)
8 |
9 | // StringToOpcode is a map of opcodes to strings.
10 | var StringToOpcode = reverseInt(OpcodeToString)
11 |
12 | // StringToRcode is a map of rcodes to strings.
13 | var StringToRcode = reverseInt(RcodeToString)
14 |
15 | func init() {
16 | // Preserve previous NOTIMP typo, see github.com/miekg/dns/issues/733.
17 | StringToRcode["NOTIMPL"] = RcodeNotImplemented
18 | }
19 |
20 | // StringToAlgorithm is the reverse of AlgorithmToString.
21 | var StringToAlgorithm = reverseInt8(AlgorithmToString)
22 |
23 | // StringToHash is a map of names to hash IDs.
24 | var StringToHash = reverseInt8(HashToString)
25 |
26 | // StringToCertType is the reverseof CertTypeToString.
27 | var StringToCertType = reverseInt16(CertTypeToString)
28 |
29 | // Reverse a map
30 | func reverseInt8(m map[uint8]string) map[string]uint8 {
31 | n := make(map[string]uint8, len(m))
32 | for u, s := range m {
33 | n[s] = u
34 | }
35 | return n
36 | }
37 |
38 | func reverseInt16(m map[uint16]string) map[string]uint16 {
39 | n := make(map[string]uint16, len(m))
40 | for u, s := range m {
41 | n[s] = u
42 | }
43 | return n
44 | }
45 |
46 | func reverseInt(m map[int]string) map[string]int {
47 | n := make(map[string]int, len(m))
48 | for u, s := range m {
49 | n[s] = u
50 | }
51 | return n
52 | }
53 |
--------------------------------------------------------------------------------
/dns/rr_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // testRR is a helper that wraps a call to NewRR and panics if the error is non-nil.
4 | func testRR(s string) RR {
5 | r, err := NewRR(s)
6 | if err != nil {
7 | panic(err)
8 | }
9 |
10 | return r
11 | }
12 |
--------------------------------------------------------------------------------
/dns/sanitize.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // Dedup removes identical RRs from rrs. It preserves the original ordering.
4 | // The lowest TTL of any duplicates is used in the remaining one. Dedup modifies
5 | // rrs.
6 | // m is used to store the RRs temporary. If it is nil a new map will be allocated.
7 | func Dedup(rrs []RR, m map[string]RR) []RR {
8 |
9 | if m == nil {
10 | m = make(map[string]RR)
11 | }
12 | // Save the keys, so we don't have to call normalizedString twice.
13 | keys := make([]*string, 0, len(rrs))
14 |
15 | for _, r := range rrs {
16 | key := normalizedString(r)
17 | keys = append(keys, &key)
18 | if mr, ok := m[key]; ok {
19 | // Shortest TTL wins.
20 | rh, mrh := r.Header(), mr.Header()
21 | if mrh.Ttl > rh.Ttl {
22 | mrh.Ttl = rh.Ttl
23 | }
24 | continue
25 | }
26 |
27 | m[key] = r
28 | }
29 | // If the length of the result map equals the amount of RRs we got,
30 | // it means they were all different. We can then just return the original rrset.
31 | if len(m) == len(rrs) {
32 | return rrs
33 | }
34 |
35 | j := 0
36 | for i, r := range rrs {
37 | // If keys[i] lives in the map, we should copy and remove it.
38 | if _, ok := m[*keys[i]]; ok {
39 | delete(m, *keys[i])
40 | rrs[j] = r
41 | j++
42 | }
43 |
44 | if len(m) == 0 {
45 | break
46 | }
47 | }
48 |
49 | return rrs[:j]
50 | }
51 |
52 | // normalizedString returns a normalized string from r. The TTL
53 | // is removed and the domain name is lowercased. We go from this:
54 | // DomainNameTTLCLASSTYPERDATA to:
55 | // lowercasenameCLASSTYPE...
56 | func normalizedString(r RR) string {
57 | // A string Go DNS makes has: domainnameTTL...
58 | b := []byte(r.String())
59 |
60 | // find the first non-escaped tab, then another, so we capture where the TTL lives.
61 | esc := false
62 | ttlStart, ttlEnd := 0, 0
63 | for i := 0; i < len(b) && ttlEnd == 0; i++ {
64 | switch {
65 | case b[i] == '\\':
66 | esc = !esc
67 | case b[i] == '\t' && !esc:
68 | if ttlStart == 0 {
69 | ttlStart = i
70 | continue
71 | }
72 | if ttlEnd == 0 {
73 | ttlEnd = i
74 | }
75 | case b[i] >= 'A' && b[i] <= 'Z' && !esc:
76 | b[i] += 32
77 | default:
78 | esc = false
79 | }
80 | }
81 |
82 | // remove TTL.
83 | copy(b[ttlStart:], b[ttlEnd:])
84 | cut := ttlEnd - ttlStart
85 | return string(b[:len(b)-cut])
86 | }
87 |
--------------------------------------------------------------------------------
/dns/sanitize_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "testing"
4 |
5 | func TestDedup(t *testing.T) {
6 | testcases := map[[3]RR][]string{
7 | [...]RR{
8 | testRR("mIek.nl. IN A 127.0.0.1"),
9 | testRR("mieK.nl. IN A 127.0.0.1"),
10 | testRR("miek.Nl. IN A 127.0.0.1"),
11 | }: {"mIek.nl.\t3600\tIN\tA\t127.0.0.1"},
12 | [...]RR{
13 | testRR("miEk.nl. 2000 IN A 127.0.0.1"),
14 | testRR("mieK.Nl. 1000 IN A 127.0.0.1"),
15 | testRR("Miek.nL. 500 IN A 127.0.0.1"),
16 | }: {"miEk.nl.\t500\tIN\tA\t127.0.0.1"},
17 | [...]RR{
18 | testRR("miek.nl. IN A 127.0.0.1"),
19 | testRR("miek.nl. CH A 127.0.0.1"),
20 | testRR("miek.nl. IN A 127.0.0.1"),
21 | }: {"miek.nl.\t3600\tIN\tA\t127.0.0.1",
22 | "miek.nl.\t3600\tCH\tA\t127.0.0.1",
23 | },
24 | [...]RR{
25 | testRR("miek.nl. CH A 127.0.0.1"),
26 | testRR("miek.nl. IN A 127.0.0.1"),
27 | testRR("miek.de. IN A 127.0.0.1"),
28 | }: {"miek.nl.\t3600\tCH\tA\t127.0.0.1",
29 | "miek.nl.\t3600\tIN\tA\t127.0.0.1",
30 | "miek.de.\t3600\tIN\tA\t127.0.0.1",
31 | },
32 | [...]RR{
33 | testRR("miek.de. IN A 127.0.0.1"),
34 | testRR("miek.nl. 200 IN A 127.0.0.1"),
35 | testRR("miek.nl. 300 IN A 127.0.0.1"),
36 | }: {"miek.de.\t3600\tIN\tA\t127.0.0.1",
37 | "miek.nl.\t200\tIN\tA\t127.0.0.1",
38 | },
39 | }
40 |
41 | for rr, expected := range testcases {
42 | out := Dedup([]RR{rr[0], rr[1], rr[2]}, nil)
43 | for i, o := range out {
44 | if o.String() != expected[i] {
45 | t.Fatalf("expected %v, got %v", expected[i], o.String())
46 | }
47 | }
48 | }
49 | }
50 |
51 | func BenchmarkDedup(b *testing.B) {
52 | rrs := []RR{
53 | testRR("miEk.nl. 2000 IN A 127.0.0.1"),
54 | testRR("mieK.Nl. 1000 IN A 127.0.0.1"),
55 | testRR("Miek.nL. 500 IN A 127.0.0.1"),
56 | }
57 | m := make(map[string]RR)
58 | for i := 0; i < b.N; i++ {
59 | Dedup(rrs, m)
60 | }
61 | }
62 |
63 | func TestNormalizedString(t *testing.T) {
64 | tests := map[RR]string{
65 | testRR("mIEk.Nl. 3600 IN A 127.0.0.1"): "miek.nl.\tIN\tA\t127.0.0.1",
66 | testRR("m\\ iek.nL. 3600 IN A 127.0.0.1"): "m\\ iek.nl.\tIN\tA\t127.0.0.1",
67 | testRR("m\\\tIeK.nl. 3600 in A 127.0.0.1"): "m\\009iek.nl.\tIN\tA\t127.0.0.1",
68 | }
69 | for tc, expected := range tests {
70 | n := normalizedString(tc)
71 | if n != expected {
72 | t.Errorf("expected %s, got %s", expected, n)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/dns/serve_mux.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "context"
5 | "sync"
6 | )
7 |
8 | // ServeMux is an DNS request multiplexer. It matches the zone name of
9 | // each incoming request against a list of registered patterns add calls
10 | // the handler for the pattern that most closely matches the zone name.
11 | //
12 | // ServeMux is DNSSEC aware, meaning that queries for the DS record are
13 | // redirected to the parent zone (if that is also registered), otherwise
14 | // the child gets the query.
15 | //
16 | // ServeMux is also safe for concurrent access from multiple goroutines.
17 | //
18 | // The zero ServeMux is empty and ready for use.
19 | type ServeMux struct {
20 | z map[string]Handler
21 | m sync.RWMutex
22 | }
23 |
24 | // NewServeMux allocates and returns a new ServeMux.
25 | func NewServeMux() *ServeMux {
26 | return new(ServeMux)
27 | }
28 |
29 | // DefaultServeMux is the default ServeMux used by Serve.
30 | var DefaultServeMux = NewServeMux()
31 |
32 | func (mux *ServeMux) match(q string, t uint16) Handler {
33 | mux.m.RLock()
34 | defer mux.m.RUnlock()
35 | if mux.z == nil {
36 | return nil
37 | }
38 |
39 | q = CanonicalName(q)
40 |
41 | var handler Handler
42 | for off, end := 0, false; !end; off, end = NextLabel(q, off) {
43 | if h, ok := mux.z[q[off:]]; ok {
44 | if t != TypeDS {
45 | return h
46 | }
47 | // Continue for DS to see if we have a parent too, if so delegate to the parent
48 | handler = h
49 | }
50 | }
51 |
52 | // Wildcard match, if we have found nothing try the root zone as a last resort.
53 | if h, ok := mux.z["."]; ok {
54 | return h
55 | }
56 |
57 | return handler
58 | }
59 |
60 | // Handle adds a handler to the ServeMux for pattern.
61 | func (mux *ServeMux) Handle(pattern string, handler Handler) {
62 | if pattern == "" {
63 | panic("dns: invalid pattern " + pattern)
64 | }
65 | mux.m.Lock()
66 | if mux.z == nil {
67 | mux.z = make(map[string]Handler)
68 | }
69 | mux.z[CanonicalName(pattern)] = handler
70 | mux.m.Unlock()
71 | }
72 |
73 | // HandleFunc adds a handler function to the ServeMux for pattern.
74 | func (mux *ServeMux) HandleFunc(pattern string, handler func(context.Context, ResponseWriter, *Msg)) {
75 | mux.Handle(pattern, HandlerFunc(handler))
76 | }
77 |
78 | // HandleRemove deregisters the handler specific for pattern from the ServeMux.
79 | func (mux *ServeMux) HandleRemove(pattern string) {
80 | if pattern == "" {
81 | panic("dns: invalid pattern " + pattern)
82 | }
83 | mux.m.Lock()
84 | delete(mux.z, CanonicalName(pattern))
85 | mux.m.Unlock()
86 | }
87 |
88 | // ServeDNS dispatches the request to the handler whose pattern most
89 | // closely matches the request message.
90 | //
91 | // ServeDNS is DNSSEC aware, meaning that queries for the DS record
92 | // are redirected to the parent zone (if that is also registered),
93 | // otherwise the child gets the query.
94 | //
95 | // If no handler is found, or there is no question, a standard REFUSED
96 | // message is returned
97 | func (mux *ServeMux) ServeDNS(ctx context.Context, w ResponseWriter, req *Msg) {
98 | var h Handler
99 | if len(req.Question) >= 1 { // allow more than one question
100 | h = mux.match(req.Question[0].Name, req.Question[0].Qtype)
101 | }
102 |
103 | if h != nil {
104 | h.ServeDNS(ctx, w, req)
105 | } else {
106 | handleRefused(ctx, w, req)
107 | }
108 | }
109 |
110 | // Handle registers the handler with the given pattern
111 | // in the DefaultServeMux. The documentation for
112 | // ServeMux explains how patterns are matched.
113 | func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
114 |
115 | // HandleRemove deregisters the handle with the given pattern
116 | // in the DefaultServeMux.
117 | func HandleRemove(pattern string) { DefaultServeMux.HandleRemove(pattern) }
118 |
119 | // HandleFunc registers the handler function with the given pattern
120 | // in the DefaultServeMux.
121 | func HandleFunc(pattern string, handler func(context.Context, ResponseWriter, *Msg)) {
122 | DefaultServeMux.HandleFunc(pattern, handler)
123 | }
124 |
--------------------------------------------------------------------------------
/dns/serve_mux_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "testing"
4 |
5 | func TestDotAsCatchAllWildcard(t *testing.T) {
6 | mux := NewServeMux()
7 | mux.Handle(".", HandlerFunc(HelloServer))
8 | mux.Handle("example.com.", HandlerFunc(AnotherHelloServer))
9 |
10 | handler := mux.match("www.miek.nl.", TypeTXT)
11 | if handler == nil {
12 | t.Error("wildcard match failed")
13 | }
14 |
15 | handler = mux.match("www.example.com.", TypeTXT)
16 | if handler == nil {
17 | t.Error("example.com match failed")
18 | }
19 |
20 | handler = mux.match("a.www.example.com.", TypeTXT)
21 | if handler == nil {
22 | t.Error("a.www.example.com match failed")
23 | }
24 |
25 | handler = mux.match("boe.", TypeTXT)
26 | if handler == nil {
27 | t.Error("boe. match failed")
28 | }
29 | }
30 |
31 | func TestCaseFolding(t *testing.T) {
32 | mux := NewServeMux()
33 | mux.Handle("_udp.example.com.", HandlerFunc(HelloServer))
34 |
35 | handler := mux.match("_dns._udp.example.com.", TypeSRV)
36 | if handler == nil {
37 | t.Error("case sensitive characters folded")
38 | }
39 |
40 | handler = mux.match("_DNS._UDP.EXAMPLE.COM.", TypeSRV)
41 | if handler == nil {
42 | t.Error("case insensitive characters not folded")
43 | }
44 | }
45 |
46 | func TestRootServer(t *testing.T) {
47 | mux := NewServeMux()
48 | mux.Handle(".", HandlerFunc(HelloServer))
49 |
50 | handler := mux.match(".", TypeNS)
51 | if handler == nil {
52 | t.Error("root match failed")
53 | }
54 | }
55 |
56 | func BenchmarkMuxMatch(b *testing.B) {
57 | mux := NewServeMux()
58 | mux.Handle("_udp.example.com.", HandlerFunc(HelloServer))
59 |
60 | bench := func(q string) func(*testing.B) {
61 | return func(b *testing.B) {
62 | for n := 0; n < b.N; n++ {
63 | handler := mux.match(q, TypeSRV)
64 | if handler == nil {
65 | b.Fatal("couldn't find match")
66 | }
67 | }
68 | }
69 | }
70 | b.Run("lowercase", bench("_dns._udp.example.com."))
71 | b.Run("uppercase", bench("_DNS._UDP.EXAMPLE.COM."))
72 | }
73 |
--------------------------------------------------------------------------------
/dns/sig0.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/ed25519"
7 | "crypto/rsa"
8 | "encoding/binary"
9 | "math/big"
10 | "strings"
11 | "time"
12 | )
13 |
14 | // Sign signs a dns.Msg. It fills the signature with the appropriate data.
15 | // The SIG record should have the SignerName, KeyTag, Algorithm, Inception
16 | // and Expiration set.
17 | func (rr *SIG) Sign(k crypto.Signer, m *Msg) ([]byte, error) {
18 | if k == nil {
19 | return nil, ErrPrivKey
20 | }
21 | if rr.KeyTag == 0 || rr.SignerName == "" || rr.Algorithm == 0 {
22 | return nil, ErrKey
23 | }
24 |
25 | rr.Hdr = RR_Header{Name: ".", Rrtype: TypeSIG, Class: ClassANY, Ttl: 0}
26 | rr.OrigTtl, rr.TypeCovered, rr.Labels = 0, 0, 0
27 |
28 | buf := make([]byte, m.Len()+Len(rr))
29 | mbuf, err := m.PackBuffer(buf)
30 | if err != nil {
31 | return nil, err
32 | }
33 | if &buf[0] != &mbuf[0] {
34 | return nil, ErrBuf
35 | }
36 | off, err := PackRR(rr, buf, len(mbuf), nil, false)
37 | if err != nil {
38 | return nil, err
39 | }
40 | buf = buf[:off:cap(buf)]
41 |
42 | h, cryptohash, err := hashFromAlgorithm(rr.Algorithm)
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | // Write SIG rdata
48 | h.Write(buf[len(mbuf)+1+2+2+4+2:])
49 | // Write message
50 | h.Write(buf[:len(mbuf)])
51 |
52 | signature, err := sign(k, h.Sum(nil), cryptohash, rr.Algorithm)
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | rr.Signature = toBase64(signature)
58 |
59 | buf = append(buf, signature...)
60 | if len(buf) > int(^uint16(0)) {
61 | return nil, ErrBuf
62 | }
63 | // Adjust sig data length
64 | rdoff := len(mbuf) + 1 + 2 + 2 + 4
65 | rdlen := binary.BigEndian.Uint16(buf[rdoff:])
66 | rdlen += uint16(len(signature))
67 | binary.BigEndian.PutUint16(buf[rdoff:], rdlen)
68 | // Adjust additional count
69 | adc := binary.BigEndian.Uint16(buf[10:])
70 | adc++
71 | binary.BigEndian.PutUint16(buf[10:], adc)
72 | return buf, nil
73 | }
74 |
75 | // Verify validates the message buf using the key k.
76 | // It's assumed that buf is a valid message from which rr was unpacked.
77 | func (rr *SIG) Verify(k *KEY, buf []byte) error {
78 | if k == nil {
79 | return ErrKey
80 | }
81 | if rr.KeyTag == 0 || rr.SignerName == "" || rr.Algorithm == 0 {
82 | return ErrKey
83 | }
84 |
85 | h, cryptohash, err := hashFromAlgorithm(rr.Algorithm)
86 | if err != nil {
87 | return err
88 | }
89 |
90 | buflen := len(buf)
91 | qdc := binary.BigEndian.Uint16(buf[4:])
92 | anc := binary.BigEndian.Uint16(buf[6:])
93 | auc := binary.BigEndian.Uint16(buf[8:])
94 | adc := binary.BigEndian.Uint16(buf[10:])
95 | offset := headerSize
96 | for i := uint16(0); i < qdc && offset < buflen; i++ {
97 | _, offset, err = UnpackDomainName(buf, offset)
98 | if err != nil {
99 | return err
100 | }
101 | // Skip past Type and Class
102 | offset += 2 + 2
103 | }
104 | for i := uint16(1); i < anc+auc+adc && offset < buflen; i++ {
105 | _, offset, err = UnpackDomainName(buf, offset)
106 | if err != nil {
107 | return err
108 | }
109 | // Skip past Type, Class and TTL
110 | offset += 2 + 2 + 4
111 | if offset+1 >= buflen {
112 | continue
113 | }
114 | rdlen := binary.BigEndian.Uint16(buf[offset:])
115 | offset += 2
116 | offset += int(rdlen)
117 | }
118 | if offset >= buflen {
119 | return &Error{err: "overflowing unpacking signed message"}
120 | }
121 |
122 | // offset should be just prior to SIG
123 | bodyend := offset
124 | // owner name SHOULD be root
125 | _, offset, err = UnpackDomainName(buf, offset)
126 | if err != nil {
127 | return err
128 | }
129 | // Skip Type, Class, TTL, RDLen
130 | offset += 2 + 2 + 4 + 2
131 | sigstart := offset
132 | // Skip Type Covered, Algorithm, Labels, Original TTL
133 | offset += 2 + 1 + 1 + 4
134 | if offset+4+4 >= buflen {
135 | return &Error{err: "overflow unpacking signed message"}
136 | }
137 | expire := binary.BigEndian.Uint32(buf[offset:])
138 | offset += 4
139 | incept := binary.BigEndian.Uint32(buf[offset:])
140 | offset += 4
141 | now := uint32(time.Now().Unix())
142 | if now < incept || now > expire {
143 | return ErrTime
144 | }
145 | // Skip key tag
146 | offset += 2
147 | var signername string
148 | signername, offset, err = UnpackDomainName(buf, offset)
149 | if err != nil {
150 | return err
151 | }
152 | // If key has come from the DNS name compression might
153 | // have mangled the case of the name
154 | if !strings.EqualFold(signername, k.Header().Name) {
155 | return &Error{err: "signer name doesn't match key name"}
156 | }
157 | sigend := offset
158 | h.Write(buf[sigstart:sigend])
159 | h.Write(buf[:10])
160 | h.Write([]byte{
161 | byte((adc - 1) << 8),
162 | byte(adc - 1),
163 | })
164 | h.Write(buf[12:bodyend])
165 |
166 | hashed := h.Sum(nil)
167 | sig := buf[sigend:]
168 | switch k.Algorithm {
169 | case RSASHA1, RSASHA256, RSASHA512:
170 | pk := k.publicKeyRSA()
171 | if pk != nil {
172 | return rsa.VerifyPKCS1v15(pk, cryptohash, hashed, sig)
173 | }
174 | case ECDSAP256SHA256, ECDSAP384SHA384:
175 | pk := k.publicKeyECDSA()
176 | r := new(big.Int).SetBytes(sig[:len(sig)/2])
177 | s := new(big.Int).SetBytes(sig[len(sig)/2:])
178 | if pk != nil {
179 | if ecdsa.Verify(pk, hashed, r, s) {
180 | return nil
181 | }
182 | return ErrSig
183 | }
184 | case ED25519:
185 | pk := k.publicKeyED25519()
186 | if pk != nil {
187 | if ed25519.Verify(pk, hashed, sig) {
188 | return nil
189 | }
190 | return ErrSig
191 | }
192 | }
193 | return ErrKeyAlg
194 | }
195 |
--------------------------------------------------------------------------------
/dns/sig0_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "crypto"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestSIG0(t *testing.T) {
10 | if testing.Short() {
11 | t.Skip("skipping test in short mode.")
12 | }
13 | m := new(Msg)
14 | m.SetQuestion("example.org.", TypeSOA)
15 | for _, alg := range []uint8{ECDSAP256SHA256, ECDSAP384SHA384, RSASHA1, RSASHA256, RSASHA512, ED25519} {
16 | algstr := AlgorithmToString[alg]
17 | keyrr := new(KEY)
18 | keyrr.Hdr.Name = algstr + "."
19 | keyrr.Hdr.Rrtype = TypeKEY
20 | keyrr.Hdr.Class = ClassINET
21 | keyrr.Algorithm = alg
22 | keysize := 512
23 | switch alg {
24 | case ECDSAP256SHA256, ED25519:
25 | keysize = 256
26 | case ECDSAP384SHA384:
27 | keysize = 384
28 | case RSASHA512:
29 | keysize = 1024
30 | }
31 | pk, err := keyrr.Generate(keysize)
32 | if err != nil {
33 | t.Errorf("failed to generate key for %q: %v", algstr, err)
34 | continue
35 | }
36 | now := uint32(time.Now().Unix())
37 | sigrr := new(SIG)
38 | sigrr.Hdr.Name = "."
39 | sigrr.Hdr.Rrtype = TypeSIG
40 | sigrr.Hdr.Class = ClassANY
41 | sigrr.Algorithm = alg
42 | sigrr.Expiration = now + 300
43 | sigrr.Inception = now - 300
44 | sigrr.KeyTag = keyrr.KeyTag()
45 | sigrr.SignerName = keyrr.Hdr.Name
46 | mb, err := sigrr.Sign(pk.(crypto.Signer), m)
47 | if err != nil {
48 | t.Errorf("failed to sign message using %q: %v", algstr, err)
49 | continue
50 | }
51 | m := new(Msg)
52 | if err := m.Unpack(mb); err != nil {
53 | t.Errorf("failed to unpack message signed using %q: %v", algstr, err)
54 | continue
55 | }
56 | if len(m.Extra) != 1 {
57 | t.Errorf("missing SIG for message signed using %q", algstr)
58 | continue
59 | }
60 | var sigrrwire *SIG
61 | switch rr := m.Extra[0].(type) {
62 | case *SIG:
63 | sigrrwire = rr
64 | default:
65 | t.Errorf("expected SIG RR, instead: %v", rr)
66 | continue
67 | }
68 | for _, rr := range []*SIG{sigrr, sigrrwire} {
69 | id := "sigrr"
70 | if rr == sigrrwire {
71 | id = "sigrrwire"
72 | }
73 | if err := rr.Verify(keyrr, mb); err != nil {
74 | t.Errorf("failed to verify %q signed SIG(%s): %v", algstr, id, err)
75 | continue
76 | }
77 | }
78 | mb[13]++
79 | if err := sigrr.Verify(keyrr, mb); err == nil {
80 | t.Errorf("verify succeeded on an altered message using %q", algstr)
81 | continue
82 | }
83 | sigrr.Expiration = 2
84 | sigrr.Inception = 1
85 | mb, _ = sigrr.Sign(pk.(crypto.Signer), m)
86 | if err := sigrr.Verify(keyrr, mb); err == nil {
87 | t.Errorf("verify succeeded on an expired message using %q", algstr)
88 | continue
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/dns/singleinflight.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // Adapted for dns package usage by Miek Gieben.
6 |
7 | package dns
8 |
9 | import "sync"
10 | import "time"
11 |
12 | // call is an in-flight or completed singleflight.Do call
13 | type call struct {
14 | wg sync.WaitGroup
15 | val *Msg
16 | rtt time.Duration
17 | err error
18 | dups int
19 | }
20 |
21 | // singleflight represents a class of work and forms a namespace in
22 | // which units of work can be executed with duplicate suppression.
23 | type singleflight struct {
24 | sync.Mutex // protects m
25 | m map[string]*call // lazily initialized
26 |
27 | dontDeleteForTesting bool // this is only to be used by TestConcurrentExchanges
28 | }
29 |
30 | // Do executes and returns the results of the given function, making
31 | // sure that only one execution is in-flight for a given key at a
32 | // time. If a duplicate comes in, the duplicate caller waits for the
33 | // original to complete and receives the same results.
34 | // The return value shared indicates whether v was given to multiple callers.
35 | func (g *singleflight) Do(key string, fn func() (*Msg, time.Duration, error)) (v *Msg, rtt time.Duration, err error, shared bool) {
36 | g.Lock()
37 | if g.m == nil {
38 | g.m = make(map[string]*call)
39 | }
40 | if c, ok := g.m[key]; ok {
41 | c.dups++
42 | g.Unlock()
43 | c.wg.Wait()
44 | return c.val, c.rtt, c.err, true
45 | }
46 | c := new(call)
47 | c.wg.Add(1)
48 | g.m[key] = c
49 | g.Unlock()
50 |
51 | c.val, c.rtt, c.err = fn()
52 | c.wg.Done()
53 |
54 | if !g.dontDeleteForTesting {
55 | g.Lock()
56 | delete(g.m, key)
57 | g.Unlock()
58 | }
59 |
60 | return c.val, c.rtt, c.err, c.dups > 0
61 | }
62 |
--------------------------------------------------------------------------------
/dns/smimea.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "crypto/sha256"
5 | "crypto/x509"
6 | "encoding/hex"
7 | )
8 |
9 | // Sign creates a SMIMEA record from an SSL certificate.
10 | func (r *SMIMEA) Sign(usage, selector, matchingType int, cert *x509.Certificate) (err error) {
11 | r.Hdr.Rrtype = TypeSMIMEA
12 | r.Usage = uint8(usage)
13 | r.Selector = uint8(selector)
14 | r.MatchingType = uint8(matchingType)
15 |
16 | r.Certificate, err = CertificateToDANE(r.Selector, r.MatchingType, cert)
17 | return err
18 | }
19 |
20 | // Verify verifies a SMIMEA record against an SSL certificate. If it is OK
21 | // a nil error is returned.
22 | func (r *SMIMEA) Verify(cert *x509.Certificate) error {
23 | c, err := CertificateToDANE(r.Selector, r.MatchingType, cert)
24 | if err != nil {
25 | return err // Not also ErrSig?
26 | }
27 | if r.Certificate == c {
28 | return nil
29 | }
30 | return ErrSig // ErrSig, really?
31 | }
32 |
33 | // SMIMEAName returns the ownername of a SMIMEA resource record as per the
34 | // format specified in RFC 'draft-ietf-dane-smime-12' Section 2 and 3
35 | func SMIMEAName(email, domain string) (string, error) {
36 | hasher := sha256.New()
37 | hasher.Write([]byte(email))
38 |
39 | // RFC Section 3: "The local-part is hashed using the SHA2-256
40 | // algorithm with the hash truncated to 28 octets and
41 | // represented in its hexadecimal representation to become the
42 | // left-most label in the prepared domain name"
43 | return hex.EncodeToString(hasher.Sum(nil)[:28]) + "." + "_smimecert." + domain, nil
44 | }
45 |
--------------------------------------------------------------------------------
/dns/svcb_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | // This tests everything valid about SVCB but parsing.
8 | // Parsing tests belong to parse_test.go.
9 | func TestSVCB(t *testing.T) {
10 | svcbs := []struct {
11 | key string
12 | data string
13 | }{
14 | {`mandatory`, `alpn,key65000`},
15 | {`alpn`, `h2,h2c`},
16 | {`port`, `499`},
17 | {`ipv4hint`, `3.4.3.2,1.1.1.1`},
18 | {`no-default-alpn`, ``},
19 | {`ipv6hint`, `1::4:4:4:4,1::3:3:3:3`},
20 | {`echconfig`, `YUdWc2JHOD0=`},
21 | {`key65000`, `4\ 3`},
22 | {`key65001`, `\"\ `},
23 | {`key65002`, ``},
24 | {`key65003`, `=\"\"`},
25 | {`key65004`, `\254\ \ \030\000`},
26 | }
27 |
28 | for _, o := range svcbs {
29 | keyCode := svcbStringToKey(o.key)
30 | kv := makeSVCBKeyValue(keyCode)
31 | if kv == nil {
32 | t.Error("failed to parse svc key: ", o.key)
33 | continue
34 | }
35 | if kv.Key() != keyCode {
36 | t.Error("key constant is not in sync: ", keyCode)
37 | continue
38 | }
39 | err := kv.parse(o.data)
40 | if err != nil {
41 | t.Error("failed to parse svc pair: ", o.key)
42 | continue
43 | }
44 | b, err := kv.pack()
45 | if err != nil {
46 | t.Error("failed to pack value of svc pair: ", o.key, err)
47 | continue
48 | }
49 | if len(b) != int(kv.len()) {
50 | t.Errorf("expected packed svc value %s to be of length %d but got %d", o.key, int(kv.len()), len(b))
51 | }
52 | err = kv.unpack(b)
53 | if err != nil {
54 | t.Error("failed to unpack value of svc pair: ", o.key, err)
55 | continue
56 | }
57 | if str := kv.String(); str != o.data {
58 | t.Errorf("`%s' should be equal to\n`%s', but is `%s'", o.key, o.data, str)
59 | }
60 | }
61 | }
62 |
63 | func TestDecodeBadSVCB(t *testing.T) {
64 | svcbs := []struct {
65 | key SVCBKey
66 | data []byte
67 | }{
68 | {
69 | key: SVCB_ALPN,
70 | data: []byte{3, 0, 0}, // There aren't three octets after 3
71 | },
72 | {
73 | key: SVCB_NO_DEFAULT_ALPN,
74 | data: []byte{0},
75 | },
76 | {
77 | key: SVCB_PORT,
78 | data: []byte{},
79 | },
80 | {
81 | key: SVCB_IPV4HINT,
82 | data: []byte{0, 0, 0},
83 | },
84 | {
85 | key: SVCB_IPV6HINT,
86 | data: []byte{0, 0, 0},
87 | },
88 | }
89 | for _, o := range svcbs {
90 | err := makeSVCBKeyValue(SVCBKey(o.key)).unpack(o.data)
91 | if err == nil {
92 | t.Error("accepted invalid svc value with key ", SVCBKey(o.key).String())
93 | }
94 | }
95 | }
96 |
97 | func TestCompareSVCB(t *testing.T) {
98 | val1 := []SVCBKeyValue{
99 | &SVCBPort{
100 | Port: 117,
101 | },
102 | &SVCBAlpn{
103 | Alpn: []string{"h2", "h3"},
104 | },
105 | }
106 | val2 := []SVCBKeyValue{
107 | &SVCBAlpn{
108 | Alpn: []string{"h2", "h3"},
109 | },
110 | &SVCBPort{
111 | Port: 117,
112 | },
113 | }
114 | if !areSVCBPairArraysEqual(val1, val2) {
115 | t.Error("svcb pairs were compared without sorting")
116 | }
117 | if val1[0].Key() != SVCB_PORT || val2[0].Key() != SVCB_ALPN {
118 | t.Error("original svcb pairs were reordered during comparison")
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/dns/tlsa.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "crypto/x509"
5 | "net"
6 | "strconv"
7 | )
8 |
9 | // Sign creates a TLSA record from an SSL certificate.
10 | func (r *TLSA) Sign(usage, selector, matchingType int, cert *x509.Certificate) (err error) {
11 | r.Hdr.Rrtype = TypeTLSA
12 | r.Usage = uint8(usage)
13 | r.Selector = uint8(selector)
14 | r.MatchingType = uint8(matchingType)
15 |
16 | r.Certificate, err = CertificateToDANE(r.Selector, r.MatchingType, cert)
17 | return err
18 | }
19 |
20 | // Verify verifies a TLSA record against an SSL certificate. If it is OK
21 | // a nil error is returned.
22 | func (r *TLSA) Verify(cert *x509.Certificate) error {
23 | c, err := CertificateToDANE(r.Selector, r.MatchingType, cert)
24 | if err != nil {
25 | return err // Not also ErrSig?
26 | }
27 | if r.Certificate == c {
28 | return nil
29 | }
30 | return ErrSig // ErrSig, really?
31 | }
32 |
33 | // TLSAName returns the ownername of a TLSA resource record as per the
34 | // rules specified in RFC 6698, Section 3.
35 | func TLSAName(name, service, network string) (string, error) {
36 | if !IsFqdn(name) {
37 | return "", ErrFqdn
38 | }
39 | p, err := net.LookupPort(network, service)
40 | if err != nil {
41 | return "", err
42 | }
43 | return "_" + strconv.Itoa(p) + "._" + network + "." + name, nil
44 | }
45 |
--------------------------------------------------------------------------------
/dns/tools.go:
--------------------------------------------------------------------------------
1 | // +build tools
2 |
3 | // We include our tool dependencies for `go generate` here to ensure they're
4 | // properly tracked by the go tool. See the Go Wiki for the rationale behind this:
5 | // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module.
6 |
7 | package dns
8 |
9 | import _ "golang.org/x/tools/go/packages"
10 |
--------------------------------------------------------------------------------
/dns/types_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestCmToM(t *testing.T) {
8 | s := cmToM(0, 0)
9 | if s != "0.00" {
10 | t.Error("0, 0")
11 | }
12 |
13 | s = cmToM(1, 0)
14 | if s != "0.01" {
15 | t.Error("1, 0")
16 | }
17 |
18 | s = cmToM(3, 1)
19 | if s != "0.30" {
20 | t.Error("3, 1")
21 | }
22 |
23 | s = cmToM(4, 2)
24 | if s != "4" {
25 | t.Error("4, 2")
26 | }
27 |
28 | s = cmToM(5, 3)
29 | if s != "50" {
30 | t.Error("5, 3")
31 | }
32 |
33 | s = cmToM(7, 5)
34 | if s != "7000" {
35 | t.Error("7, 5")
36 | }
37 |
38 | s = cmToM(9, 9)
39 | if s != "90000000" {
40 | t.Error("9, 9")
41 | }
42 | }
43 |
44 | func TestSplitN(t *testing.T) {
45 | xs := splitN("abc", 5)
46 | if len(xs) != 1 && xs[0] != "abc" {
47 | t.Errorf("failure to split abc")
48 | }
49 |
50 | s := ""
51 | for i := 0; i < 255; i++ {
52 | s += "a"
53 | }
54 |
55 | xs = splitN(s, 255)
56 | if len(xs) != 1 && xs[0] != s {
57 | t.Errorf("failure to split 255 char long string")
58 | }
59 |
60 | s += "b"
61 | xs = splitN(s, 255)
62 | if len(xs) != 2 || xs[1] != "b" {
63 | t.Errorf("failure to split 256 char long string: %d", len(xs))
64 | }
65 |
66 | // Make s longer
67 | for i := 0; i < 255; i++ {
68 | s += "a"
69 | }
70 | xs = splitN(s, 255)
71 | if len(xs) != 3 || xs[2] != "a" {
72 | t.Errorf("failure to split 510 char long string: %d", len(xs))
73 | }
74 | }
75 |
76 | func TestSprintName(t *testing.T) {
77 | tests := map[string]string{
78 | // Non-numeric escaping of special printable characters.
79 | " '@;()\"\\..example": `\ \'\@\;\(\)\"\..example`,
80 | "\\032\\039\\064\\059\\040\\041\\034\\046\\092.example": `\ \'\@\;\(\)\"\.\\.example`,
81 |
82 | // Numeric escaping of nonprintable characters.
83 | "\x00\x07\x09\x0a\x1f.\x7f\x80\xad\xef\xff": `\000\007\009\010\031.\127\128\173\239\255`,
84 | "\\000\\007\\009\\010\\031.\\127\\128\\173\\239\\255": `\000\007\009\010\031.\127\128\173\239\255`,
85 |
86 | // No escaping of other printable characters, at least after a prior escape.
87 | ";[a-zA-Z0-9_]+/*.~": `\;[a-zA-Z0-9_]+/*.~`,
88 | ";\\091\\097\\045\\122\\065\\045\\090\\048\\045\\057\\095\\093\\043\\047\\042.\\126": `\;[a-zA-Z0-9_]+/*.~`,
89 | // "\\091\\097\\045\\122\\065\\045\\090\\048\\045\\057\\095\\093\\043\\047\\042.\\126": `[a-zA-Z0-9_]+/*.~`,
90 |
91 | // Incomplete "dangling" escapes are dropped regardless of prior escaping.
92 | "a\\": `a`,
93 | ";\\": `\;`,
94 |
95 | // Escaped dots stay escaped regardless of prior escaping.
96 | "a\\.\\046.\\.\\046": `a\.\..\.\.`,
97 | "a\\046\\..\\046\\.": `a\.\..\.\.`,
98 | }
99 | for input, want := range tests {
100 | got := sprintName(input)
101 | if got != want {
102 | t.Errorf("input %q: expected %q, got %q", input, want, got)
103 | }
104 | }
105 | }
106 |
107 | func TestSprintTxtOctet(t *testing.T) {
108 | got := sprintTxtOctet("abc\\.def\007\"\127@\255\x05\xef\\")
109 |
110 | if want := "\"abc\\.def\\007\\\"W@\\173\\005\\239\""; got != want {
111 | t.Errorf("expected %q, got %q", want, got)
112 | }
113 | }
114 |
115 | func TestSprintTxt(t *testing.T) {
116 | got := sprintTxt([]string{
117 | "abc\\.def\007\"\127@\255\x05\xef\\",
118 | "example.com",
119 | })
120 |
121 | if want := "\"abc.def\\007\\\"W@\\173\\005\\239\" \"example.com\""; got != want {
122 | t.Errorf("expected %q, got %q", want, got)
123 | }
124 | }
125 |
126 | func TestRPStringer(t *testing.T) {
127 | rp := &RP{
128 | Hdr: RR_Header{
129 | Name: "test.example.com.",
130 | Rrtype: TypeRP,
131 | Class: ClassINET,
132 | Ttl: 600,
133 | },
134 | Mbox: "\x05first.example.com.",
135 | Txt: "second.\x07example.com.",
136 | }
137 |
138 | const expected = "test.example.com.\t600\tIN\tRP\t\\005first.example.com. second.\\007example.com."
139 | if rp.String() != expected {
140 | t.Errorf("expected %v, got %v", expected, rp)
141 | }
142 |
143 | _, err := NewRR(rp.String())
144 | if err != nil {
145 | t.Fatalf("error parsing %q: %v", rp, err)
146 | }
147 | }
148 |
149 | func BenchmarkSprintName(b *testing.B) {
150 | for n := 0; n < b.N; n++ {
151 | got := sprintName("abc\\.def\007\"\127@\255\x05\xef\\")
152 |
153 | if want := "abc\\.def\\007\\\"W\\@\\173\\005\\239"; got != want {
154 | b.Fatalf("expected %q, got %q", want, got)
155 | }
156 | }
157 | }
158 |
159 | func BenchmarkSprintName_NoEscape(b *testing.B) {
160 | for n := 0; n < b.N; n++ {
161 | got := sprintName("large.example.com")
162 |
163 | if want := "large.example.com"; got != want {
164 | b.Fatalf("expected %q, got %q", want, got)
165 | }
166 | }
167 | }
168 |
169 | func BenchmarkSprintTxtOctet(b *testing.B) {
170 | for n := 0; n < b.N; n++ {
171 | got := sprintTxtOctet("abc\\.def\007\"\127@\255\x05\xef\\")
172 |
173 | if want := "\"abc\\.def\\007\\\"W@\\173\\005\\239\""; got != want {
174 | b.Fatalf("expected %q, got %q", want, got)
175 | }
176 | }
177 | }
178 |
179 | func BenchmarkSprintTxt(b *testing.B) {
180 | txt := []string{
181 | "abc\\.def\007\"\127@\255\x05\xef\\",
182 | "example.com",
183 | }
184 |
185 | b.ResetTimer()
186 | for n := 0; n < b.N; n++ {
187 | got := sprintTxt(txt)
188 |
189 | if want := "\"abc.def\\007\\\"W@\\173\\005\\239\" \"example.com\""; got != want {
190 | b.Fatalf("expected %q, got %q", got, want)
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/dns/udp.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package dns
4 |
5 | import (
6 | "net"
7 |
8 | "golang.org/x/net/ipv4"
9 | "golang.org/x/net/ipv6"
10 | )
11 |
12 | // This is the required size of the OOB buffer to pass to ReadMsgUDP.
13 | var udpOOBSize = func() int {
14 | // We can't know whether we'll get an IPv4 control message or an
15 | // IPv6 control message ahead of time. To get around this, we size
16 | // the buffer equal to the largest of the two.
17 |
18 | oob4 := ipv4.NewControlMessage(ipv4.FlagDst | ipv4.FlagInterface)
19 | oob6 := ipv6.NewControlMessage(ipv6.FlagDst | ipv6.FlagInterface)
20 |
21 | if len(oob4) > len(oob6) {
22 | return len(oob4)
23 | }
24 |
25 | return len(oob6)
26 | }()
27 |
28 | // SessionUDP holds the remote address and the associated
29 | // out-of-band data.
30 | type SessionUDP struct {
31 | raddr *net.UDPAddr
32 | context []byte
33 | }
34 |
35 | // RemoteAddr returns the remote network address.
36 | func (s *SessionUDP) RemoteAddr() net.Addr { return s.raddr }
37 |
38 | // ReadFromSessionUDP acts just like net.UDPConn.ReadFrom(), but returns a session object instead of a
39 | // net.UDPAddr.
40 | func ReadFromSessionUDP(conn *net.UDPConn, b []byte) (int, *SessionUDP, error) {
41 | oob := make([]byte, udpOOBSize)
42 | n, oobn, _, raddr, err := conn.ReadMsgUDP(b, oob)
43 | if err != nil {
44 | return n, nil, err
45 | }
46 | return n, &SessionUDP{raddr, oob[:oobn]}, err
47 | }
48 |
49 | // WriteToSessionUDP acts just like net.UDPConn.WriteTo(), but uses a *SessionUDP instead of a net.Addr.
50 | func WriteToSessionUDP(conn *net.UDPConn, b []byte, session *SessionUDP) (int, error) {
51 | oob := correctSource(session.context)
52 | n, _, err := conn.WriteMsgUDP(b, oob, session.raddr)
53 | return n, err
54 | }
55 |
56 | func setUDPSocketOptions(conn *net.UDPConn) error {
57 | // Try setting the flags for both families and ignore the errors unless they
58 | // both error.
59 | err6 := ipv6.NewPacketConn(conn).SetControlMessage(ipv6.FlagDst|ipv6.FlagInterface, true)
60 | err4 := ipv4.NewPacketConn(conn).SetControlMessage(ipv4.FlagDst|ipv4.FlagInterface, true)
61 | if err6 != nil && err4 != nil {
62 | return err4
63 | }
64 | return nil
65 | }
66 |
67 | // parseDstFromOOB takes oob data and returns the destination IP.
68 | func parseDstFromOOB(oob []byte) net.IP {
69 | // Start with IPv6 and then fallback to IPv4
70 | // TODO(fastest963): Figure out a way to prefer one or the other. Looking at
71 | // the lvl of the header for a 0 or 41 isn't cross-platform.
72 | cm6 := new(ipv6.ControlMessage)
73 | if cm6.Parse(oob) == nil && cm6.Dst != nil {
74 | return cm6.Dst
75 | }
76 | cm4 := new(ipv4.ControlMessage)
77 | if cm4.Parse(oob) == nil && cm4.Dst != nil {
78 | return cm4.Dst
79 | }
80 | return nil
81 | }
82 |
83 | // correctSource takes oob data and returns new oob data with the Src equal to the Dst
84 | func correctSource(oob []byte) []byte {
85 | dst := parseDstFromOOB(oob)
86 | if dst == nil {
87 | return nil
88 | }
89 | // If the dst is definitely an IPv6, then use ipv6's ControlMessage to
90 | // respond otherwise use ipv4's because ipv6's marshal ignores ipv4
91 | // addresses.
92 | if dst.To4() == nil {
93 | cm := new(ipv6.ControlMessage)
94 | cm.Src = dst
95 | oob = cm.Marshal()
96 | } else {
97 | cm := new(ipv4.ControlMessage)
98 | cm.Src = dst
99 | oob = cm.Marshal()
100 | }
101 | return oob
102 | }
103 |
--------------------------------------------------------------------------------
/dns/udp_test.go:
--------------------------------------------------------------------------------
1 | // +build linux,!appengine
2 |
3 | package dns
4 |
5 | import (
6 | "bytes"
7 | "net"
8 | "runtime"
9 | "strings"
10 | "testing"
11 | "time"
12 |
13 | "golang.org/x/net/ipv4"
14 | "golang.org/x/net/ipv6"
15 | )
16 |
17 | func TestSetUDPSocketOptions(t *testing.T) {
18 | // returns an error if we cannot resolve that address
19 | testFamily := func(n, addr string) error {
20 | a, err := net.ResolveUDPAddr(n, addr)
21 | if err != nil {
22 | return err
23 | }
24 | c, err := net.ListenUDP(n, a)
25 | if err != nil {
26 | return err
27 | }
28 | if err := setUDPSocketOptions(c); err != nil {
29 | t.Fatalf("failed to set socket options: %v", err)
30 | }
31 | ch := make(chan *SessionUDP)
32 | go func() {
33 | // Set some deadline so this goroutine doesn't hang forever
34 | c.SetReadDeadline(time.Now().Add(time.Minute))
35 | b := make([]byte, 1)
36 | _, sess, err := ReadFromSessionUDP(c, b)
37 | if err != nil {
38 | t.Errorf("failed to read from conn: %v", err)
39 | // fallthrough to chan send below
40 | }
41 | ch <- sess
42 | }()
43 |
44 | c2, err := net.Dial("udp", c.LocalAddr().String())
45 | if err != nil {
46 | t.Fatalf("failed to dial udp: %v", err)
47 | }
48 | if _, err := c2.Write([]byte{1}); err != nil {
49 | t.Fatalf("failed to write to conn: %v", err)
50 | }
51 | sess := <-ch
52 | if sess == nil {
53 | // t.Error was already called in the goroutine above.
54 | t.FailNow()
55 | }
56 | if len(sess.context) == 0 {
57 | t.Fatalf("empty session context: %v", sess)
58 | }
59 | ip := parseDstFromOOB(sess.context)
60 | if ip == nil {
61 | t.Fatalf("failed to parse dst: %v", sess)
62 | }
63 | if !strings.Contains(c.LocalAddr().String(), ip.String()) {
64 | t.Fatalf("dst was different than listen addr: %v != %v", ip.String(), c.LocalAddr().String())
65 | }
66 | return nil
67 | }
68 |
69 | // we require that ipv4 be supported
70 | if err := testFamily("udp4", "127.0.0.1:0"); err != nil {
71 | t.Fatalf("failed to test socket options on IPv4: %v", err)
72 | }
73 | // IPv6 might not be supported so these will just log
74 | if err := testFamily("udp6", "[::1]:0"); err != nil {
75 | t.Logf("failed to test socket options on IPv6-only: %v", err)
76 | }
77 | if err := testFamily("udp", "[::1]:0"); err != nil {
78 | t.Logf("failed to test socket options on IPv6/IPv4: %v", err)
79 | }
80 | }
81 |
82 | func TestParseDstFromOOB(t *testing.T) {
83 | if runtime.GOARCH != "amd64" {
84 | // The cmsghdr struct differs in the width (32/64-bit) of
85 | // lengths and the struct padding between architectures.
86 | // The data below was only written with amd64 in mind, and
87 | // thus the test must be skipped on other architectures.
88 | t.Skip("skipping test on unsupported architecture")
89 | }
90 |
91 | // dst is :ffff:100.100.100.100
92 | oob := []byte{36, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 100, 100, 100, 100, 2, 0, 0, 0}
93 | dst := parseDstFromOOB(oob)
94 | dst4 := dst.To4()
95 | if dst4 == nil {
96 | t.Errorf("failed to parse IPv4 in IPv6: %v", dst)
97 | } else if dst4.String() != "100.100.100.100" {
98 | t.Errorf("unexpected IPv4: %v", dst4)
99 | }
100 |
101 | // dst is 2001:db8::1
102 | oob = []byte{36, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 50, 0, 0, 0, 32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0}
103 | dst = parseDstFromOOB(oob)
104 | dst6 := dst.To16()
105 | if dst6 == nil {
106 | t.Errorf("failed to parse IPv6: %v", dst)
107 | } else if dst6.String() != "2001:db8::1" {
108 | t.Errorf("unexpected IPv6: %v", dst4)
109 | }
110 |
111 | // dst is 100.100.100.100 but was received on 10.10.10.10
112 | oob = []byte{28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 10, 10, 10, 10, 100, 100, 100, 100, 0, 0, 0, 0}
113 | dst = parseDstFromOOB(oob)
114 | dst4 = dst.To4()
115 | if dst4 == nil {
116 | t.Errorf("failed to parse IPv4: %v", dst)
117 | } else if dst4.String() != "100.100.100.100" {
118 | t.Errorf("unexpected IPv4: %v", dst4)
119 | }
120 | }
121 |
122 | func TestCorrectSource(t *testing.T) {
123 | if runtime.GOARCH != "amd64" {
124 | // See comment above in TestParseDstFromOOB.
125 | t.Skip("skipping test on unsupported architecture")
126 | }
127 |
128 | // dst is :ffff:100.100.100.100 which should be counted as IPv4
129 | oob := []byte{36, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 100, 100, 100, 100, 2, 0, 0, 0}
130 | soob := correctSource(oob)
131 | cm4 := new(ipv4.ControlMessage)
132 | cm4.Src = net.ParseIP("100.100.100.100")
133 | if !bytes.Equal(soob, cm4.Marshal()) {
134 | t.Errorf("unexpected oob for ipv4 address: %v", soob)
135 | }
136 |
137 | // dst is 2001:db8::1
138 | oob = []byte{36, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 50, 0, 0, 0, 32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0}
139 | soob = correctSource(oob)
140 | cm6 := new(ipv6.ControlMessage)
141 | cm6.Src = net.ParseIP("2001:db8::1")
142 | if !bytes.Equal(soob, cm6.Marshal()) {
143 | t.Errorf("unexpected oob for IPv6 address: %v", soob)
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/dns/udp_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package dns
4 |
5 | import "net"
6 |
7 | // SessionUDP holds the remote address
8 | type SessionUDP struct {
9 | raddr *net.UDPAddr
10 | }
11 |
12 | // RemoteAddr returns the remote network address.
13 | func (s *SessionUDP) RemoteAddr() net.Addr { return s.raddr }
14 |
15 | // ReadFromSessionUDP acts just like net.UDPConn.ReadFrom(), but returns a session object instead of a
16 | // net.UDPAddr.
17 | // TODO(fastest963): Once go1.10 is released, use ReadMsgUDP.
18 | func ReadFromSessionUDP(conn *net.UDPConn, b []byte) (int, *SessionUDP, error) {
19 | n, raddr, err := conn.ReadFrom(b)
20 | if err != nil {
21 | return n, nil, err
22 | }
23 | return n, &SessionUDP{raddr.(*net.UDPAddr)}, err
24 | }
25 |
26 | // WriteToSessionUDP acts just like net.UDPConn.WriteTo(), but uses a *SessionUDP instead of a net.Addr.
27 | // TODO(fastest963): Once go1.10 is released, use WriteMsgUDP.
28 | func WriteToSessionUDP(conn *net.UDPConn, b []byte, session *SessionUDP) (int, error) {
29 | return conn.WriteTo(b, session.raddr)
30 | }
31 |
32 | // TODO(fastest963): Once go1.10 is released and we can use *MsgUDP methods
33 | // use the standard method in udp.go for these.
34 | func setUDPSocketOptions(*net.UDPConn) error { return nil }
35 | func parseDstFromOOB([]byte, net.IP) net.IP { return nil }
36 |
--------------------------------------------------------------------------------
/dns/update.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | // NameUsed sets the RRs in the prereq section to
4 | // "Name is in use" RRs. RFC 2136 section 2.4.4.
5 | func (u *Msg) NameUsed(rr []RR) {
6 | if u.Answer == nil {
7 | u.Answer = make([]RR, 0, len(rr))
8 | }
9 | for _, r := range rr {
10 | u.Answer = append(u.Answer, &ANY{Hdr: RR_Header{Name: r.Header().Name, Ttl: 0, Rrtype: TypeANY, Class: ClassANY}})
11 | }
12 | }
13 |
14 | // NameNotUsed sets the RRs in the prereq section to
15 | // "Name is in not use" RRs. RFC 2136 section 2.4.5.
16 | func (u *Msg) NameNotUsed(rr []RR) {
17 | if u.Answer == nil {
18 | u.Answer = make([]RR, 0, len(rr))
19 | }
20 | for _, r := range rr {
21 | u.Answer = append(u.Answer, &ANY{Hdr: RR_Header{Name: r.Header().Name, Ttl: 0, Rrtype: TypeANY, Class: ClassNONE}})
22 | }
23 | }
24 |
25 | // Used sets the RRs in the prereq section to
26 | // "RRset exists (value dependent -- with rdata)" RRs. RFC 2136 section 2.4.2.
27 | func (u *Msg) Used(rr []RR) {
28 | if len(u.Question) == 0 {
29 | panic("dns: empty question section")
30 | }
31 | if u.Answer == nil {
32 | u.Answer = make([]RR, 0, len(rr))
33 | }
34 | for _, r := range rr {
35 | hdr := r.Header()
36 | hdr.Class = u.Question[0].Qclass
37 | hdr.Ttl = 0
38 | u.Answer = append(u.Answer, r)
39 | }
40 | }
41 |
42 | // RRsetUsed sets the RRs in the prereq section to
43 | // "RRset exists (value independent -- no rdata)" RRs. RFC 2136 section 2.4.1.
44 | func (u *Msg) RRsetUsed(rr []RR) {
45 | if u.Answer == nil {
46 | u.Answer = make([]RR, 0, len(rr))
47 | }
48 | for _, r := range rr {
49 | h := r.Header()
50 | u.Answer = append(u.Answer, &ANY{Hdr: RR_Header{Name: h.Name, Ttl: 0, Rrtype: h.Rrtype, Class: ClassANY}})
51 | }
52 | }
53 |
54 | // RRsetNotUsed sets the RRs in the prereq section to
55 | // "RRset does not exist" RRs. RFC 2136 section 2.4.3.
56 | func (u *Msg) RRsetNotUsed(rr []RR) {
57 | if u.Answer == nil {
58 | u.Answer = make([]RR, 0, len(rr))
59 | }
60 | for _, r := range rr {
61 | h := r.Header()
62 | u.Answer = append(u.Answer, &ANY{Hdr: RR_Header{Name: h.Name, Ttl: 0, Rrtype: h.Rrtype, Class: ClassNONE}})
63 | }
64 | }
65 |
66 | // Insert creates a dynamic update packet that adds an complete RRset, see RFC 2136 section 2.5.1.
67 | func (u *Msg) Insert(rr []RR) {
68 | if len(u.Question) == 0 {
69 | panic("dns: empty question section")
70 | }
71 | if u.Ns == nil {
72 | u.Ns = make([]RR, 0, len(rr))
73 | }
74 | for _, r := range rr {
75 | r.Header().Class = u.Question[0].Qclass
76 | u.Ns = append(u.Ns, r)
77 | }
78 | }
79 |
80 | // RemoveRRset creates a dynamic update packet that deletes an RRset, see RFC 2136 section 2.5.2.
81 | func (u *Msg) RemoveRRset(rr []RR) {
82 | if u.Ns == nil {
83 | u.Ns = make([]RR, 0, len(rr))
84 | }
85 | for _, r := range rr {
86 | h := r.Header()
87 | u.Ns = append(u.Ns, &ANY{Hdr: RR_Header{Name: h.Name, Ttl: 0, Rrtype: h.Rrtype, Class: ClassANY}})
88 | }
89 | }
90 |
91 | // RemoveName creates a dynamic update packet that deletes all RRsets of a name, see RFC 2136 section 2.5.3
92 | func (u *Msg) RemoveName(rr []RR) {
93 | if u.Ns == nil {
94 | u.Ns = make([]RR, 0, len(rr))
95 | }
96 | for _, r := range rr {
97 | u.Ns = append(u.Ns, &ANY{Hdr: RR_Header{Name: r.Header().Name, Ttl: 0, Rrtype: TypeANY, Class: ClassANY}})
98 | }
99 | }
100 |
101 | // Remove creates a dynamic update packet deletes RR from a RRSset, see RFC 2136 section 2.5.4
102 | func (u *Msg) Remove(rr []RR) {
103 | if u.Ns == nil {
104 | u.Ns = make([]RR, 0, len(rr))
105 | }
106 | for _, r := range rr {
107 | h := r.Header()
108 | h.Class = ClassNONE
109 | h.Ttl = 0
110 | u.Ns = append(u.Ns, r)
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/dns/update_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestDynamicUpdateParsing(t *testing.T) {
9 | const prefix = "example.com. IN "
10 |
11 | for typ, name := range TypeToString {
12 | switch typ {
13 | case TypeNone, TypeReserved:
14 | continue
15 | case TypeANY:
16 | // ANY is ambiguous here and ends up parsed as a CLASS.
17 | //
18 | // TODO(tmthrgd): Using TYPE255 here doesn't seem to work and also
19 | // seems to fail for some other record types. Investigate.
20 | continue
21 | }
22 |
23 | s := prefix + name
24 | if _, err := NewRR(s); err != nil {
25 | t.Errorf("failure to parse: %s: %v", s, err)
26 | }
27 |
28 | s += " \\# 0"
29 | if _, err := NewRR(s); err != nil {
30 | t.Errorf("failure to parse: %s: %v", s, err)
31 | }
32 | }
33 | }
34 |
35 | func TestDynamicUpdateUnpack(t *testing.T) {
36 | // From https://github.com/miekg/dns/issues/150#issuecomment-62296803
37 | // It should be an update message for the zone "example.",
38 | // deleting the A RRset "example." and then adding an A record at "example.".
39 | // class ANY, TYPE A
40 | buf := []byte{171, 68, 40, 0, 0, 1, 0, 0, 0, 2, 0, 0, 7, 101, 120, 97, 109, 112, 108, 101, 0, 0, 6, 0, 1, 192, 12, 0, 1, 0, 255, 0, 0, 0, 0, 0, 0, 192, 12, 0, 1, 0, 1, 0, 0, 0, 0, 0, 4, 127, 0, 0, 1}
41 | msg := new(Msg)
42 | err := msg.Unpack(buf)
43 | if err != nil {
44 | t.Errorf("failed to unpack: %v\n%s", err, msg.String())
45 | }
46 | }
47 |
48 | func TestDynamicUpdateZeroRdataUnpack(t *testing.T) {
49 | m := new(Msg)
50 | rr := &RR_Header{Name: ".", Rrtype: 0, Class: 1, Ttl: ^uint32(0), Rdlength: 0}
51 | m.Answer = []RR{rr, rr, rr, rr, rr}
52 | m.Ns = m.Answer
53 | for n, s := range TypeToString {
54 | rr.Rrtype = n
55 | bytes, err := m.Pack()
56 | if err != nil {
57 | t.Errorf("failed to pack %s: %v", s, err)
58 | continue
59 | }
60 | if err := new(Msg).Unpack(bytes); err != nil {
61 | t.Errorf("failed to unpack %s: %v", s, err)
62 | }
63 | }
64 | }
65 |
66 | func TestRemoveRRset(t *testing.T) {
67 | // Should add a zero data RR in Class ANY with a TTL of 0
68 | // for each set mentioned in the RRs provided to it.
69 | rr := testRR(". 100 IN A 127.0.0.1")
70 | m := new(Msg)
71 | m.Ns = []RR{&RR_Header{Name: ".", Rrtype: TypeA, Class: ClassANY, Ttl: 0, Rdlength: 0}}
72 | expectstr := m.String()
73 | expect, err := m.Pack()
74 | if err != nil {
75 | t.Fatalf("error packing expected msg: %v", err)
76 | }
77 |
78 | m.Ns = nil
79 | m.RemoveRRset([]RR{rr})
80 | actual, err := m.Pack()
81 | if err != nil {
82 | t.Fatalf("error packing actual msg: %v", err)
83 | }
84 | if !bytes.Equal(actual, expect) {
85 | tmp := new(Msg)
86 | if err := tmp.Unpack(actual); err != nil {
87 | t.Fatalf("error unpacking actual msg: %v\nexpected: %v\ngot: %v\n", err, expect, actual)
88 | }
89 | t.Errorf("expected msg:\n%s", expectstr)
90 | t.Errorf("actual msg:\n%v", tmp)
91 | }
92 | }
93 |
94 | func TestPreReqAndRemovals(t *testing.T) {
95 | // Build a list of multiple prereqs and then somes removes followed by an insert.
96 | // We should be able to add multiple prereqs and updates.
97 | m := new(Msg)
98 | m.SetUpdate("example.org.")
99 | m.Id = 1234
100 |
101 | // Use a full set of RRs each time, so we are sure the rdata is stripped.
102 | rrName1 := testRR("name_used. 3600 IN A 127.0.0.1")
103 | rrName2 := testRR("name_not_used. 3600 IN A 127.0.0.1")
104 | rrRemove1 := testRR("remove1. 3600 IN A 127.0.0.1")
105 | rrRemove2 := testRR("remove2. 3600 IN A 127.0.0.1")
106 | rrRemove3 := testRR("remove3. 3600 IN A 127.0.0.1")
107 | rrInsert := testRR("insert. 3600 IN A 127.0.0.1")
108 | rrRrset1 := testRR("rrset_used1. 3600 IN A 127.0.0.1")
109 | rrRrset2 := testRR("rrset_used2. 3600 IN A 127.0.0.1")
110 | rrRrset3 := testRR("rrset_not_used. 3600 IN A 127.0.0.1")
111 |
112 | // Handle the prereqs.
113 | m.NameUsed([]RR{rrName1})
114 | m.NameNotUsed([]RR{rrName2})
115 | m.RRsetUsed([]RR{rrRrset1})
116 | m.Used([]RR{rrRrset2})
117 | m.RRsetNotUsed([]RR{rrRrset3})
118 |
119 | // and now the updates.
120 | m.RemoveName([]RR{rrRemove1})
121 | m.RemoveRRset([]RR{rrRemove2})
122 | m.Remove([]RR{rrRemove3})
123 | m.Insert([]RR{rrInsert})
124 |
125 | // This test function isn't a Example function because we print these RR with tabs at the
126 | // end and the Example function trim these, thus they never match.
127 | // TODO(miek): don't print these tabs and make this into an Example function.
128 | expect := `;; opcode: UPDATE, status: NOERROR, id: 1234
129 | ;; flags:; QUERY: 1, ANSWER: 5, AUTHORITY: 4, ADDITIONAL: 0
130 |
131 | ;; QUESTION SECTION:
132 | ;example.org. IN SOA
133 |
134 | ;; ANSWER SECTION:
135 | name_used. 0 CLASS255 ANY
136 | name_not_used. 0 NONE ANY
137 | rrset_used1. 0 CLASS255 A
138 | rrset_used2. 0 IN A 127.0.0.1
139 | rrset_not_used. 0 NONE A
140 |
141 | ;; AUTHORITY SECTION:
142 | remove1. 0 CLASS255 ANY
143 | remove2. 0 CLASS255 A
144 | remove3. 0 NONE A 127.0.0.1
145 | insert. 3600 IN A 127.0.0.1
146 | `
147 |
148 | if m.String() != expect {
149 | t.Errorf("expected msg:\n%s", expect)
150 | t.Errorf("actual msg:\n%v", m.String())
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/dns/version.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "fmt"
4 |
5 | // Version is current version of this library.
6 | var Version = v{1, 1, 47}
7 |
8 | // v holds the version of this library.
9 | type v struct {
10 | Major, Minor, Patch int
11 | }
12 |
13 | func (v v) String() string {
14 | return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
15 | }
16 |
--------------------------------------------------------------------------------
/dns/version_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import "testing"
4 |
5 | func TestVersion(t *testing.T) {
6 | v := v{1, 0, 0}
7 | if x := v.String(); x != "1.0.0" {
8 | t.Fatalf("Failed to convert version %v, got: %s", v, x)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/dns/xfr_test.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 | )
8 |
9 | var (
10 | tsigSecret = map[string]string{"axfr.": "so6ZGir4GPAqINNh9U5c3A=="}
11 | xfrSoa = testRR(`miek.nl. 0 IN SOA linode.atoom.net. miek.miek.nl. 2009032802 21600 7200 604800 3600`)
12 | xfrA = testRR(`x.miek.nl. 1792 IN A 10.0.0.1`)
13 | xfrMX = testRR(`miek.nl. 1800 IN MX 1 x.miek.nl.`)
14 | xfrTestData = []RR{xfrSoa, xfrA, xfrMX, xfrSoa}
15 | )
16 |
17 | func InvalidXfrServer(ctx context.Context, w ResponseWriter, req *Msg) {
18 | ch := make(chan *Envelope)
19 | tr := new(Transfer)
20 |
21 | go tr.Out(w, req, ch)
22 | ch <- &Envelope{RR: []RR{}}
23 | close(ch)
24 | w.Hijack()
25 | }
26 |
27 | func SingleEnvelopeXfrServer(ctx context.Context, w ResponseWriter, req *Msg) {
28 | ch := make(chan *Envelope)
29 | tr := new(Transfer)
30 |
31 | go tr.Out(w, req, ch)
32 | ch <- &Envelope{RR: xfrTestData}
33 | close(ch)
34 | w.Hijack()
35 | }
36 |
37 | func MultipleEnvelopeXfrServer(ctx context.Context, w ResponseWriter, req *Msg) {
38 | ch := make(chan *Envelope)
39 | tr := new(Transfer)
40 |
41 | go tr.Out(w, req, ch)
42 |
43 | for _, rr := range xfrTestData {
44 | ch <- &Envelope{RR: []RR{rr}}
45 | }
46 | close(ch)
47 | w.Hijack()
48 | }
49 |
50 | func TestInvalidXfr(t *testing.T) {
51 | HandleFunc("miek.nl.", InvalidXfrServer)
52 | defer HandleRemove("miek.nl.")
53 |
54 | s, addrstr, _, err := RunLocalTCPServer(":0")
55 | if err != nil {
56 | t.Fatalf("unable to run test server: %s", err)
57 | }
58 | defer s.Shutdown()
59 |
60 | tr := new(Transfer)
61 | m := new(Msg)
62 | m.SetAxfr("miek.nl.")
63 |
64 | c, err := tr.In(m, addrstr)
65 | if err != nil {
66 | t.Fatal("failed to zone transfer in", err)
67 | }
68 |
69 | for msg := range c {
70 | if msg.Error == nil {
71 | t.Fatal("failed to catch 'no SOA' error")
72 | }
73 | }
74 | }
75 |
76 | func TestSingleEnvelopeXfr(t *testing.T) {
77 | HandleFunc("miek.nl.", SingleEnvelopeXfrServer)
78 | defer HandleRemove("miek.nl.")
79 |
80 | s, addrstr, _, err := RunLocalTCPServer(":0", func(srv *Server) {
81 | srv.TsigSecret = tsigSecret
82 | })
83 | if err != nil {
84 | t.Fatalf("unable to run test server: %s", err)
85 | }
86 | defer s.Shutdown()
87 |
88 | axfrTestingSuite(t, addrstr)
89 | }
90 |
91 | func TestMultiEnvelopeXfr(t *testing.T) {
92 | HandleFunc("miek.nl.", MultipleEnvelopeXfrServer)
93 | defer HandleRemove("miek.nl.")
94 |
95 | s, addrstr, _, err := RunLocalTCPServer(":0", func(srv *Server) {
96 | srv.TsigSecret = tsigSecret
97 | })
98 | if err != nil {
99 | t.Fatalf("unable to run test server: %s", err)
100 | }
101 | defer s.Shutdown()
102 |
103 | axfrTestingSuite(t, addrstr)
104 | }
105 |
106 | func axfrTestingSuite(t *testing.T, addrstr string) {
107 | tr := new(Transfer)
108 | m := new(Msg)
109 | m.SetAxfr("miek.nl.")
110 |
111 | c, err := tr.In(m, addrstr)
112 | if err != nil {
113 | t.Fatal("failed to zone transfer in", err)
114 | }
115 |
116 | var records []RR
117 | for msg := range c {
118 | if msg.Error != nil {
119 | t.Fatal(msg.Error)
120 | }
121 | records = append(records, msg.RR...)
122 | }
123 |
124 | if len(records) != len(xfrTestData) {
125 | t.Fatalf("bad axfr: expected %v, got %v", records, xfrTestData)
126 | }
127 |
128 | for i, rr := range records {
129 | if !IsDuplicate(rr, xfrTestData[i]) {
130 | t.Fatalf("bad axfr: expected %v, got %v", records, xfrTestData)
131 | }
132 | }
133 | }
134 |
135 | func axfrTestingSuiteWithCustomTsig(t *testing.T, addrstr string, provider TsigProvider) {
136 | tr := new(Transfer)
137 | m := new(Msg)
138 | var err error
139 | tr.Conn, err = Dial("tcp", addrstr)
140 | if err != nil {
141 | t.Fatal("failed to dial", err)
142 | }
143 | tr.TsigProvider = provider
144 | m.SetAxfr("miek.nl.")
145 | m.SetTsig("axfr.", HmacSHA256, 300, time.Now().Unix())
146 |
147 | c, err := tr.In(m, addrstr)
148 | if err != nil {
149 | t.Fatal("failed to zone transfer in", err)
150 | }
151 |
152 | var records []RR
153 | for msg := range c {
154 | if msg.Error != nil {
155 | t.Fatal(msg.Error)
156 | }
157 | records = append(records, msg.RR...)
158 | }
159 |
160 | if len(records) != len(xfrTestData) {
161 | t.Fatalf("bad axfr: expected %v, got %v", records, xfrTestData)
162 | }
163 |
164 | for i, rr := range records {
165 | if !IsDuplicate(rr, xfrTestData[i]) {
166 | t.Errorf("bad axfr: expected %v, got %v", records, xfrTestData)
167 | }
168 | }
169 | }
170 |
171 | func TestCustomTsigProvider(t *testing.T) {
172 | HandleFunc("miek.nl.", SingleEnvelopeXfrServer)
173 | defer HandleRemove("miek.nl.")
174 |
175 | s, addrstr, _, err := RunLocalTCPServer(":0", func(srv *Server) {
176 | srv.TsigProvider = tsigSecretProvider(tsigSecret)
177 | })
178 | if err != nil {
179 | t.Fatalf("unable to run test server: %s", err)
180 | }
181 | defer s.Shutdown()
182 |
183 | axfrTestingSuiteWithCustomTsig(t, addrstr, tsigSecretProvider(tsigSecret))
184 | }
185 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/fusion/kittendns
2 |
3 | go 1.18
4 |
5 | replace github.com/miekg/dns => ./dns
6 |
7 | require (
8 | github.com/antonmedv/expr v1.9.0
9 | github.com/davecgh/go-spew v1.1.1
10 | github.com/dop251/goja v0.0.0-20220516123900-4418d4575a41
11 | github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d
12 | github.com/fsnotify/fsnotify v1.5.4
13 | github.com/hydronica/toml v0.5.0
14 | github.com/miekg/dns v1.1.47
15 | github.com/stretchr/testify v1.5.1
16 | )
17 |
18 | require (
19 | github.com/BurntSushi/toml v1.2.1 // indirect
20 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
21 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
22 | github.com/google/go-cmp v0.5.9 // indirect
23 | github.com/pmezard/go-difflib v1.0.0 // indirect
24 | github.com/sergi/go-diff v1.1.0 // indirect
25 | golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect
26 | golang.org/x/mod v0.14.0 // indirect
27 | golang.org/x/net v0.16.0 // indirect
28 | golang.org/x/sync v0.4.0 // indirect
29 | golang.org/x/sys v0.14.0 // indirect
30 | golang.org/x/telemetry v0.0.0-20231114163143-69313e640400 // indirect
31 | golang.org/x/text v0.13.0 // indirect
32 | golang.org/x/tools v0.14.1-0.20231114185516-c9d3e7de13fd // indirect
33 | golang.org/x/tools/gopls v0.14.2 // indirect
34 | golang.org/x/vuln v1.0.1 // indirect
35 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
36 | gopkg.in/yaml.v2 v2.4.0 // indirect
37 | honnef.co/go/tools v0.4.5 // indirect
38 | mvdan.cc/gofumpt v0.4.0 // indirect
39 | mvdan.cc/xurls/v2 v2.4.0 // indirect
40 | )
41 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os/exec"
6 | "regexp"
7 | "strings"
8 | "testing"
9 | )
10 |
11 | func TestStartOfAuthority(t *testing.T) {
12 | result := runit("example.com", "SOA")
13 | if !lookup(result, `(?s)ANSWER SECTION:+example.com. 14400 IN SOA dns1.example.com. dev.zteo.com. 1 86400 7200 100800 7200`) {
14 | inform(t, `a complete SOA record for example.com`, result)
15 | }
16 | }
17 |
18 | func TestMailXchange(t *testing.T) {
19 | result := runit("example.com", "MX")
20 | if !lookup(result, `(?s)ANSWER SECTION:+example.com. 20 IN MX 0 one.example.com.+example.com. 20 IN MX 0 two.example.com.`) {
21 | inform(t, `two mailers for example.com`, result)
22 | }
23 | }
24 |
25 | // RFC7505
26 | func TestNullMailXchange(t *testing.T) {
27 | result := runit("example.org", "MX")
28 | if !lookup(result, `(?s)ANSWER SECTION:+example.org. 20 IN MX 0 .`) {
29 | inform(t, `null mailer for example.org`, result)
30 | }
31 | }
32 |
33 | func TestCanonicalNameRecordAsIPv4(t *testing.T) {
34 | result := runit("www.example.com", "A")
35 | if !lookup(result, `(?s)ANSWER SECTION:+www.example.com. 20 IN CNAME example.com.+example.com. 20 IN A 1.2.3.4`) {
36 | inform(t, `two steps CNAME resolution for example.com`, result)
37 | }
38 | }
39 |
40 | func TestCanonicalNameRecordExplicitly(t *testing.T) {
41 | result := runit("www.example.com", "CNAME")
42 | if !lookup(result, `(?s)ANSWER SECTION:+www.example.com. 20 IN CNAME example.com.`) {
43 | inform(t, `single steps explicit CNAME resolution for example.com`, result)
44 | }
45 | }
46 |
47 | func TestCanonicalNameRecordImplicitly(t *testing.T) {
48 | result := runit("www.example.com")
49 | if !lookup(result, `(?s)ANSWER SECTION:+www.example.com. 20 IN CNAME example.com.+example.com. 20 IN A 1.2.3.4`) {
50 | inform(t, `two steps implicit CNAME resolution for example.com`, result)
51 | }
52 | }
53 |
54 | func TestService(t *testing.T) {
55 | result := runit("_sip._tcp.example.com", "SRV")
56 | if !lookup(result, `(?s)ANSWER SECTION:+_sip._tcp.example.com. 20 IN SRV 10 5 0 test.example.com.`) {
57 | inform(t, `a service record for SIP over TCP at example.com`, result)
58 | }
59 | }
60 |
61 | func TestMultiA(t *testing.T) {
62 | // If we are running the jsscript plugin, by default we will have modified our TTL.
63 | // Detect plugin
64 | result := runit("magic.example.com", "TXT")
65 | if !lookup(result, `(?s)ANSWER SECTION:+magic.example.com. 60 IN TXT`) {
66 | // No plugin detected
67 | result = runit("test.example.com", "A")
68 | if !lookup(result, `(?s)ANSWER SECTION:+test.example.com. 20 IN A 192.168.1.2+test.example.com. 20 IN A 192.168.2.2+test.example.com. 20 IN A 192.168.3.2`) {
69 | inform(t, `three A records for test.example.com`, result)
70 | }
71 | } else {
72 | // Plugin detected
73 | result = runit("test.example.com", "A")
74 | if !lookup(result, `(?s)ANSWER SECTION:+test.example.com. 3600 IN A 192.168.1.2+test.example.com. 3600 IN A 192.168.2.2+test.example.com. 3600 IN A 192.168.3.2`) {
75 | inform(t, `three A records for test.example.com (with jsscript plugin)`, result)
76 | }
77 | }
78 | }
79 |
80 | func TestAuthoritative(t *testing.T) {
81 | result := runit("test.example.com", "A")
82 | if !lookup(result, `(?s)AUTHORITY SECTION:+example.com. 14400 IN SOA dns1.example.com. dev.zteo.com. 1 86400 7200 100800 7200`) {
83 | inform(t, `authoritative assertion for test.example.com`, result)
84 | }
85 | }
86 |
87 | func runit(args ...string) string {
88 | stdout, err := exec.Command("dig", append([]string{"@localhost"}, args...)...).Output()
89 | if err != nil {
90 | log.Fatal(err)
91 | }
92 | return string(stdout)
93 | }
94 |
95 | func lookup(source string, str string) bool {
96 | r := regexp.MustCompile(ex(str))
97 | return r.FindString(source) != ""
98 | }
99 |
100 | func ex(raw string) string {
101 | return strings.ReplaceAll(
102 | strings.ReplaceAll(
103 | strings.ReplaceAll(raw,
104 | ".", "\\."),
105 | "+", ".+?"),
106 | " ", "\\s+?")
107 | }
108 |
109 | func inform(t *testing.T, expected string, msg string) {
110 | t.Errorf("Expected %s, got (full output) %s", expected, msg)
111 | }
112 |
--------------------------------------------------------------------------------
/plugins/Makefile:
--------------------------------------------------------------------------------
1 | include plugins/example/Makefile
2 | include plugins/jsscript/Makefile
3 |
--------------------------------------------------------------------------------
/plugins/config.toml.template:
--------------------------------------------------------------------------------
1 | [settings]
2 | debuglevel = 1
3 | autoreload = true
4 | listen = 53
5 | # Cache recursive queries.
6 | cache = true
7 | # Flatten CNAME chains down to A records. Not fully functional yet.
8 | flatten = false
9 | # Will return a single record, round-robin, when multiple records are available.
10 | loadbalance = true
11 |
12 | # A parent DNS to recurse non authoritative queries to
13 | [settings.parent]
14 | address = "192.168.1.254"
15 |
16 | # A few rules. Use a natural language engine similar to the one I included
17 | # in https://github.com/fusion/mailbiter
18 |
19 | [[rule]]
20 | condition = "remoteip != '192.168.1.19' and host startsWith 'google.'"
21 | action = "rewrite '142.250.189.14'"
22 |
23 | [[rule]]
24 | condition = "remoteip != '192.168.1.19'"
25 | action = "drop"
26 |
27 | [[rule]]
28 | condition = "not (remoteip startsWith '192.168.1')"
29 | action = "inspect"
30 |
31 | # Plugins (you can chain them... but be careful)
32 |
33 | [[plugin]]
34 | enabled = true
35 | path = "bin/jsscript.so"
36 | prehandler = "JsScriptPreHandler"
37 | posthandler = "JsScriptPostHandler"
38 | arguments = ["plugins/jsscript/example.js"]
39 | monitor = ["plugins/jsscript/example.js"]
40 |
41 | # Zone definitions
42 |
43 | [[zone]]
44 | origin = "example.com."
45 | TTL = 60
46 |
47 | # SOA information
48 | [zone.auth]
49 | ns = "dns1.example.com"
50 | email = "chris.example.com"
51 | serial = 1
52 |
53 | # Top-level record
54 | [[zone.record]]
55 | host = "@"
56 | ipv4 = "192.168.1.1"
57 |
58 | # An A record, with multiple replies
59 | # (can be load balanced, either in server or in client)
60 | [[zone.record]]
61 | host = "test"
62 | ipv4 = "192.168.1.2"
63 | ipv4s = ["192.168.2.2", "192.168.3.2"]
64 |
65 | # An SRV record
66 | [[zone.record]]
67 | Service = "sip"
68 | Proto = "tcp"
69 | Priority = 10
70 | Weight = 5
71 | Target = "test"
72 |
73 | # A CNAME record
74 | [[zone.record]]
75 | host = "bogus2"
76 | aliased = "test"
--------------------------------------------------------------------------------
/plugins/example/Makefile:
--------------------------------------------------------------------------------
1 | plugin_example: dist/$(PLUGIN_OS)$(PLUGIN_ARCH)/example.so
2 |
3 | dist/$(PLUGIN_OS)$(PLUGIN_ARCH)/example.so: plugins/example/example.go
4 | GOOS=$(PLUGIN_OS) GOARCH=$(PLUGIN_ARCH) CGO_ENABLED=1 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -buildmode=plugin -o $@ $^
5 |
--------------------------------------------------------------------------------
/plugins/example/example.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/fusion/kittendns/plugins"
7 | "github.com/miekg/dns"
8 | )
9 |
10 | /*
11 | * Here is what this example plugin does. Note that's is a very contrived set of rules.
12 | * First, if checks whether it is being invoked during the "pre" phase (i.e. before actually processing a query),
13 | * or, conversely, in the "post" phase (i.e. after processing a query and creating an answer).
14 | * If in the "pre" phase, and we are requesting a specific TXT record, it builds one and replies with it.
15 | * It also asks kittendns to not process the query any further ("Done"), but other plugins may decide otherwise.
16 | * If querying "plugintest" it simply rewrites the query to "test.example.com." so that this is what kittendns
17 | * will answer. Other "pre" plugins will not be run ("stop")
18 | * During the "post" phase, if we previously rewrote the query, we will add ("reply") an additional entry.
19 | * If we did not rewrite the query, and "test.example.com." was thus the original query, we will instead
20 | * override ("rewrite") the answer with a longer TTL.
21 | */
22 |
23 | type exampleHandler struct {
24 | iRewroteSomething bool
25 | }
26 |
27 | var (
28 | instance *exampleHandler
29 | )
30 |
31 | func main() {} // Keeping toolchain happy
32 |
33 | func ExamplePreHandler(arguments []string) plugins.PreHandler {
34 | if instance == nil {
35 | instance = &exampleHandler{}
36 | }
37 | return instance
38 | }
39 |
40 | func ExamplePostHandler(arguments []string) plugins.PostHandler {
41 | if instance == nil {
42 | instance = &exampleHandler{}
43 | }
44 | return instance
45 | }
46 |
47 | func (h *exampleHandler) ProcessQuery(p plugins.PreOrPost, ip string, m *dns.Msg, q *dns.Question) (*plugins.Update, error) {
48 | if p == plugins.Pre {
49 | h.iRewroteSomething = false
50 | if q.Qtype == dns.TypeTXT && q.Name == "magic.example.com." {
51 | srv := new(dns.TXT)
52 | srv.Hdr = dns.RR_Header{
53 | Name: q.Name,
54 | Rrtype: dns.TypeTXT,
55 | Class: dns.ClassINET,
56 | Ttl: 60,
57 | Rdlength: 0}
58 | srv.Txt = []string{"this is a magic record"}
59 | return &plugins.Update{
60 | Action: plugins.Reply,
61 | Done: true,
62 | RR: []dns.RR{srv}},
63 | nil
64 | }
65 | if q.Qtype == dns.TypeA && q.Name == "plugintest.example.com." {
66 | h.iRewroteSomething = true
67 | newQuestion := q
68 | newQuestion.Name = "test.example.com."
69 | return &plugins.Update{
70 | Action: plugins.Question,
71 | Stop: true,
72 | Question: newQuestion},
73 | nil
74 | }
75 | } else {
76 | if h.iRewroteSomething {
77 | if q.Qtype == dns.TypeA && q.Name == "test.example.com." {
78 | rr, _ := dns.NewRR(
79 | fmt.Sprintf(
80 | "%s %d %s %s",
81 | q.Name,
82 | 60,
83 | "A",
84 | "5.6.7.8"))
85 | return &plugins.Update{
86 | Action: plugins.Reply,
87 | RR: []dns.RR{rr}},
88 | nil
89 | }
90 | } else {
91 | if q.Qtype == dns.TypeA && q.Name == "test.example.com." {
92 | newAnswers := []dns.RR{}
93 | for _, rr := range m.Answer {
94 | rr.Header().Ttl = 3600
95 | newAnswers = append(newAnswers, rr)
96 | }
97 | return &plugins.Update{
98 | Action: plugins.Rewrite,
99 | RR: newAnswers},
100 | nil
101 | }
102 | }
103 | }
104 | return nil, nil
105 | }
106 |
--------------------------------------------------------------------------------
/plugins/handler.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | import (
4 | "github.com/miekg/dns"
5 | )
6 |
7 | type Action uint64
8 |
9 | const (
10 | Noop Action = iota
11 | Question
12 | Reply
13 | Rewrite
14 | Deny
15 | )
16 |
17 | type PreOrPost uint64
18 |
19 | const (
20 | Pre PreOrPost = iota
21 | Post
22 | )
23 |
24 | type PreHandler interface {
25 | ProcessQuery(p PreOrPost, ip string, m *dns.Msg, q *dns.Question) (*Update, error)
26 | }
27 |
28 | type PreParam struct {
29 | RemoteIp string
30 | QueryType string
31 | Question string
32 | }
33 |
34 | type PostHandler interface {
35 | ProcessQuery(p PreOrPost, ip string, m *dns.Msg, q *dns.Question) (*Update, error)
36 | }
37 |
38 | type PostParam struct {
39 | RemoteIp string
40 | QueryType string
41 | Question string
42 | }
43 |
44 | type Update struct {
45 | Action Action
46 | Stop bool // Stop processing plugins
47 | Done bool // Stop processing question
48 | Question *dns.Question
49 | RR []dns.RR
50 | }
51 |
--------------------------------------------------------------------------------
/plugins/jsscript/Makefile:
--------------------------------------------------------------------------------
1 | plugin_jsscript: dist/$(PLUGIN_OS)$(PLUGIN_ARCH)/jsscript.so
2 |
3 | dist/$(PLUGIN_OS)$(PLUGIN_ARCH)/jsscript.so: plugins/jsscript/jsscript.go
4 | GOOS=$(PLUGIN_OS) GOARCH=$(PLUGIN_ARCH) CGO_ENABLED=1 go build ${TRIM_FLAGS} -ldflags "${BUILD_VARS}" -buildmode=plugin -o $@ $^
5 |
--------------------------------------------------------------------------------
/plugins/jsscript/example.js:
--------------------------------------------------------------------------------
1 | function main(preOrPost, ip, answers, type, name) {
2 | if (preOrPost == pre)
3 | return prefn(ip, type, name);
4 | return postfn(answers, type, name);
5 | }
6 |
7 | function prefn(ip, type, name) {
8 | // Pulling a TXT record from the magician's hat.
9 | if (type == typeTXT && name == "magic.example.com.") {
10 | return {"action": Reply, "type": type, "TTL": 60, "RR": [{"target": "this is a magic record"}], "Done": true};
11 | }
12 | // You asked for 'plugintest' but I am going to pretend it was 'test'
13 | if (type == typeA && name == "plugintest.example.com.") {
14 | return {"action": Question, "question": {"type": type, "name": "test.example.com."}};
15 | }
16 | return toy_dns_fun(ip, type, name);
17 | }
18 |
19 | function postfn(answers, type, name) {
20 | // Rewrite TTL to be 3600 seconds. That is all.
21 | if (type == typeA && name == "test.example.com.") {
22 | newAnswers = [];
23 | for (i = 0; i < answers.length; i++) {
24 | newAnswers.push({"type": type, "host": answers[i].Header().Name, "ip": answers[i].A.String()});
25 | }
26 | return {"action": Rewrite, "type": type, "TTL": 3600, "RR": newAnswers};
27 | }
28 | // Alternatively, we could have added a new value using Reply:
29 | // return {"action": Reply, "type": type, "TTL": 3600, "RR": [{"type": type, "host": name, "ip": "5.6.7.8"}]};
30 | return {}
31 | }
32 |
33 | function toy_dns_fun(ip, type, name) {
34 | // Replace with "false" to play with toy dns features
35 | if (false)
36 | return {}
37 | var m = require("./plugins/jsscript/extended_example.js");
38 | var s = require("./plugins/jsscript/secrets.js");
39 | if (type == typeA && name == "nine.example.com.") {
40 | console.log("Plugin information: Querying nine responder");
41 | var res = m.nineResponder();
42 | return {"action": Reply, "type": type, "TTL": 60, "RR": [{"host": "nine.example.com", "ip": res}], "Stop": true};
43 | }
44 | if (type == typeTXT && name == "whereami.example.com.") {
45 | return m.geoResponder(ip, s);
46 | }
47 | if (name.endsWith(".time.example.com.")) {
48 | var bits = name.split(".", 2);
49 | return m.timeResponder(bits[0]);
50 | }
51 | return {}
52 | }
53 |
--------------------------------------------------------------------------------
/plugins/jsscript/extended_example.js:
--------------------------------------------------------------------------------
1 | function nineResponder() {
2 | return "9.9.9.9";
3 | }
4 |
5 | // Thank you WorldTimeAPI! (http://worldtimeapi.org/)
6 | function timeResponder(locale) {
7 | /* Maybe at some point I'll play with Promises,
8 | considering I'm not entirely clear whether
9 | I can run this in a goroutine without consequences.
10 | */
11 | var response = null;
12 | fetch.get("http://worldtimeapi.org/api/timezone/" + locale,
13 | function(r) {
14 | if (r.StatusCode == 200) {
15 | var json = JSON.parse(r.Body);
16 | response = {"action": Reply, "type": typeTXT, "TTL": 60, "RR": [{"target": json['utc_datetime']}], "Done": true};
17 | }
18 | },
19 | function(e) {
20 | console.error("Failed: " + e);
21 | }
22 | );
23 | if (response == null)
24 | return {"action": Reply, "type": typeTXT, "TTL": 60, "RR": [{"target": "Unknown!"}], "Done": true};
25 | return response;
26 | }
27 |
28 | function geoResponder(ip, s) {
29 | console.log("Using the Javascript plugin to get-locate ip: " + ip);
30 | var response = null;
31 | fetch.get("https://api.ipgeolocation.io/ipgeo?apiKey=" + s.geoapikey() + "&ip=" + ip,
32 | function(r) {
33 | if (r.StatusCode == 200) {
34 | var json = JSON.parse(r.Body);
35 | response = {"action": Reply, "type": typeTXT, "TTL": 60, "RR": [{"target": json['city']}], "Done": true};
36 | }
37 | },
38 | function(e) {
39 | console.error("Failed: " + e);
40 | }
41 | );
42 | if (response == null)
43 | return {"action": Reply, "type": typeTXT, "TTL": 60, "RR": [{"target": "Unknown!"}], "Done": true};
44 | return response;
45 | }
46 |
47 | module.exports = {
48 | nineResponder: nineResponder,
49 | timeResponder: timeResponder,
50 | geoResponder: geoResponder
51 | }
52 |
--------------------------------------------------------------------------------
/plugins/jsscript/fetch/fetcher.go:
--------------------------------------------------------------------------------
1 | package fetch
2 |
3 | import (
4 | "io/ioutil"
5 | "net/http"
6 |
7 | "github.com/dop251/goja"
8 | "github.com/dop251/goja_nodejs/require"
9 | _ "github.com/dop251/goja_nodejs/util"
10 | )
11 |
12 | type Fetch struct {
13 | runtime *goja.Runtime
14 | util *goja.Object
15 | }
16 |
17 | type Response struct {
18 | StatusCode int
19 | Body string
20 | }
21 |
22 | func (f *Fetch) getter(v goja.Value, thenAction goja.Callable, rejectAction goja.Callable) {
23 | res, err := http.Get(v.String())
24 | if err != nil {
25 | rejectAction(goja.Undefined(), f.runtime.ToValue("Error: "+err.Error()))
26 | return
27 | }
28 | body, err := ioutil.ReadAll(res.Body)
29 | if err != nil {
30 | rejectAction(goja.Undefined(), f.runtime.ToValue("Error: "+err.Error()))
31 | return
32 | }
33 | thenAction(goja.Undefined(), f.runtime.ToValue(Response{StatusCode: res.StatusCode, Body: string(body)}))
34 | }
35 |
36 | func Require(runtime *goja.Runtime, module *goja.Object) {
37 | func(runtime *goja.Runtime, module *goja.Object) {
38 | f := &Fetch{
39 | runtime: runtime,
40 | }
41 | f.util = require.Require(runtime, "util").(*goja.Object)
42 | o := module.Get("exports").(*goja.Object)
43 | o.Set("get", f.getter)
44 | }(runtime, module)
45 | }
46 |
47 | func Enable(runtime *goja.Runtime) {
48 | runtime.Set("fetch", require.Require(runtime, "fetch"))
49 | }
50 |
51 | func init() {
52 | require.RegisterNativeModule("fetch", Require)
53 | }
54 |
--------------------------------------------------------------------------------
/plugins/jsscript/secrets.js.template:
--------------------------------------------------------------------------------
1 | function geoapikey() {
2 | return "";
3 | }
4 |
5 | module.exports = {
6 | geoapikey: geoapikey
7 | }
8 |
--------------------------------------------------------------------------------
/plugins/plugins.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "plugin"
7 |
8 | "github.com/fusion/kittendns/config"
9 | )
10 |
11 | type Plugins struct {
12 | PreHandler []PreHandler
13 | PostHandler []PostHandler
14 | }
15 |
16 | func Load(cfg *config.Config) *Plugins {
17 | plugins := &Plugins{}
18 |
19 | for _, pluginDef := range cfg.Plugin {
20 | if !pluginDef.Enabled {
21 | continue
22 | }
23 | plug, err := plugin.Open(pluginDef.Path)
24 | if err != nil {
25 | log.Fatal(fmt.Sprintf("Unable to load specified helper plugin: %s", err))
26 | }
27 | if pluginDef.PreHandler != "" {
28 | nph, err := plug.Lookup(pluginDef.PreHandler)
29 | if err != nil {
30 | log.Fatal(fmt.Sprintf("Unable to find pre handler ('%s') in loaded helper plugin", pluginDef.PreHandler))
31 | }
32 | initFunc, ok := nph.(func([]string) PreHandler)
33 | if !ok {
34 | log.Fatal("Loaded helper plugin lacks a proper pre handler function")
35 | }
36 | preHandler := initFunc(pluginDef.Arguments)
37 | plugins.PreHandler = append(plugins.PreHandler, preHandler)
38 | log.Println("Loaded pre handler:", pluginDef.PreHandler)
39 | }
40 | if pluginDef.PostHandler != "" {
41 | nph, err := plug.Lookup(pluginDef.PostHandler)
42 | if err != nil {
43 | log.Fatal(fmt.Sprintf("Unable to find post handler ('%s') in loaded helper plugin", pluginDef.PostHandler))
44 | }
45 | initFunc, ok := nph.(func([]string) PostHandler)
46 | if !ok {
47 | log.Fatal("Loaded helper plugin lacks a proper post handler function")
48 | }
49 | postHandler := initFunc(pluginDef.Arguments)
50 | plugins.PostHandler = append(plugins.PostHandler, postHandler)
51 | log.Println("Loaded post handler:", pluginDef.PostHandler)
52 | }
53 | if pluginDef.Monitor != nil {
54 | for _, fileToMonitor := range pluginDef.Monitor {
55 | cfg.Monitor = append(cfg.Monitor, fileToMonitor)
56 | }
57 | }
58 | }
59 | return plugins
60 | }
61 |
--------------------------------------------------------------------------------
/secret.toml.template:
--------------------------------------------------------------------------------
1 | key = "keyname."
2 | signature = "secret"
3 |
--------------------------------------------------------------------------------
/secret/secret.go:
--------------------------------------------------------------------------------
1 | package secret
2 |
3 | type Secret struct {
4 | Key string
5 | Signature string
6 | }
7 |
--------------------------------------------------------------------------------
/version/const.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | // Version is the tag of the latest release, it gets changed by the ci
4 | // process when a release is happening
5 | const Version = "v0.0.5" // x-release-please-version
6 |
--------------------------------------------------------------------------------
/version/vcs.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "fmt"
5 | "runtime/debug"
6 | )
7 |
8 | type gitInfo struct {
9 | BuildTime string
10 | Commit string
11 | Dirty bool
12 | }
13 |
14 | func GetVersion() string {
15 |
16 | name := "KittenDNS"
17 |
18 | info, ok := debug.ReadBuildInfo()
19 |
20 | if !ok {
21 | return fmt.Sprintf(`
22 | %s
23 | Non-release build`,
24 | name)
25 | }
26 |
27 | gitInfo := vcsInfo(info.Settings)
28 |
29 | // If a release, use the tag
30 | if !gitInfo.Dirty {
31 | return fmt.Sprintf(`%s %s
32 | Build time: %s
33 | Commit: %s`, name, Version, gitInfo.BuildTime, gitInfo.Commit)
34 | }
35 |
36 | return fmt.Sprintf(`%s
37 | Non-release build based on tag %s
38 | Build time: %s
39 | Commit: %s`, name, Version, gitInfo.BuildTime, gitInfo.Commit)
40 |
41 | }
42 |
43 | func vcsInfo(settings []debug.BuildSetting) *gitInfo {
44 | info := new(gitInfo)
45 |
46 | info.BuildTime = "unknown"
47 | info.Commit = "unknown"
48 | info.Dirty = false
49 |
50 | for _, v := range settings {
51 | switch v.Key {
52 | case "vcs.revision":
53 | info.Commit = v.Value
54 | case "vcs.modified":
55 | info.Dirty = v.Value == "true"
56 | case "vcs.time":
57 | info.BuildTime = v.Value
58 | }
59 | }
60 |
61 | return info
62 | }
63 |
--------------------------------------------------------------------------------