├── .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 |
2 | 3 | 4 | 5 | 16 | 17 |
6 | 7 |

8 | 9 | 📖 Documentation 10 | • 11 | Releases 🏷️ 12 | 13 |

14 | 15 |
18 | 19 |
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 | ![](assets/jmeter-kittendns.png) 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 |
Logo created by DesignEvo logo maker
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 | [![Build Status](https://travis-ci.org/miekg/dns.svg?branch=master)](https://travis-ci.org/miekg/dns) 2 | [![Code Coverage](https://img.shields.io/codecov/c/github/miekg/dns/master.svg)](https://codecov.io/github/miekg/dns?branch=master) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/miekg/dns)](https://goreportcard.com/report/miekg/dns) 4 | [![](https://godoc.org/github.com/miekg/dns?status.svg)](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 | --------------------------------------------------------------------------------