├── .github └── workflows │ └── test.yml ├── LICENSE.md ├── Makefile ├── README.md ├── event.go ├── example └── main.go ├── go.mod ├── go.sum ├── proxy.go ├── query.go ├── record.go ├── record_test.go ├── resolver.go ├── resolver_test.go ├── run.go ├── server.go ├── server_test.go ├── set.go ├── set_test.go ├── tools.go ├── tools_test.go ├── type.go ├── utils_test.go ├── zone.go └── zone_test.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v4 9 | - name: Install 10 | uses: actions/setup-go@v4 11 | with: 12 | go-version: 1.22 13 | - name: Test 14 | run: go test ./... 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Joël Gähwiler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: fmt vet lint 2 | 3 | fmt: 4 | go fmt . 5 | 6 | vet: 7 | go vet . 8 | 9 | lint: 10 | golint . 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # newdns 2 | 3 | [![Test](https://github.com/256dpi/newdns/actions/workflows/test.yml/badge.svg)](https://github.com/256dpi/newdns/actions/workflows/test.yml) 4 | [![GoDoc](https://godoc.org/github.com/256dpi/newdns?status.svg)](http://godoc.org/github.com/256dpi/newdns) 5 | [![Release](https://img.shields.io/github/release/256dpi/newdns.svg)](https://github.com/256dpi/newdns/releases) 6 | 7 | **A library for building custom DNS servers in Go.** 8 | 9 | The newdns library wraps the widely used, but low-level [github.com/miekg/dns](https://github.com/miekg/dns) package with a simple interface to quickly build custom DNS servers. The implemented server only supports a subset of record types (A, AAAA, CNAME, MX, TXT) and is intended to be used as a leaf authoritative name server only. It supports UDP and TCP as transport protocols and implements EDNS0. Conformance is tested by issuing a corpus of tests against a zone in AWS Route53 and comparing the response and behavior. 10 | 11 | The intention of this project is not to build a feature-complete alternative to "managed zone" offerings by major cloud platforms. However, some projects may require frequent synchronization of many records between a custom database and a cloud-hosted "managed zone". In this scenario, a custom DNS server that queries the own database might be a lot simpler to manage and operate. Also, the distributed nature of the DNS system offers interesting qualities that could be leveraged by future applications. 12 | 13 | ## Example 14 | 15 | ```go 16 | // create zone 17 | zone := &newdns.Zone{ 18 | Name: "example.com.", 19 | MasterNameServer: "ns1.hostmaster.com.", 20 | AllNameServers: []string{ 21 | "ns1.hostmaster.com.", 22 | "ns2.hostmaster.com.", 23 | "ns3.hostmaster.com.", 24 | }, 25 | Handler: func(name string) ([]newdns.Set, error) { 26 | // return apex records 27 | if name == "" { 28 | return []newdns.Set{ 29 | { 30 | Name: "example.com.", 31 | Type: newdns.A, 32 | Records: []newdns.Record{ 33 | {Address: "1.2.3.4"}, 34 | }, 35 | }, 36 | { 37 | Name: "example.com.", 38 | Type: newdns.AAAA, 39 | Records: []newdns.Record{ 40 | {Address: "1:2:3:4::"}, 41 | }, 42 | }, 43 | }, nil 44 | } 45 | 46 | // return sub records 47 | if name == "foo" { 48 | return []newdns.Set{ 49 | { 50 | Name: "foo.example.com.", 51 | Type: newdns.CNAME, 52 | Records: []newdns.Record{ 53 | {Address: "bar.example.com."}, 54 | }, 55 | }, 56 | }, nil 57 | } 58 | 59 | return nil, nil 60 | }, 61 | } 62 | 63 | // create server 64 | server := newdns.NewServer(newdns.Config{ 65 | Handler: func(name string) (*newdns.Zone, error) { 66 | // check name 67 | if newdns.InZone("example.com.", name) { 68 | return zone, nil 69 | } 70 | 71 | return nil, nil 72 | }, 73 | Logger: func(e newdns.Event, msg *dns.Msg, err error, reason string) { 74 | fmt.Println(e, err, reason) 75 | }, 76 | }) 77 | 78 | // run server 79 | go func() { 80 | err := server.Run(":1337") 81 | if err != nil { 82 | panic(err) 83 | } 84 | }() 85 | 86 | // print info 87 | fmt.Println("Query apex: dig example.com @0.0.0.0 -p 1337") 88 | fmt.Println("Query other: dig foo.example.com @0.0.0.0 -p 1337") 89 | 90 | // wait forever 91 | select {} 92 | ``` 93 | 94 | ## Credits 95 | 96 | - https://github.com/miekg/dns 97 | - https://github.com/coredns/coredns 98 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import "github.com/miekg/dns" 4 | 5 | // Event denotes an event type emitted to the logger. 6 | type Event int 7 | 8 | const ( 9 | // Ignored are requests that haven been dropped by leaving the connection 10 | // hanging to mitigate attacks. Inspect the reason for more information. 11 | Ignored Event = iota 12 | 13 | // Request is emitted for every accepted request. For every request event 14 | // a finish event fill follow. You can inspect the message to see the 15 | // complete request sent by the client. 16 | Request Event = iota 17 | 18 | // Refused are requests that received an error due to some incompatibility. 19 | // Inspect the reason for more information. 20 | Refused Event = iota 21 | 22 | // BackendError is emitted with errors returned by the callback and 23 | // validation functions. Inspect the error for more information. 24 | BackendError Event = iota 25 | 26 | // NetworkError is emitted with errors returned by the connection. Inspect 27 | // the error for more information. 28 | NetworkError Event = iota 29 | 30 | // Response is emitted with the final response to the client. You can inspect 31 | // the message to see the complete response to the client. 32 | Response Event = iota 33 | 34 | // Finish is emitted when a request has been processed. 35 | Finish Event = iota 36 | 37 | // ProxyRequest is emitted with every request forwarded to the fallback 38 | // DNS server. 39 | ProxyRequest Event = iota 40 | 41 | // ProxyResponse is emitted with ever response received from the fallback 42 | // DNS server. 43 | ProxyResponse Event = iota 44 | 45 | // ProxyError is emitted with errors returned by the fallback DNS server. 46 | // Inspect the error for more information. 47 | ProxyError Event = iota 48 | ) 49 | 50 | // String will return the name of the event. 51 | func (e Event) String() string { 52 | switch e { 53 | case Ignored: 54 | return "Ignored" 55 | case Request: 56 | return "Request" 57 | case Refused: 58 | return "Refused" 59 | case BackendError: 60 | return "BackendError" 61 | case NetworkError: 62 | return "NetworkError" 63 | case Response: 64 | return "Response" 65 | case Finish: 66 | return "Finish" 67 | case ProxyRequest: 68 | return "ProxyRequest" 69 | case ProxyResponse: 70 | return "ProxyResponse" 71 | case ProxyError: 72 | return "ProxyError" 73 | default: 74 | return "Unknown" 75 | } 76 | } 77 | 78 | // Logger is function that accepts logging events. 79 | type Logger func(e Event, msg *dns.Msg, err error, reason string) 80 | 81 | func log(l Logger, e Event, msg *dns.Msg, err error, reason string) { 82 | if l != nil { 83 | l(e, msg, err, reason) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/miekg/dns" 7 | 8 | "github.com/256dpi/newdns" 9 | ) 10 | 11 | func main() { 12 | // create zone 13 | zone := &newdns.Zone{ 14 | Name: "example.com.", 15 | MasterNameServer: "ns1.hostmaster.com.", 16 | AllNameServers: []string{ 17 | "ns1.hostmaster.com.", 18 | "ns2.hostmaster.com.", 19 | "ns3.hostmaster.com.", 20 | }, 21 | Handler: func(name string) ([]newdns.Set, error) { 22 | // return apex records 23 | if name == "" { 24 | return []newdns.Set{ 25 | { 26 | Name: "example.com.", 27 | Type: newdns.A, 28 | Records: []newdns.Record{ 29 | {Address: "1.2.3.4"}, 30 | }, 31 | }, 32 | { 33 | Name: "example.com.", 34 | Type: newdns.AAAA, 35 | Records: []newdns.Record{ 36 | {Address: "1:2:3:4::"}, 37 | }, 38 | }, 39 | }, nil 40 | } 41 | 42 | // return sub records 43 | if name == "foo" { 44 | return []newdns.Set{ 45 | { 46 | Name: "foo.example.com.", 47 | Type: newdns.CNAME, 48 | Records: []newdns.Record{ 49 | {Address: "bar.example.com."}, 50 | }, 51 | }, 52 | }, nil 53 | } 54 | 55 | return nil, nil 56 | }, 57 | } 58 | 59 | // create server 60 | server := newdns.NewServer(newdns.Config{ 61 | Handler: func(name string) (*newdns.Zone, error) { 62 | // check name 63 | if newdns.InZone("example.com.", name) { 64 | return zone, nil 65 | } 66 | 67 | return nil, nil 68 | }, 69 | Logger: func(e newdns.Event, msg *dns.Msg, err error, reason string) { 70 | fmt.Println(e, err, reason) 71 | }, 72 | }) 73 | 74 | // run server 75 | go func() { 76 | err := server.Run(":1337") 77 | if err != nil { 78 | panic(err) 79 | } 80 | }() 81 | 82 | // print info 83 | fmt.Println("Query apex: dig example.com @0.0.0.0 -p 1337") 84 | fmt.Println("Query other: dig foo.example.com @0.0.0.0 -p 1337") 85 | 86 | // wait forever 87 | select {} 88 | } 89 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/256dpi/newdns 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/miekg/dns v1.1.58 7 | github.com/stretchr/testify v1.9.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= 5 | github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 10 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 11 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 12 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 14 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 15 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 16 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 18 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 19 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 20 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 21 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 22 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 23 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 24 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 25 | golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 26 | golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 27 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 28 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 29 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 30 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 31 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 32 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 33 | golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= 34 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 35 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 37 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 39 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 40 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 41 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 42 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 47 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 50 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 51 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 52 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 53 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 54 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 55 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 56 | golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= 57 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 58 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 59 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 60 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 61 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 62 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 63 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 64 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 65 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 66 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 67 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 68 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 69 | golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= 70 | golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= 71 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 72 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 75 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 76 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 77 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import "github.com/miekg/dns" 4 | 5 | // Proxy returns a handler that proxies requests to the provided DNS server. The 6 | // optional logger is called with events about the processing of requests. 7 | func Proxy(addr string, logger Logger) dns.Handler { 8 | return dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { 9 | // log request 10 | if logger != nil { 11 | logger(ProxyRequest, req, nil, "") 12 | } 13 | 14 | // forward request to fallback 15 | rs, err := dns.Exchange(req, addr) 16 | if err != nil { 17 | if logger != nil { 18 | logger(ProxyError, nil, err, "") 19 | } 20 | _ = w.Close() 21 | return 22 | } 23 | 24 | // log response 25 | if logger != nil { 26 | logger(ProxyResponse, rs, nil, "") 27 | } 28 | 29 | // write response 30 | err = w.WriteMsg(rs) 31 | if err != nil { 32 | if logger != nil { 33 | logger(NetworkError, nil, err, "") 34 | } 35 | _ = w.Close() 36 | } 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // Query can be used to query a DNS server over the provided protocol on its 10 | // address for the specified name and type. The supplied function can be set to 11 | // mutate the request before sending. 12 | func Query(proto, addr, name, typ string, fn func(*dns.Msg)) (*dns.Msg, error) { 13 | // prepare request 14 | req := &dns.Msg{ 15 | MsgHdr: dns.MsgHdr{ 16 | Id: dns.Id(), 17 | }, 18 | Question: []dns.Question{ 19 | { 20 | Name: name, 21 | Qtype: dns.StringToType[typ], 22 | Qclass: dns.ClassINET, 23 | }, 24 | }, 25 | } 26 | 27 | // call function if available 28 | if fn != nil { 29 | fn(req) 30 | } 31 | 32 | // prepare client 33 | client := dns.Client{ 34 | Net: proto, 35 | Timeout: time.Second, 36 | } 37 | 38 | // send request 39 | res, _, err := client.Exchange(req, addr) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | // reset id to allow direct comparison 45 | res.Id = 0 46 | 47 | return res, nil 48 | } 49 | -------------------------------------------------------------------------------- /record.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | // Record holds a single DNS record. 9 | type Record struct { 10 | // The target address for A, AAAA, CNAME and MX records. 11 | Address string 12 | 13 | // The priority for MX records. 14 | Priority int 15 | 16 | // The data for TXT records. 17 | Data []string 18 | } 19 | 20 | // Validate will validate the record. 21 | func (r *Record) Validate(typ Type) error { 22 | // validate A address 23 | if typ == A { 24 | ip := net.ParseIP(r.Address) 25 | if ip == nil || ip.To4() == nil { 26 | return fmt.Errorf("invalid IPv4 address: %s", r.Address) 27 | } 28 | } 29 | 30 | // validate AAAA address 31 | if typ == AAAA { 32 | ip := net.ParseIP(r.Address) 33 | if ip == nil || ip.To16() == nil { 34 | return fmt.Errorf("invalid IPv6 address: %s", r.Address) 35 | } 36 | } 37 | 38 | // validate CNAME and MX addresses 39 | if typ == CNAME || typ == MX { 40 | if !IsDomain(r.Address, true) { 41 | return fmt.Errorf("invalid domain name: %s", r.Address) 42 | } 43 | } 44 | 45 | // check TXT data 46 | if typ == TXT { 47 | if len(r.Data) == 0 { 48 | return fmt.Errorf("missing data") 49 | } 50 | 51 | for _, data := range r.Data { 52 | if len(data) > 255 { 53 | return fmt.Errorf("data too long") 54 | } 55 | } 56 | } 57 | 58 | // validate NS addresses 59 | if typ == NS { 60 | if !IsDomain(r.Address, true) { 61 | return fmt.Errorf("invalid ns name: %s", r.Address) 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /record_test.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRecordValidate(t *testing.T) { 10 | table := []struct { 11 | typ Type 12 | rec Record 13 | err string 14 | }{ 15 | { 16 | typ: A, 17 | rec: Record{Address: "foo"}, 18 | err: "invalid IPv4 address: foo", 19 | }, 20 | { 21 | typ: AAAA, 22 | rec: Record{Address: "foo"}, 23 | err: "invalid IPv6 address: foo", 24 | }, 25 | { 26 | typ: A, 27 | rec: Record{Address: "1:2:3:4::"}, 28 | err: "invalid IPv4 address: 1:2:3:4::", 29 | }, 30 | { 31 | typ: A, 32 | rec: Record{Address: "1.2.3.4"}, 33 | }, 34 | { 35 | typ: AAAA, 36 | rec: Record{Address: "1:2:3:4::"}, 37 | }, 38 | { 39 | typ: CNAME, 40 | rec: Record{Address: "---"}, 41 | err: "invalid domain name: ---", 42 | }, 43 | { 44 | typ: CNAME, 45 | rec: Record{Address: "foo.com"}, 46 | err: "invalid domain name: foo.com", 47 | }, 48 | { 49 | typ: CNAME, 50 | rec: Record{Address: "foo.com."}, 51 | }, 52 | { 53 | typ: MX, 54 | rec: Record{Address: "foo.com"}, 55 | err: "invalid domain name: foo.com", 56 | }, 57 | { 58 | typ: MX, 59 | rec: Record{Address: "foo.com."}, 60 | }, 61 | { 62 | typ: TXT, 63 | rec: Record{Data: nil}, 64 | err: "missing data", 65 | }, 66 | { 67 | typ: TXT, 68 | rec: Record{Data: []string{"z4e6ycRMp6MP3WvWQMxIAOXglxANbj3oB0xD8BffktO4eo3VCR0s6TyGHKixvarOFJU0fqNkXeFOeI7sTXH5X0iXZukfLgnGTxLXNC7KkVFwtVFsh1P0IUNXtNBlOVWrVbxkS62ezbLpENNkiBwbkCvcTjwF2kyI0curAt9JhhJFb3AAq0q1iHWlJLn1KSrev9PIsY3alndDKjYTPxAojxzGKdK3A7rWLJ8Uzb3Z5OhLwP7jTKqbWVUocJRFLYpL"}}, 69 | err: "data too long", 70 | }, 71 | { 72 | typ: TXT, 73 | rec: Record{Data: []string{"foo"}}, 74 | }, 75 | { 76 | typ: NS, 77 | rec: Record{Address: "foo.com"}, 78 | err: "invalid ns name: foo.com", 79 | }, 80 | { 81 | typ: NS, 82 | rec: Record{Address: "foo.com."}, 83 | }, 84 | } 85 | 86 | for i, item := range table { 87 | err := item.rec.Validate(item.typ) 88 | if err != nil { 89 | assert.Equal(t, item.err, err.Error(), i) 90 | } else { 91 | assert.Equal(t, item.err, "", item) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /resolver.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | var fakeAddr = &net.TCPAddr{ 10 | IP: net.IP{0, 0, 0, 0}, 11 | Port: 0, 12 | } 13 | 14 | type responseWriter struct { 15 | msg *dns.Msg 16 | } 17 | 18 | func (w *responseWriter) LocalAddr() net.Addr { 19 | return fakeAddr 20 | } 21 | 22 | func (w *responseWriter) RemoteAddr() net.Addr { 23 | return fakeAddr 24 | } 25 | 26 | func (w *responseWriter) WriteMsg(msg *dns.Msg) error { 27 | // check message 28 | if w.msg != nil { 29 | panic("message already set") 30 | } 31 | 32 | // set message 33 | w.msg = msg 34 | 35 | return nil 36 | } 37 | 38 | func (w *responseWriter) Write([]byte) (int, error) { 39 | panic("not implemented") 40 | } 41 | 42 | func (w *responseWriter) Close() error { 43 | return nil 44 | } 45 | 46 | func (w *responseWriter) TsigStatus() error { 47 | panic("not implemented") 48 | } 49 | 50 | func (w *responseWriter) TsigTimersOnly(bool) { 51 | panic("not implemented") 52 | } 53 | 54 | func (w *responseWriter) Hijack() { 55 | panic("not implemented") 56 | } 57 | 58 | // Resolver returns a very primitive recursive resolver that uses the provided 59 | // handler to resolve all names. 60 | func Resolver(handler dns.Handler) dns.Handler { 61 | return dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) { 62 | // forward query if no recursion is desired 63 | if !req.RecursionDesired { 64 | handler.ServeDNS(w, req) 65 | return 66 | } 67 | 68 | // prepare response 69 | res := new(dns.Msg) 70 | res.SetReply(req) 71 | res.RecursionAvailable = true 72 | 73 | // query handler 74 | var wr responseWriter 75 | handler.ServeDNS(&wr, req) 76 | 77 | // check response 78 | if wr.msg == nil { 79 | _ = w.WriteMsg(res) 80 | return 81 | } 82 | 83 | // add resolved answers 84 | res.Answer = append(res.Answer, resolve(handler, wr.msg.Answer)...) 85 | 86 | // write response 87 | err := w.WriteMsg(res) 88 | if err != nil { 89 | _ = w.Close() 90 | } 91 | }) 92 | } 93 | 94 | func resolve(handler dns.Handler, records []dns.RR) []dns.RR { 95 | // prepare result 96 | var res []dns.RR 97 | res = append(res, records...) 98 | 99 | // handle records 100 | for _, record := range records { 101 | if cname, ok := record.(*dns.CNAME); ok { 102 | // query handler 103 | var wr responseWriter 104 | handler.ServeDNS(&wr, &dns.Msg{ 105 | Question: []dns.Question{ 106 | { 107 | Name: cname.Target, 108 | Qtype: dns.TypeA, 109 | Qclass: dns.ClassINET, 110 | }, 111 | }, 112 | }) 113 | 114 | // add resolved answers 115 | res = append(res, resolve(handler, wr.msg.Answer)...) 116 | } 117 | } 118 | 119 | return res 120 | } 121 | -------------------------------------------------------------------------------- /resolver_test.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestResolver(t *testing.T) { 11 | ret, err := Query("tcp", "1.1.1.1:53", "example.newdns.256dpi.com.", "A", func(msg *dns.Msg) { 12 | msg.RecursionDesired = true 13 | }) 14 | assert.NoError(t, err) 15 | equalJSON(t, &dns.Msg{ 16 | MsgHdr: dns.MsgHdr{ 17 | Response: true, 18 | RecursionDesired: true, 19 | RecursionAvailable: true, 20 | }, 21 | Question: []dns.Question{ 22 | {Name: "example.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 23 | }, 24 | Answer: []dns.RR{ 25 | &dns.CNAME{ 26 | Hdr: dns.RR_Header{ 27 | Name: "example.newdns.256dpi.com.", 28 | Rrtype: dns.TypeCNAME, 29 | Class: dns.ClassINET, 30 | Ttl: ret.Answer[0].(*dns.CNAME).Hdr.Ttl, 31 | Rdlength: 10, 32 | }, 33 | Target: "example.com.", 34 | }, 35 | &dns.A{ 36 | Hdr: dns.RR_Header{ 37 | Name: "example.com.", 38 | Rrtype: dns.TypeA, 39 | Class: dns.ClassINET, 40 | Ttl: ret.Answer[1].(*dns.A).Hdr.Ttl, 41 | Rdlength: 4, 42 | }, 43 | A: ret.Answer[1].(*dns.A).A, 44 | }, 45 | }, 46 | }, ret) 47 | 48 | addr := "0.0.0.0:53002" 49 | mux := dns.NewServeMux() 50 | mux.Handle("newdns.256dpi.com", Proxy(awsNS[0]+":53", nil)) 51 | mux.Handle("example.com", Proxy("a.iana-servers.net:53", nil)) 52 | handler := Resolver(mux) 53 | 54 | serve(handler, addr, func() { 55 | ret, err := Query("udp", addr, "example.newdns.256dpi.com.", "A", func(msg *dns.Msg) { 56 | msg.RecursionDesired = true 57 | }) 58 | assert.NoError(t, err) 59 | equalJSON(t, &dns.Msg{ 60 | MsgHdr: dns.MsgHdr{ 61 | Response: true, 62 | RecursionDesired: true, 63 | RecursionAvailable: true, 64 | }, 65 | Question: []dns.Question{ 66 | {Name: "example.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 67 | }, 68 | Answer: []dns.RR{ 69 | &dns.CNAME{ 70 | Hdr: dns.RR_Header{ 71 | Name: "example.newdns.256dpi.com.", 72 | Rrtype: dns.TypeCNAME, 73 | Class: dns.ClassINET, 74 | Ttl: ret.Answer[0].(*dns.CNAME).Hdr.Ttl, 75 | Rdlength: 13, 76 | }, 77 | Target: "example.com.", 78 | }, 79 | &dns.A{ 80 | Hdr: dns.RR_Header{ 81 | Name: "example.com.", 82 | Rrtype: dns.TypeA, 83 | Class: dns.ClassINET, 84 | Ttl: ret.Answer[1].(*dns.A).Hdr.Ttl, 85 | Rdlength: 4, 86 | }, 87 | A: ret.Answer[1].(*dns.A).A, 88 | }, 89 | }, 90 | }, ret) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // Accept will return a dns.MsgAcceptFunc that only accepts normal queries. 10 | func Accept(logger Logger) dns.MsgAcceptFunc { 11 | return func(dh dns.Header) dns.MsgAcceptAction { 12 | // check if request 13 | if dh.Bits&(1<<15) != 0 { 14 | log(logger, Ignored, nil, nil, fmt.Sprintf("not a request")) 15 | return dns.MsgIgnore 16 | } 17 | 18 | // check opcode 19 | if int(dh.Bits>>11)&0xF != dns.OpcodeQuery { 20 | log(logger, Ignored, nil, nil, fmt.Sprintf("not a query")) 21 | return dns.MsgIgnore 22 | } 23 | 24 | // check question count 25 | if dh.Qdcount != 1 { 26 | log(logger, Ignored, nil, nil, fmt.Sprintf("invalid question count: %d", dh.Qdcount)) 27 | return dns.MsgIgnore 28 | } 29 | 30 | return dns.MsgAccept 31 | } 32 | } 33 | 34 | // Run will start a UDP and TCP listener to serve the specified handler with the 35 | // specified accept function until the provided close channel is closed. It will 36 | // return the first error of a listener. 37 | func Run(addr string, handler dns.Handler, accept dns.MsgAcceptFunc, close <-chan struct{}) error { 38 | // prepare servers 39 | udp := &dns.Server{Addr: addr, Net: "udp", Handler: handler, MsgAcceptFunc: accept} 40 | tcp := &dns.Server{Addr: addr, Net: "tcp", Handler: handler, MsgAcceptFunc: accept} 41 | 42 | // prepare errors 43 | errs := make(chan error, 2) 44 | 45 | // run udp server 46 | go func() { 47 | errs <- udp.ListenAndServe() 48 | }() 49 | 50 | // run tcp server 51 | go func() { 52 | errs <- tcp.ListenAndServe() 53 | }() 54 | 55 | // await first error 56 | var err error 57 | select { 58 | case err = <-errs: 59 | case <-close: 60 | } 61 | 62 | // shutdown servers 63 | _ = udp.Shutdown() 64 | _ = tcp.Shutdown() 65 | 66 | return err 67 | } 68 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | // Config provides configuration for a DNS server. 11 | type Config struct { 12 | // The buffer size used if EDNS is enabled by a client. 13 | // 14 | // Default: 1220. 15 | BufferSize int 16 | 17 | // The list of zones handled by this server. 18 | // 19 | // Default: ["."]. 20 | Zones []string 21 | 22 | // Handler is the callback that returns a zone for the specified name. 23 | // The returned zone must not be altered going forward. 24 | Handler func(name string) (*Zone, error) 25 | 26 | // The fallback DNS server to be used if the zones is not matched. Exact 27 | // zones must be provided above for this to work. 28 | Fallback string 29 | 30 | // Reporter is the callback called with request errors. 31 | Logger Logger 32 | } 33 | 34 | // Server is a DNS server. 35 | type Server struct { 36 | config Config 37 | close chan struct{} 38 | } 39 | 40 | // NewServer creates and returns a new DNS server. 41 | func NewServer(config Config) *Server { 42 | // set default buffer size 43 | if config.BufferSize <= 0 { 44 | config.BufferSize = 1220 45 | } 46 | 47 | // set default zone 48 | if len(config.Zones) == 0 { 49 | config.Zones = []string{"."} 50 | } 51 | 52 | // check zones if fallback 53 | if config.Fallback != "" { 54 | for _, zone := range config.Zones { 55 | if zone == "." { 56 | panic(`fallback conflicts with the match all pattern "." (default)`) 57 | } 58 | } 59 | } 60 | 61 | return &Server{ 62 | config: config, 63 | close: make(chan struct{}), 64 | } 65 | } 66 | 67 | // Run will run a UDP and TCP server on the specified address. It will return 68 | // on the first accept error and close all servers. 69 | func (s *Server) Run(addr string) error { 70 | // prepare mux 71 | mux := dns.NewServeMux() 72 | 73 | // register handler 74 | for _, zone := range s.config.Zones { 75 | mux.Handle(zone, s) 76 | } 77 | 78 | // add fallback if available 79 | if s.config.Fallback != "" { 80 | mux.Handle(".", Proxy(s.config.Fallback, s.config.Logger)) 81 | } 82 | 83 | // run server 84 | err := Run(addr, mux, Accept(s.config.Logger), s.close) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // ServeDNS implements the dns.Handler interface. 93 | func (s *Server) ServeDNS(w dns.ResponseWriter, req *dns.Msg) { 94 | // get question 95 | question := req.Question[0] 96 | 97 | // check class 98 | if question.Qclass != dns.ClassINET { 99 | log(s.config.Logger, Ignored, nil, nil, fmt.Sprintf("unsupported class: %s", dns.ClassToString[question.Qclass])) 100 | return 101 | } 102 | 103 | // log request and finish 104 | log(s.config.Logger, Request, req, nil, "") 105 | defer log(s.config.Logger, Finish, nil, nil, "") 106 | 107 | // prepare response 108 | res := new(dns.Msg) 109 | res.SetReply(req) 110 | 111 | // always compress responses 112 | res.Compress = true 113 | 114 | // set flag 115 | res.Authoritative = true 116 | 117 | // check edns 118 | if req.IsEdns0() != nil { 119 | // use edns in reply 120 | res.SetEdns0(uint16(s.config.BufferSize), false) 121 | 122 | // check version 123 | if req.IsEdns0().Version() != 0 { 124 | log(s.config.Logger, Refused, nil, nil, fmt.Sprintf("unsupported EDNS version: %d", req.IsEdns0().Version())) 125 | s.writeError(w, req, res, nil, dns.RcodeBadVers) 126 | return 127 | } 128 | } 129 | 130 | // check any type 131 | if question.Qtype == dns.TypeANY { 132 | log(s.config.Logger, Refused, nil, nil, "unsupported type: ANY") 133 | s.writeError(w, req, res, nil, dns.RcodeNotImplemented) 134 | return 135 | } 136 | 137 | // get name 138 | name := NormalizeDomain(question.Name, true, false, false) 139 | 140 | // get zone 141 | zone, err := s.config.Handler(name) 142 | if err != nil { 143 | err = fmt.Errorf("server handler error: %w", err) 144 | log(s.config.Logger, BackendError, nil, err, "") 145 | s.writeError(w, req, res, nil, dns.RcodeServerFailure) 146 | return 147 | } 148 | 149 | // check zone 150 | if zone == nil { 151 | log(s.config.Logger, Refused, nil, nil, "no zone") 152 | res.Authoritative = false 153 | s.writeError(w, req, res, nil, dns.RcodeRefused) 154 | return 155 | } 156 | 157 | // validate zone 158 | err = zone.Validate() 159 | if err != nil { 160 | log(s.config.Logger, BackendError, nil, err, "") 161 | s.writeError(w, req, res, nil, dns.RcodeServerFailure) 162 | return 163 | } 164 | 165 | // answer SOA directly 166 | if question.Qtype == dns.TypeSOA && name == zone.Name { 167 | s.writeSOAResponse(w, req, res, zone) 168 | return 169 | } 170 | 171 | // answer NS directly 172 | if question.Qtype == dns.TypeNS && name == zone.Name { 173 | s.writeNSResponse(w, req, res, zone) 174 | return 175 | } 176 | 177 | // check type 178 | typ := Type(question.Qtype) 179 | 180 | // lookup main answer 181 | answer, exists, err := zone.Lookup(name, typ) 182 | if err != nil { 183 | log(s.config.Logger, BackendError, nil, err, "") 184 | s.writeError(w, req, res, nil, dns.RcodeServerFailure) 185 | return 186 | } 187 | 188 | // handle absence 189 | if len(answer) == 0 { 190 | if exists { 191 | // we have a record, but not of the requested type 192 | s.writeError(w, req, res, zone, dns.RcodeSuccess) 193 | } else { 194 | // we have no record at all for this name 195 | s.writeError(w, req, res, zone, dns.RcodeNameError) 196 | } 197 | return 198 | } 199 | 200 | // prepare extra set 201 | var extra []Set 202 | 203 | // TODO: Lookup glue records for NS records? 204 | 205 | // lookup extra sets 206 | for _, set := range answer { 207 | for _, record := range set.Records { 208 | switch set.Type { 209 | case MX: 210 | // lookup internal MX target A and AAAA records 211 | if InZone(zone.Name, record.Address) { 212 | ret, _, err := zone.Lookup(record.Address, A, AAAA) 213 | if err != nil { 214 | log(s.config.Logger, BackendError, nil, err, "") 215 | s.writeError(w, req, res, nil, dns.RcodeServerFailure) 216 | return 217 | } 218 | 219 | // add to extra 220 | extra = append(extra, ret...) 221 | } 222 | } 223 | } 224 | } 225 | 226 | // set answer 227 | for _, set := range answer { 228 | res.Answer = append(res.Answer, s.convert(question.Name, zone, set)...) 229 | } 230 | 231 | // set extra 232 | for _, set := range extra { 233 | res.Extra = append(res.Extra, s.convert(question.Name, zone, set)...) 234 | } 235 | 236 | // add ns records 237 | for _, ns := range zone.AllNameServers { 238 | res.Ns = append(res.Ns, &dns.NS{ 239 | Hdr: dns.RR_Header{ 240 | Name: TransferCase(question.Name, zone.Name), 241 | Rrtype: dns.TypeNS, 242 | Class: dns.ClassINET, 243 | Ttl: toSeconds(zone.NSTTL), 244 | }, 245 | Ns: ns, 246 | }) 247 | } 248 | 249 | // check if NS query 250 | if typ == NS { 251 | // move answers 252 | res.Ns = res.Answer 253 | res.Answer = nil 254 | 255 | // no authoritative response for other zone in NS queries 256 | res.Authoritative = false 257 | } 258 | 259 | // write message 260 | s.writeMessage(w, req, res) 261 | } 262 | 263 | // Close will close the server. 264 | func (s *Server) Close() { 265 | defer func() { recover() }() 266 | close(s.close) 267 | } 268 | 269 | func (s *Server) writeSOAResponse(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone) { 270 | // add soa record 271 | rs.Answer = append(rs.Answer, &dns.SOA{ 272 | Hdr: dns.RR_Header{ 273 | Name: zone.Name, 274 | Rrtype: dns.TypeSOA, 275 | Class: dns.ClassINET, 276 | Ttl: toSeconds(zone.SOATTL), 277 | }, 278 | Ns: zone.MasterNameServer, 279 | Mbox: emailToDomain(zone.AdminEmail), 280 | Serial: 1, 281 | Refresh: toSeconds(zone.Refresh), 282 | Retry: toSeconds(zone.Retry), 283 | Expire: toSeconds(zone.Expire), 284 | Minttl: toSeconds(zone.MinTTL), 285 | }) 286 | 287 | // add ns records 288 | for _, ns := range zone.AllNameServers { 289 | rs.Ns = append(rs.Ns, &dns.NS{ 290 | Hdr: dns.RR_Header{ 291 | Name: zone.Name, 292 | Rrtype: dns.TypeNS, 293 | Class: dns.ClassINET, 294 | Ttl: toSeconds(zone.NSTTL), 295 | }, 296 | Ns: ns, 297 | }) 298 | } 299 | 300 | // write message 301 | s.writeMessage(w, rq, rs) 302 | } 303 | 304 | func (s *Server) writeNSResponse(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone) { 305 | // add ns records 306 | for _, ns := range zone.AllNameServers { 307 | rs.Answer = append(rs.Answer, &dns.NS{ 308 | Hdr: dns.RR_Header{ 309 | Name: zone.Name, 310 | Rrtype: dns.TypeNS, 311 | Class: dns.ClassINET, 312 | Ttl: toSeconds(zone.NSTTL), 313 | }, 314 | Ns: ns, 315 | }) 316 | } 317 | 318 | // write message 319 | s.writeMessage(w, rq, rs) 320 | } 321 | 322 | func (s *Server) writeError(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone, code int) { 323 | // set code 324 | rs.Rcode = code 325 | 326 | // add soa record 327 | if zone != nil { 328 | rs.Ns = append(rs.Ns, &dns.SOA{ 329 | Hdr: dns.RR_Header{ 330 | Name: zone.Name, 331 | Rrtype: dns.TypeSOA, 332 | Class: dns.ClassINET, 333 | Ttl: toSeconds(zone.SOATTL), 334 | }, 335 | Ns: zone.MasterNameServer, 336 | Mbox: emailToDomain(zone.AdminEmail), 337 | Serial: 1, 338 | Refresh: toSeconds(zone.Refresh), 339 | Retry: toSeconds(zone.Retry), 340 | Expire: toSeconds(zone.Expire), 341 | Minttl: toSeconds(zone.MinTTL), 342 | }) 343 | } 344 | 345 | // write message 346 | s.writeMessage(w, rq, rs) 347 | } 348 | 349 | func (s *Server) writeMessage(w dns.ResponseWriter, rq, rs *dns.Msg) { 350 | // get buffer size 351 | var buffer = 512 352 | if rq.IsEdns0() != nil { 353 | buffer = int(rq.IsEdns0().UDPSize()) 354 | } 355 | 356 | // determine if client is using UDP 357 | isUDP := w.RemoteAddr().Network() == "udp" 358 | 359 | // truncate message if client is using UDP and message is too long 360 | if isUDP && rs.Len() > buffer { 361 | rs.Truncated = true 362 | rs.Answer = nil 363 | rs.Ns = nil 364 | rs.Extra = nil 365 | } 366 | 367 | // write message 368 | err := w.WriteMsg(rs) 369 | if err != nil { 370 | log(s.config.Logger, NetworkError, nil, err, "") 371 | _ = w.Close() 372 | return 373 | } 374 | 375 | // log response 376 | log(s.config.Logger, Response, rs, nil, "") 377 | } 378 | 379 | func (s *Server) convert(query string, zone *Zone, set Set) []dns.RR { 380 | // prepare header 381 | header := dns.RR_Header{ 382 | Name: TransferCase(query, set.Name), 383 | Rrtype: uint16(set.Type), 384 | Class: dns.ClassINET, 385 | Ttl: toSeconds(set.TTL), 386 | } 387 | 388 | // ensure zone min TTL 389 | if set.TTL < zone.MinTTL { 390 | header.Ttl = toSeconds(zone.MinTTL) 391 | } 392 | 393 | // prepare list 394 | var list []dns.RR 395 | 396 | // add records 397 | for _, record := range set.Records { 398 | // construct record 399 | switch set.Type { 400 | case A: 401 | list = append(list, &dns.A{ 402 | Hdr: header, 403 | A: net.ParseIP(record.Address), 404 | }) 405 | case AAAA: 406 | list = append(list, &dns.AAAA{ 407 | Hdr: header, 408 | AAAA: net.ParseIP(record.Address), 409 | }) 410 | case CNAME: 411 | list = append(list, &dns.CNAME{ 412 | Hdr: header, 413 | Target: dns.Fqdn(record.Address), 414 | }) 415 | case MX: 416 | list = append(list, &dns.MX{ 417 | Hdr: header, 418 | Preference: uint16(record.Priority), 419 | Mx: dns.Fqdn(record.Address), 420 | }) 421 | case TXT: 422 | list = append(list, &dns.TXT{ 423 | Hdr: header, 424 | Txt: record.Data, 425 | }) 426 | case NS: 427 | list = append(list, &dns.NS{ 428 | Hdr: header, 429 | Ns: dns.Fqdn(record.Address), 430 | }) 431 | } 432 | } 433 | 434 | return list 435 | } 436 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "sort" 7 | "testing" 8 | "time" 9 | 10 | "github.com/miekg/dns" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var awsNS = []string{ 15 | "ns-1071.awsdns-05.org.", 16 | "ns-140.awsdns-17.com.", 17 | "ns-1978.awsdns-55.co.uk.", 18 | "ns-812.awsdns-37.net.", 19 | } 20 | 21 | const awsPrimaryNS = "ns-140.awsdns-17.com." 22 | 23 | var awsOtherNS = []string{ 24 | "ns-1074.awsdns-06.org.", 25 | "ns-1631.awsdns-11.co.uk.", 26 | "ns-243.awsdns-30.com.", 27 | "ns-869.awsdns-44.net.", 28 | } 29 | 30 | var nsRRs = []dns.RR{ 31 | &dns.NS{ 32 | Hdr: dns.RR_Header{ 33 | Name: "newdns.256dpi.com.", 34 | Rrtype: dns.TypeNS, 35 | Class: dns.ClassINET, 36 | Ttl: 172800, 37 | Rdlength: 23, 38 | }, 39 | Ns: awsNS[0], 40 | }, 41 | &dns.NS{ 42 | Hdr: dns.RR_Header{ 43 | Name: "newdns.256dpi.com.", 44 | Rrtype: dns.TypeNS, 45 | Class: dns.ClassINET, 46 | Ttl: 172800, 47 | Rdlength: 19, 48 | }, 49 | Ns: awsNS[1], 50 | }, 51 | &dns.NS{ 52 | Hdr: dns.RR_Header{ 53 | Name: "newdns.256dpi.com.", 54 | Rrtype: dns.TypeNS, 55 | Class: dns.ClassINET, 56 | Ttl: 172800, 57 | Rdlength: 25, 58 | }, 59 | Ns: awsNS[2], 60 | }, 61 | &dns.NS{ 62 | Hdr: dns.RR_Header{ 63 | Name: "newdns.256dpi.com.", 64 | Rrtype: dns.TypeNS, 65 | Class: dns.ClassINET, 66 | Ttl: 172800, 67 | Rdlength: 22, 68 | }, 69 | Ns: awsNS[3], 70 | }, 71 | } 72 | 73 | var otherNSRRs = []dns.RR{ 74 | &dns.NS{ 75 | Hdr: dns.RR_Header{ 76 | Name: "other.newdns.256dpi.com.", 77 | Rrtype: dns.TypeNS, 78 | Class: dns.ClassINET, 79 | Ttl: 300, 80 | Rdlength: 23, 81 | }, 82 | Ns: awsOtherNS[0], 83 | }, 84 | &dns.NS{ 85 | Hdr: dns.RR_Header{ 86 | Name: "other.newdns.256dpi.com.", 87 | Rrtype: dns.TypeNS, 88 | Class: dns.ClassINET, 89 | Ttl: 300, 90 | Rdlength: 19, 91 | }, 92 | Ns: awsOtherNS[1], 93 | }, 94 | &dns.NS{ 95 | Hdr: dns.RR_Header{ 96 | Name: "other.newdns.256dpi.com.", 97 | Rrtype: dns.TypeNS, 98 | Class: dns.ClassINET, 99 | Ttl: 300, 100 | Rdlength: 25, 101 | }, 102 | Ns: awsOtherNS[2], 103 | }, 104 | &dns.NS{ 105 | Hdr: dns.RR_Header{ 106 | Name: "other.newdns.256dpi.com.", 107 | Rrtype: dns.TypeNS, 108 | Class: dns.ClassINET, 109 | Ttl: 300, 110 | Rdlength: 22, 111 | }, 112 | Ns: awsOtherNS[3], 113 | }, 114 | } 115 | 116 | func TestAWS(t *testing.T) { 117 | t.Run("UDP", func(t *testing.T) { 118 | conformanceTests(t, "udp", awsPrimaryNS+":53", false) 119 | }) 120 | 121 | t.Run("TCP", func(t *testing.T) { 122 | conformanceTests(t, "tcp", awsPrimaryNS+":53", false) 123 | }) 124 | 125 | t.Run("Resolver", func(t *testing.T) { 126 | resolverTests(t, awsPrimaryNS+":53") 127 | }) 128 | } 129 | 130 | func TestServer(t *testing.T) { 131 | zone := &Zone{ 132 | Name: "newdns.256dpi.com.", 133 | MasterNameServer: awsPrimaryNS, 134 | AllNameServers: []string{ 135 | awsNS[0], 136 | awsNS[1], 137 | awsNS[2], 138 | awsNS[3], 139 | }, 140 | AdminEmail: "awsdns-hostmaster@amazon.com", 141 | Refresh: 2 * time.Hour, 142 | Retry: 15 * time.Minute, 143 | Expire: 336 * time.Hour, 144 | SOATTL: 15 * time.Minute, 145 | NSTTL: 48 * time.Hour, 146 | MinTTL: 5 * time.Minute, 147 | Handler: func(name string) ([]Set, error) { 148 | // handle apex records 149 | if name == "" { 150 | return []Set{ 151 | { 152 | Name: "newdns.256dpi.com.", 153 | Type: A, 154 | Records: []Record{ 155 | {Address: "1.2.3.4"}, 156 | }, 157 | }, 158 | { 159 | Name: "newdns.256dpi.com.", 160 | Type: AAAA, 161 | Records: []Record{ 162 | {Address: "1:2:3:4::"}, 163 | }, 164 | }, 165 | { 166 | Name: "newdns.256dpi.com.", 167 | Type: TXT, 168 | Records: []Record{ 169 | {Data: []string{"baz"}}, 170 | {Data: []string{"foo", "bar"}}, 171 | }, 172 | }, 173 | }, nil 174 | } 175 | 176 | // handle example 177 | if name == "example" { 178 | return []Set{ 179 | { 180 | Name: "example.newdns.256dpi.com.", 181 | Type: CNAME, 182 | Records: []Record{ 183 | {Address: "example.com."}, 184 | }, 185 | }, 186 | }, nil 187 | } 188 | 189 | // handle ip4 190 | if name == "ip4" { 191 | return []Set{ 192 | { 193 | Name: "ip4.newdns.256dpi.com.", 194 | Type: A, 195 | Records: []Record{ 196 | {Address: "1.2.3.4"}, 197 | }, 198 | }, 199 | }, nil 200 | } 201 | 202 | // handle ip6 203 | if name == "ip6" { 204 | return []Set{ 205 | { 206 | Name: "ip6.newdns.256dpi.com.", 207 | Type: AAAA, 208 | Records: []Record{ 209 | {Address: "1:2:3:4::"}, 210 | }, 211 | }, 212 | }, nil 213 | } 214 | 215 | // handle mail 216 | if name == "mail" { 217 | return []Set{ 218 | { 219 | Name: "mail.newdns.256dpi.com.", 220 | Type: MX, 221 | Records: []Record{ 222 | {Address: "mail.example.com.", Priority: 7}, 223 | }, 224 | }, 225 | }, nil 226 | } 227 | 228 | // handle multimail 229 | if name == "multimail" { 230 | return []Set{ 231 | { 232 | Name: "multimail.newdns.256dpi.com.", 233 | Type: MX, 234 | Records: []Record{ 235 | {Address: "mail1.example.com.", Priority: 1}, 236 | {Address: "mail2.example.com.", Priority: 10}, 237 | {Address: "mail3.example.com.", Priority: 10}, 238 | }, 239 | }, 240 | }, nil 241 | } 242 | 243 | // handle text 244 | if name == "text" { 245 | return []Set{ 246 | { 247 | Name: "text.newdns.256dpi.com.", 248 | Type: TXT, 249 | Records: []Record{ 250 | {Data: []string{"foo", "bar"}}, 251 | }, 252 | }, 253 | }, nil 254 | } 255 | 256 | // handle ref4 257 | if name == "ref4" { 258 | return []Set{ 259 | { 260 | Name: "ref4.newdns.256dpi.com.", 261 | Type: CNAME, 262 | Records: []Record{ 263 | {Address: "ip4.newdns.256dpi.com."}, 264 | }, 265 | }, 266 | }, nil 267 | } 268 | 269 | // handle ref6 270 | if name == "ref6" { 271 | return []Set{ 272 | { 273 | Name: "ref6.newdns.256dpi.com.", 274 | Type: CNAME, 275 | Records: []Record{ 276 | {Address: "ip6.newdns.256dpi.com."}, 277 | }, 278 | }, 279 | }, nil 280 | } 281 | 282 | // handle refref 283 | if name == "refref" { 284 | return []Set{ 285 | { 286 | Name: "refref.newdns.256dpi.com.", 287 | Type: CNAME, 288 | Records: []Record{ 289 | {Address: "ref4.newdns.256dpi.com."}, 290 | }, 291 | }, 292 | }, nil 293 | } 294 | 295 | // handle ref4m 296 | if name == "ref4m" { 297 | return []Set{ 298 | { 299 | Name: "ref4m.newdns.256dpi.com.", 300 | Type: MX, 301 | Records: []Record{ 302 | {Address: "ip4.newdns.256dpi.com.", Priority: 7}, 303 | }, 304 | }, 305 | }, nil 306 | } 307 | 308 | // handle ref6m 309 | if name == "ref6m" { 310 | return []Set{ 311 | { 312 | Name: "ref6m.newdns.256dpi.com.", 313 | Type: MX, 314 | Records: []Record{ 315 | {Address: "ip6.newdns.256dpi.com.", Priority: 7}, 316 | }, 317 | }, 318 | }, nil 319 | } 320 | 321 | // handle long 322 | if name == "long" { 323 | return []Set{ 324 | { 325 | Name: "long.newdns.256dpi.com.", 326 | Type: TXT, 327 | Records: []Record{ 328 | {Data: []string{"gyK4oL9X8Zn3b6TwmUIYAgQx43rBOWMqJWR3wGMGNaZgajnhd2u9JaIbGwNo6gzZunyKYRxID3mKLmYUCcIrNYuo8R4UkijZeshwqEAM2EWnjNsB1hJHOlu6VyRKW13rsFUJedOSqc7YjjUoxm9c3mF28tEXmc3GVsC476wJ2ciSbp7ujDjQ032SQRD6kpayzFX8GncS5KXP8mLK2ZIqK2U4fUmYEpTPQMmp7w24GKkfGJzE4JfMBxSybDUScLq"}}, 329 | {Data: []string{"upNh05zi9flqN2puI9eIGgAgl3gwc65l3WjFdnE3u55dhyUyIoKbOlc1mQJPULPkn1V5TTG9rLBB8AzNfeL8jvwO8h0mzmJhPH8n6dkgI546jB8Z0g0MRJxN5VNSixjFjdR8vtUp6EWlVi7QSe9SYInghV0M17zZ8mXSHwTfYZaPH54ng22mSWzVbRX2tlUPLTNRB5CHrEtxliyhhQlRey98P5G0eo35FUXdqzOSJ3HGqDssBWQAxK3I9feOjbE"}}, 330 | {Data: []string{"z4e6ycRMp6MP3WvWQMxIAOXglxANbj3oB0xD8BffktO4eo3VCR0s6TyGHKixvarOFJU0fqNkXeFOeI7sTXH5X0iXZukfLgnGTxLXNC7KkVFwtVFsh1P0IUNXtNBlOVWrVbxkS62ezbLpENNkiBwbkCvcTjwF2kyI0curAt9JhhJFb3AAq0q1iHWlJLn1KSrev9PIsY3alndDKjYTPxAojxzGKdK3A7rWLJ8Uzb3Z5OhLwP7jTKqbWVUocJRFLYp"}}, 331 | }, 332 | }, 333 | }, nil 334 | } 335 | 336 | // handle other 337 | if name == "other" { 338 | return []Set{ 339 | { 340 | Name: "other.newdns.256dpi.com.", 341 | Type: NS, 342 | Records: []Record{ 343 | {Address: awsOtherNS[0]}, 344 | {Address: awsOtherNS[1]}, 345 | {Address: awsOtherNS[2]}, 346 | {Address: awsOtherNS[3]}, 347 | }, 348 | }, 349 | }, nil 350 | } 351 | 352 | return nil, nil 353 | }, 354 | } 355 | 356 | server := NewServer(Config{ 357 | BufferSize: 4096, 358 | Handler: func(name string) (*Zone, error) { 359 | if InZone("newdns.256dpi.com.", name) { 360 | return zone, nil 361 | } 362 | 363 | return nil, nil 364 | }, 365 | Logger: func(e Event, msg *dns.Msg, err error, reason string) { 366 | if e == NetworkError { 367 | panic(err.Error()) 368 | } 369 | }, 370 | }) 371 | 372 | addr := "0.0.0.0:53001" 373 | 374 | run(server, addr, func() { 375 | t.Run("UDP", func(t *testing.T) { 376 | conformanceTests(t, "udp", addr, true) 377 | additionalTests(t, "udp", addr) 378 | }) 379 | 380 | t.Run("TCP", func(t *testing.T) { 381 | conformanceTests(t, "tcp", addr, true) 382 | additionalTests(t, "tcp", addr) 383 | }) 384 | 385 | t.Run("Resolver", func(t *testing.T) { 386 | resolverTests(t, addr) 387 | }) 388 | }) 389 | } 390 | 391 | func TestServerFallback(t *testing.T) { 392 | zone := &Zone{ 393 | Name: "example.com.", 394 | MasterNameServer: "ns1.example.com.", 395 | AllNameServers: []string{ 396 | "ns1.example.com.", 397 | "ns2.example.com.", 398 | }, 399 | Handler: func(name string) ([]Set, error) { 400 | // handle apex 401 | if name == "" { 402 | return []Set{ 403 | { 404 | Name: "example.com.", 405 | Type: A, 406 | Records: []Record{ 407 | {Address: "1.2.3.4"}, 408 | }, 409 | }, 410 | }, nil 411 | } 412 | 413 | return nil, nil 414 | }, 415 | } 416 | 417 | server := NewServer(Config{ 418 | Zones: []string{"example.com."}, 419 | Handler: func(name string) (*Zone, error) { 420 | if InZone("example.com.", name) { 421 | return zone, nil 422 | } 423 | 424 | return nil, nil 425 | }, 426 | Fallback: "1.1.1.1:53", 427 | }) 428 | 429 | addr := "0.0.0.0:53002" 430 | 431 | run(server, addr, func() { 432 | // internal zone 433 | ret, err := Query("udp", addr, "example.com.", "A", func(msg *dns.Msg) { 434 | msg.RecursionDesired = true 435 | }) 436 | assert.NoError(t, err) 437 | equalJSON(t, &dns.Msg{ 438 | MsgHdr: dns.MsgHdr{ 439 | Response: true, 440 | Authoritative: true, 441 | RecursionDesired: true, 442 | }, 443 | Question: []dns.Question{ 444 | {Name: "example.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 445 | }, 446 | Answer: []dns.RR{ 447 | &dns.A{ 448 | Hdr: dns.RR_Header{ 449 | Name: "example.com.", 450 | Rrtype: dns.TypeA, 451 | Class: dns.ClassINET, 452 | Ttl: 300, 453 | Rdlength: 4, 454 | }, 455 | A: net.ParseIP("1.2.3.4"), 456 | }, 457 | }, 458 | Ns: []dns.RR{ 459 | &dns.NS{ 460 | Hdr: dns.RR_Header{ 461 | Name: "example.com.", 462 | Rrtype: dns.TypeNS, 463 | Class: dns.ClassINET, 464 | Ttl: 172800, 465 | Rdlength: 6, 466 | }, 467 | Ns: "ns1.example.com.", 468 | }, 469 | &dns.NS{ 470 | Hdr: dns.RR_Header{ 471 | Name: "example.com.", 472 | Rrtype: dns.TypeNS, 473 | Class: dns.ClassINET, 474 | Ttl: 172800, 475 | Rdlength: 6, 476 | }, 477 | Ns: "ns2.example.com.", 478 | }, 479 | }, 480 | }, ret) 481 | 482 | // external zone 483 | ret, err = Query("udp", addr, "newdns.256dpi.com.", "A", func(msg *dns.Msg) { 484 | msg.RecursionDesired = true 485 | }) 486 | assert.NoError(t, err) 487 | ret.Answer[0].(*dns.A).Hdr.Ttl = 300 488 | equalJSON(t, &dns.Msg{ 489 | MsgHdr: dns.MsgHdr{ 490 | Response: true, 491 | Authoritative: false, 492 | RecursionDesired: true, 493 | RecursionAvailable: true, 494 | }, 495 | Question: []dns.Question{ 496 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 497 | }, 498 | Answer: []dns.RR{ 499 | &dns.A{ 500 | Hdr: dns.RR_Header{ 501 | Name: "newdns.256dpi.com.", 502 | Rrtype: dns.TypeA, 503 | Class: dns.ClassINET, 504 | Ttl: 300, 505 | Rdlength: 4, 506 | }, 507 | A: net.ParseIP("1.2.3.4"), 508 | }, 509 | }, 510 | }, ret) 511 | }) 512 | } 513 | 514 | func conformanceTests(t *testing.T, proto, addr string, local bool) { 515 | t.Run("ApexA", func(t *testing.T) { 516 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "A", nil) 517 | assert.NoError(t, err) 518 | equalJSON(t, &dns.Msg{ 519 | MsgHdr: dns.MsgHdr{ 520 | Response: true, 521 | Authoritative: true, 522 | }, 523 | Question: []dns.Question{ 524 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 525 | }, 526 | Answer: []dns.RR{ 527 | &dns.A{ 528 | Hdr: dns.RR_Header{ 529 | Name: "newdns.256dpi.com.", 530 | Rrtype: dns.TypeA, 531 | Class: dns.ClassINET, 532 | Ttl: 300, 533 | Rdlength: 4, 534 | }, 535 | A: net.ParseIP("1.2.3.4"), 536 | }, 537 | }, 538 | Ns: nsRRs, 539 | }, ret) 540 | }) 541 | 542 | t.Run("ApexAAAA", func(t *testing.T) { 543 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "AAAA", nil) 544 | assert.NoError(t, err) 545 | equalJSON(t, &dns.Msg{ 546 | MsgHdr: dns.MsgHdr{ 547 | Response: true, 548 | Authoritative: true, 549 | }, 550 | Question: []dns.Question{ 551 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, 552 | }, 553 | Answer: []dns.RR{ 554 | &dns.AAAA{ 555 | Hdr: dns.RR_Header{ 556 | Name: "newdns.256dpi.com.", 557 | Rrtype: dns.TypeAAAA, 558 | Class: dns.ClassINET, 559 | Ttl: 300, 560 | Rdlength: 16, 561 | }, 562 | AAAA: net.ParseIP("1:2:3:4::"), 563 | }, 564 | }, 565 | Ns: nsRRs, 566 | }, ret) 567 | }) 568 | 569 | t.Run("ApexCNAME", func(t *testing.T) { 570 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "CNAME", nil) 571 | assert.NoError(t, err) 572 | equalJSON(t, &dns.Msg{ 573 | MsgHdr: dns.MsgHdr{ 574 | Response: true, 575 | Authoritative: true, 576 | Rcode: dns.RcodeSuccess, 577 | }, 578 | Question: []dns.Question{ 579 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeCNAME, Qclass: dns.ClassINET}, 580 | }, 581 | Ns: []dns.RR{ 582 | &dns.SOA{ 583 | Hdr: dns.RR_Header{ 584 | Name: "newdns.256dpi.com.", 585 | Rrtype: dns.TypeSOA, 586 | Class: dns.ClassINET, 587 | Ttl: 900, 588 | Rdlength: 66, 589 | }, 590 | Ns: awsPrimaryNS, 591 | Mbox: "awsdns-hostmaster.amazon.com.", 592 | Serial: 1, 593 | Refresh: 7200, 594 | Retry: 900, 595 | Expire: 1209600, 596 | Minttl: 300, 597 | }, 598 | }, 599 | }, ret) 600 | }) 601 | 602 | t.Run("ApexSOA", func(t *testing.T) { 603 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "SOA", nil) 604 | assert.NoError(t, err) 605 | equalJSON(t, &dns.Msg{ 606 | MsgHdr: dns.MsgHdr{ 607 | Response: true, 608 | Authoritative: true, 609 | }, 610 | Question: []dns.Question{ 611 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeSOA, Qclass: dns.ClassINET}, 612 | }, 613 | Answer: []dns.RR{ 614 | &dns.SOA{ 615 | Hdr: dns.RR_Header{ 616 | Name: "newdns.256dpi.com.", 617 | Rrtype: dns.TypeSOA, 618 | Class: dns.ClassINET, 619 | Ttl: 900, 620 | Rdlength: 66, 621 | }, 622 | Ns: awsPrimaryNS, 623 | Mbox: "awsdns-hostmaster.amazon.com.", 624 | Serial: 1, 625 | Refresh: 7200, 626 | Retry: 900, 627 | Expire: 1209600, 628 | Minttl: 300, 629 | }, 630 | }, 631 | Ns: []dns.RR{ 632 | &dns.NS{ 633 | Hdr: dns.RR_Header{ 634 | Name: "newdns.256dpi.com.", 635 | Rrtype: dns.TypeNS, 636 | Class: dns.ClassINET, 637 | Ttl: 172800, 638 | Rdlength: 23, 639 | }, 640 | Ns: awsNS[0], 641 | }, 642 | &dns.NS{ 643 | Hdr: dns.RR_Header{ 644 | Name: "newdns.256dpi.com.", 645 | Rrtype: dns.TypeNS, 646 | Class: dns.ClassINET, 647 | Ttl: 172800, 648 | Rdlength: 2, 649 | }, 650 | Ns: awsNS[1], 651 | }, 652 | &dns.NS{ 653 | Hdr: dns.RR_Header{ 654 | Name: "newdns.256dpi.com.", 655 | Rrtype: dns.TypeNS, 656 | Class: dns.ClassINET, 657 | Ttl: 172800, 658 | Rdlength: 25, 659 | }, 660 | Ns: awsNS[2], 661 | }, 662 | &dns.NS{ 663 | Hdr: dns.RR_Header{ 664 | Name: "newdns.256dpi.com.", 665 | Rrtype: dns.TypeNS, 666 | Class: dns.ClassINET, 667 | Ttl: 172800, 668 | Rdlength: 22, 669 | }, 670 | Ns: awsNS[3], 671 | }, 672 | }, 673 | }, ret) 674 | }) 675 | 676 | t.Run("ApexNS", func(t *testing.T) { 677 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "NS", nil) 678 | assert.NoError(t, err) 679 | equalJSON(t, &dns.Msg{ 680 | MsgHdr: dns.MsgHdr{ 681 | Response: true, 682 | Authoritative: true, 683 | }, 684 | Question: []dns.Question{ 685 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeNS, Qclass: dns.ClassINET}, 686 | }, 687 | Answer: nsRRs, 688 | }, ret) 689 | }) 690 | 691 | t.Run("ApexTXT", func(t *testing.T) { 692 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "TXT", nil) 693 | assert.NoError(t, err) 694 | equalJSON(t, &dns.Msg{ 695 | MsgHdr: dns.MsgHdr{ 696 | Response: true, 697 | Authoritative: true, 698 | }, 699 | Question: []dns.Question{ 700 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}, 701 | }, 702 | Answer: []dns.RR{ 703 | &dns.TXT{ 704 | Hdr: dns.RR_Header{ 705 | Name: "newdns.256dpi.com.", 706 | Rrtype: dns.TypeTXT, 707 | Class: dns.ClassINET, 708 | Ttl: 300, 709 | Rdlength: 4, 710 | }, 711 | Txt: []string{"baz"}, 712 | }, 713 | &dns.TXT{ 714 | Hdr: dns.RR_Header{ 715 | Name: "newdns.256dpi.com.", 716 | Rrtype: dns.TypeTXT, 717 | Class: dns.ClassINET, 718 | Ttl: 300, 719 | Rdlength: 8, 720 | }, 721 | Txt: []string{"foo", "bar"}, 722 | }, 723 | }, 724 | Ns: nsRRs, 725 | }, ret) 726 | }) 727 | 728 | t.Run("SubA", func(t *testing.T) { 729 | ret, err := Query(proto, addr, "ip4.newdns.256dpi.com.", "A", nil) 730 | assert.NoError(t, err) 731 | equalJSON(t, &dns.Msg{ 732 | MsgHdr: dns.MsgHdr{ 733 | Response: true, 734 | Authoritative: true, 735 | }, 736 | Question: []dns.Question{ 737 | {Name: "ip4.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 738 | }, 739 | Answer: []dns.RR{ 740 | &dns.A{ 741 | Hdr: dns.RR_Header{ 742 | Name: "ip4.newdns.256dpi.com.", 743 | Rrtype: dns.TypeA, 744 | Class: dns.ClassINET, 745 | Ttl: 300, 746 | Rdlength: 4, 747 | }, 748 | A: net.ParseIP("1.2.3.4"), 749 | }, 750 | }, 751 | Ns: nsRRs, 752 | }, ret) 753 | }) 754 | 755 | t.Run("SubAAAA", func(t *testing.T) { 756 | ret, err := Query(proto, addr, "ip6.newdns.256dpi.com.", "AAAA", nil) 757 | assert.NoError(t, err) 758 | equalJSON(t, &dns.Msg{ 759 | MsgHdr: dns.MsgHdr{ 760 | Response: true, 761 | Authoritative: true, 762 | }, 763 | Question: []dns.Question{ 764 | {Name: "ip6.newdns.256dpi.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, 765 | }, 766 | Answer: []dns.RR{ 767 | &dns.AAAA{ 768 | Hdr: dns.RR_Header{ 769 | Name: "ip6.newdns.256dpi.com.", 770 | Rrtype: dns.TypeAAAA, 771 | Class: dns.ClassINET, 772 | Ttl: 300, 773 | Rdlength: 16, 774 | }, 775 | AAAA: net.ParseIP("1:2:3:4::"), 776 | }, 777 | }, 778 | Ns: nsRRs, 779 | }, ret) 780 | }) 781 | 782 | t.Run("SubCNAME", func(t *testing.T) { 783 | ret, err := Query(proto, addr, "example.newdns.256dpi.com.", "CNAME", nil) 784 | assert.NoError(t, err) 785 | equalJSON(t, &dns.Msg{ 786 | MsgHdr: dns.MsgHdr{ 787 | Response: true, 788 | Authoritative: true, 789 | }, 790 | Question: []dns.Question{ 791 | {Name: "example.newdns.256dpi.com.", Qtype: dns.TypeCNAME, Qclass: dns.ClassINET}, 792 | }, 793 | Answer: []dns.RR{ 794 | &dns.CNAME{ 795 | Hdr: dns.RR_Header{ 796 | Name: "example.newdns.256dpi.com.", 797 | Rrtype: dns.TypeCNAME, 798 | Class: dns.ClassINET, 799 | Ttl: 300, 800 | Rdlength: 10, 801 | }, 802 | Target: "example.com.", 803 | }, 804 | }, 805 | Ns: nsRRs, 806 | }, ret) 807 | }) 808 | 809 | t.Run("SubMX", func(t *testing.T) { 810 | ret, err := Query(proto, addr, "mail.newdns.256dpi.com.", "MX", nil) 811 | assert.NoError(t, err) 812 | equalJSON(t, &dns.Msg{ 813 | MsgHdr: dns.MsgHdr{ 814 | Response: true, 815 | Authoritative: true, 816 | }, 817 | Question: []dns.Question{ 818 | {Name: "mail.newdns.256dpi.com.", Qtype: dns.TypeMX, Qclass: dns.ClassINET}, 819 | }, 820 | Answer: []dns.RR{ 821 | &dns.MX{ 822 | Hdr: dns.RR_Header{ 823 | Name: "mail.newdns.256dpi.com.", 824 | Rrtype: dns.TypeMX, 825 | Class: dns.ClassINET, 826 | Ttl: 300, 827 | Rdlength: 17, 828 | }, 829 | Mx: "mail.example.com.", 830 | Preference: 7, 831 | }, 832 | }, 833 | Ns: nsRRs, 834 | }, ret) 835 | }) 836 | 837 | t.Run("SubMultiMX", func(t *testing.T) { 838 | ret, err := Query(proto, addr, "multimail.newdns.256dpi.com.", "MX", nil) 839 | assert.NoError(t, err) 840 | equalJSON(t, &dns.Msg{ 841 | MsgHdr: dns.MsgHdr{ 842 | Response: true, 843 | Authoritative: true, 844 | }, 845 | Question: []dns.Question{ 846 | {Name: "multimail.newdns.256dpi.com.", Qtype: dns.TypeMX, Qclass: dns.ClassINET}, 847 | }, 848 | Answer: []dns.RR{ 849 | &dns.MX{ 850 | Hdr: dns.RR_Header{ 851 | Name: "multimail.newdns.256dpi.com.", 852 | Rrtype: dns.TypeMX, 853 | Class: dns.ClassINET, 854 | Ttl: 300, 855 | Rdlength: 18, 856 | }, 857 | Mx: "mail1.example.com.", 858 | Preference: 1, 859 | }, 860 | &dns.MX{ 861 | Hdr: dns.RR_Header{ 862 | Name: "multimail.newdns.256dpi.com.", 863 | Rrtype: dns.TypeMX, 864 | Class: dns.ClassINET, 865 | Ttl: 300, 866 | Rdlength: 10, 867 | }, 868 | Mx: "mail2.example.com.", 869 | Preference: 10, 870 | }, 871 | &dns.MX{ 872 | Hdr: dns.RR_Header{ 873 | Name: "multimail.newdns.256dpi.com.", 874 | Rrtype: dns.TypeMX, 875 | Class: dns.ClassINET, 876 | Ttl: 300, 877 | Rdlength: 10, 878 | }, 879 | Mx: "mail3.example.com.", 880 | Preference: 10, 881 | }, 882 | }, 883 | Ns: nsRRs, 884 | }, ret) 885 | }) 886 | 887 | t.Run("SubTXT", func(t *testing.T) { 888 | ret, err := Query(proto, addr, "text.newdns.256dpi.com.", "TXT", nil) 889 | assert.NoError(t, err) 890 | equalJSON(t, &dns.Msg{ 891 | MsgHdr: dns.MsgHdr{ 892 | Response: true, 893 | Authoritative: true, 894 | }, 895 | Question: []dns.Question{ 896 | {Name: "text.newdns.256dpi.com.", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}, 897 | }, 898 | Answer: []dns.RR{ 899 | &dns.TXT{ 900 | Hdr: dns.RR_Header{ 901 | Name: "text.newdns.256dpi.com.", 902 | Rrtype: dns.TypeTXT, 903 | Class: dns.ClassINET, 904 | Ttl: 300, 905 | Rdlength: 8, 906 | }, 907 | Txt: []string{"foo", "bar"}, 908 | }, 909 | }, 910 | Ns: nsRRs, 911 | }, ret) 912 | }) 913 | 914 | t.Run("SubCNAMEForA", func(t *testing.T) { 915 | ret, err := Query(proto, addr, "example.newdns.256dpi.com.", "A", nil) 916 | assert.NoError(t, err) 917 | equalJSON(t, &dns.Msg{ 918 | MsgHdr: dns.MsgHdr{ 919 | Response: true, 920 | Authoritative: true, 921 | }, 922 | Question: []dns.Question{ 923 | {Name: "example.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 924 | }, 925 | Answer: []dns.RR{ 926 | &dns.CNAME{ 927 | Hdr: dns.RR_Header{ 928 | Name: "example.newdns.256dpi.com.", 929 | Rrtype: dns.TypeCNAME, 930 | Class: dns.ClassINET, 931 | Ttl: 300, 932 | Rdlength: 10, 933 | }, 934 | Target: "example.com.", 935 | }, 936 | }, 937 | Ns: nsRRs, 938 | }, ret) 939 | }) 940 | 941 | t.Run("SubCNAMEForAAAA", func(t *testing.T) { 942 | ret, err := Query(proto, addr, "example.newdns.256dpi.com.", "AAAA", nil) 943 | assert.NoError(t, err) 944 | equalJSON(t, &dns.Msg{ 945 | MsgHdr: dns.MsgHdr{ 946 | Response: true, 947 | Authoritative: true, 948 | }, 949 | Question: []dns.Question{ 950 | {Name: "example.newdns.256dpi.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, 951 | }, 952 | Answer: []dns.RR{ 953 | &dns.CNAME{ 954 | Hdr: dns.RR_Header{ 955 | Name: "example.newdns.256dpi.com.", 956 | Rrtype: dns.TypeCNAME, 957 | Class: dns.ClassINET, 958 | Ttl: 300, 959 | Rdlength: 10, 960 | }, 961 | Target: "example.com.", 962 | }, 963 | }, 964 | Ns: nsRRs, 965 | }, ret) 966 | }) 967 | 968 | t.Run("SubCNAMEForAWithA", func(t *testing.T) { 969 | ret, err := Query(proto, addr, "ref4.newdns.256dpi.com.", "A", nil) 970 | assert.NoError(t, err) 971 | equalJSON(t, &dns.Msg{ 972 | MsgHdr: dns.MsgHdr{ 973 | Response: true, 974 | Authoritative: true, 975 | }, 976 | Question: []dns.Question{ 977 | {Name: "ref4.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 978 | }, 979 | Answer: []dns.RR{ 980 | &dns.CNAME{ 981 | Hdr: dns.RR_Header{ 982 | Name: "ref4.newdns.256dpi.com.", 983 | Rrtype: dns.TypeCNAME, 984 | Class: dns.ClassINET, 985 | Ttl: 300, 986 | Rdlength: 6, 987 | }, 988 | Target: "ip4.newdns.256dpi.com.", 989 | }, 990 | &dns.A{ 991 | Hdr: dns.RR_Header{ 992 | Name: "ip4.newdns.256dpi.com.", 993 | Rrtype: dns.TypeA, 994 | Class: dns.ClassINET, 995 | Ttl: 300, 996 | Rdlength: 4, 997 | }, 998 | A: net.ParseIP("1.2.3.4"), 999 | }, 1000 | }, 1001 | Ns: nsRRs, 1002 | }, ret) 1003 | }) 1004 | 1005 | t.Run("SubCNAMEForAAAAWithAAAA", func(t *testing.T) { 1006 | ret, err := Query(proto, addr, "ref6.newdns.256dpi.com.", "AAAA", nil) 1007 | assert.NoError(t, err) 1008 | equalJSON(t, &dns.Msg{ 1009 | MsgHdr: dns.MsgHdr{ 1010 | Response: true, 1011 | Authoritative: true, 1012 | }, 1013 | Question: []dns.Question{ 1014 | {Name: "ref6.newdns.256dpi.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, 1015 | }, 1016 | Answer: []dns.RR{ 1017 | &dns.CNAME{ 1018 | Hdr: dns.RR_Header{ 1019 | Name: "ref6.newdns.256dpi.com.", 1020 | Rrtype: dns.TypeCNAME, 1021 | Class: dns.ClassINET, 1022 | Ttl: 300, 1023 | Rdlength: 6, 1024 | }, 1025 | Target: "ip6.newdns.256dpi.com.", 1026 | }, 1027 | &dns.AAAA{ 1028 | Hdr: dns.RR_Header{ 1029 | Name: "ip6.newdns.256dpi.com.", 1030 | Rrtype: dns.TypeAAAA, 1031 | Class: dns.ClassINET, 1032 | Ttl: 300, 1033 | Rdlength: 16, 1034 | }, 1035 | AAAA: net.ParseIP("1:2:3:4::"), 1036 | }, 1037 | }, 1038 | Ns: nsRRs, 1039 | }, ret) 1040 | }) 1041 | 1042 | t.Run("SubCNAMEWithoutA", func(t *testing.T) { 1043 | ret, err := Query(proto, addr, "ref4.newdns.256dpi.com.", "CNAME", nil) 1044 | assert.NoError(t, err) 1045 | equalJSON(t, &dns.Msg{ 1046 | MsgHdr: dns.MsgHdr{ 1047 | Response: true, 1048 | Authoritative: true, 1049 | }, 1050 | Question: []dns.Question{ 1051 | {Name: "ref4.newdns.256dpi.com.", Qtype: dns.TypeCNAME, Qclass: dns.ClassINET}, 1052 | }, 1053 | Answer: []dns.RR{ 1054 | &dns.CNAME{ 1055 | Hdr: dns.RR_Header{ 1056 | Name: "ref4.newdns.256dpi.com.", 1057 | Rrtype: dns.TypeCNAME, 1058 | Class: dns.ClassINET, 1059 | Ttl: 300, 1060 | Rdlength: 6, 1061 | }, 1062 | Target: "ip4.newdns.256dpi.com.", 1063 | }, 1064 | }, 1065 | Ns: nsRRs, 1066 | }, ret) 1067 | }) 1068 | 1069 | t.Run("SubCNAMEWithoutAAAA", func(t *testing.T) { 1070 | ret, err := Query(proto, addr, "ref6.newdns.256dpi.com.", "CNAME", nil) 1071 | assert.NoError(t, err) 1072 | equalJSON(t, &dns.Msg{ 1073 | MsgHdr: dns.MsgHdr{ 1074 | Response: true, 1075 | Authoritative: true, 1076 | }, 1077 | Question: []dns.Question{ 1078 | {Name: "ref6.newdns.256dpi.com.", Qtype: dns.TypeCNAME, Qclass: dns.ClassINET}, 1079 | }, 1080 | Answer: []dns.RR{ 1081 | &dns.CNAME{ 1082 | Hdr: dns.RR_Header{ 1083 | Name: "ref6.newdns.256dpi.com.", 1084 | Rrtype: dns.TypeCNAME, 1085 | Class: dns.ClassINET, 1086 | Ttl: 300, 1087 | Rdlength: 6, 1088 | }, 1089 | Target: "ip6.newdns.256dpi.com.", 1090 | }, 1091 | }, 1092 | Ns: nsRRs, 1093 | }, ret) 1094 | }) 1095 | 1096 | t.Run("SubCNAMEForCNAMEForAWithA", func(t *testing.T) { 1097 | ret, err := Query(proto, addr, "refref.newdns.256dpi.com.", "A", nil) 1098 | assert.NoError(t, err) 1099 | equalJSON(t, &dns.Msg{ 1100 | MsgHdr: dns.MsgHdr{ 1101 | Response: true, 1102 | Authoritative: true, 1103 | }, 1104 | Question: []dns.Question{ 1105 | {Name: "refref.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 1106 | }, 1107 | Answer: []dns.RR{ 1108 | &dns.CNAME{ 1109 | Hdr: dns.RR_Header{ 1110 | Name: "refref.newdns.256dpi.com.", 1111 | Rrtype: dns.TypeCNAME, 1112 | Class: dns.ClassINET, 1113 | Ttl: 300, 1114 | Rdlength: 7, 1115 | }, 1116 | Target: "ref4.newdns.256dpi.com.", 1117 | }, 1118 | &dns.CNAME{ 1119 | Hdr: dns.RR_Header{ 1120 | Name: "ref4.newdns.256dpi.com.", 1121 | Rrtype: dns.TypeCNAME, 1122 | Class: dns.ClassINET, 1123 | Ttl: 300, 1124 | Rdlength: 6, 1125 | }, 1126 | Target: "ip4.newdns.256dpi.com.", 1127 | }, 1128 | &dns.A{ 1129 | Hdr: dns.RR_Header{ 1130 | Name: "ip4.newdns.256dpi.com.", 1131 | Rrtype: dns.TypeA, 1132 | Class: dns.ClassINET, 1133 | Ttl: 300, 1134 | Rdlength: 4, 1135 | }, 1136 | A: net.ParseIP("1.2.3.4"), 1137 | }, 1138 | }, 1139 | Ns: nsRRs, 1140 | }, ret) 1141 | }) 1142 | 1143 | t.Run("SubAAAAForCNAMEToA", func(t *testing.T) { 1144 | ret, err := Query(proto, addr, "ref4.newdns.256dpi.com.", "AAAA", nil) 1145 | assert.NoError(t, err) 1146 | equalJSON(t, &dns.Msg{ 1147 | MsgHdr: dns.MsgHdr{ 1148 | Response: true, 1149 | Authoritative: true, 1150 | }, 1151 | Question: []dns.Question{ 1152 | {Name: "ref4.newdns.256dpi.com.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}, 1153 | }, 1154 | Answer: []dns.RR{ 1155 | &dns.CNAME{ 1156 | Hdr: dns.RR_Header{ 1157 | Name: "ref4.newdns.256dpi.com.", 1158 | Rrtype: dns.TypeCNAME, 1159 | Class: dns.ClassINET, 1160 | Ttl: 300, 1161 | Rdlength: 6, 1162 | }, 1163 | Target: "ip4.newdns.256dpi.com.", 1164 | }, 1165 | }, 1166 | Ns: nsRRs, 1167 | }, ret) 1168 | }) 1169 | 1170 | t.Run("SubAForCNAMEToAAAA", func(t *testing.T) { 1171 | ret, err := Query(proto, addr, "ref6.newdns.256dpi.com.", "A", nil) 1172 | assert.NoError(t, err) 1173 | equalJSON(t, &dns.Msg{ 1174 | MsgHdr: dns.MsgHdr{ 1175 | Response: true, 1176 | Authoritative: true, 1177 | }, 1178 | Question: []dns.Question{ 1179 | {Name: "ref6.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 1180 | }, 1181 | Answer: []dns.RR{ 1182 | &dns.CNAME{ 1183 | Hdr: dns.RR_Header{ 1184 | Name: "ref6.newdns.256dpi.com.", 1185 | Rrtype: dns.TypeCNAME, 1186 | Class: dns.ClassINET, 1187 | Ttl: 300, 1188 | Rdlength: 6, 1189 | }, 1190 | Target: "ip6.newdns.256dpi.com.", 1191 | }, 1192 | }, 1193 | Ns: nsRRs, 1194 | }, ret) 1195 | }) 1196 | 1197 | t.Run("SubSRVForCNAME", func(t *testing.T) { 1198 | ret, err := Query(proto, addr, "ref4.newdns.256dpi.com.", "SRV", nil) 1199 | assert.NoError(t, err) 1200 | equalJSON(t, &dns.Msg{ 1201 | MsgHdr: dns.MsgHdr{ 1202 | Response: true, 1203 | Authoritative: true, 1204 | }, 1205 | Question: []dns.Question{ 1206 | {Name: "ref4.newdns.256dpi.com.", Qtype: dns.TypeSRV, Qclass: dns.ClassINET}, 1207 | }, 1208 | Answer: []dns.RR{ 1209 | &dns.CNAME{ 1210 | Hdr: dns.RR_Header{ 1211 | Name: "ref4.newdns.256dpi.com.", 1212 | Rrtype: dns.TypeCNAME, 1213 | Class: dns.ClassINET, 1214 | Ttl: 300, 1215 | Rdlength: 6, 1216 | }, 1217 | Target: "ip4.newdns.256dpi.com.", 1218 | }, 1219 | }, 1220 | Ns: nsRRs, 1221 | }, ret) 1222 | }) 1223 | 1224 | t.Run("SubMXWithExtraA", func(t *testing.T) { 1225 | ret, err := Query(proto, addr, "ref4m.newdns.256dpi.com.", "MX", nil) 1226 | assert.NoError(t, err) 1227 | equalJSON(t, &dns.Msg{ 1228 | MsgHdr: dns.MsgHdr{ 1229 | Response: true, 1230 | Authoritative: true, 1231 | }, 1232 | Question: []dns.Question{ 1233 | {Name: "ref4m.newdns.256dpi.com.", Qtype: dns.TypeMX, Qclass: dns.ClassINET}, 1234 | }, 1235 | Answer: []dns.RR{ 1236 | &dns.MX{ 1237 | Hdr: dns.RR_Header{ 1238 | Name: "ref4m.newdns.256dpi.com.", 1239 | Rrtype: dns.TypeMX, 1240 | Class: dns.ClassINET, 1241 | Ttl: 300, 1242 | Rdlength: 8, 1243 | }, 1244 | Preference: 7, 1245 | Mx: "ip4.newdns.256dpi.com.", 1246 | }, 1247 | }, 1248 | Extra: []dns.RR{ 1249 | &dns.A{ 1250 | Hdr: dns.RR_Header{ 1251 | Name: "ip4.newdns.256dpi.com.", 1252 | Rrtype: dns.TypeA, 1253 | Class: dns.ClassINET, 1254 | Ttl: 300, 1255 | Rdlength: 4, 1256 | }, 1257 | A: net.ParseIP("1.2.3.4"), 1258 | }, 1259 | }, 1260 | Ns: nsRRs, 1261 | }, ret) 1262 | }) 1263 | 1264 | t.Run("SubMXWithExtraAAAA", func(t *testing.T) { 1265 | ret, err := Query(proto, addr, "ref6m.newdns.256dpi.com.", "MX", nil) 1266 | assert.NoError(t, err) 1267 | equalJSON(t, &dns.Msg{ 1268 | MsgHdr: dns.MsgHdr{ 1269 | Response: true, 1270 | Authoritative: true, 1271 | }, 1272 | Question: []dns.Question{ 1273 | {Name: "ref6m.newdns.256dpi.com.", Qtype: dns.TypeMX, Qclass: dns.ClassINET}, 1274 | }, 1275 | Answer: []dns.RR{ 1276 | &dns.MX{ 1277 | Hdr: dns.RR_Header{ 1278 | Name: "ref6m.newdns.256dpi.com.", 1279 | Rrtype: dns.TypeMX, 1280 | Class: dns.ClassINET, 1281 | Ttl: 300, 1282 | Rdlength: 8, 1283 | }, 1284 | Preference: 7, 1285 | Mx: "ip6.newdns.256dpi.com.", 1286 | }, 1287 | }, 1288 | Extra: []dns.RR{ 1289 | &dns.AAAA{ 1290 | Hdr: dns.RR_Header{ 1291 | Name: "ip6.newdns.256dpi.com.", 1292 | Rrtype: dns.TypeAAAA, 1293 | Class: dns.ClassINET, 1294 | Ttl: 300, 1295 | Rdlength: 16, 1296 | }, 1297 | AAAA: net.ParseIP("1:2:3:4::"), 1298 | }, 1299 | }, 1300 | Ns: nsRRs, 1301 | }, ret) 1302 | }) 1303 | 1304 | t.Run("SubNS", func(t *testing.T) { 1305 | ret, err := Query(proto, addr, "other.newdns.256dpi.com.", "NS", nil) 1306 | assert.NoError(t, err) 1307 | order(ret.Ns) 1308 | equalJSON(t, &dns.Msg{ 1309 | MsgHdr: dns.MsgHdr{ 1310 | Response: true, 1311 | Authoritative: false, 1312 | }, 1313 | Question: []dns.Question{ 1314 | {Name: "other.newdns.256dpi.com.", Qtype: dns.TypeNS, Qclass: dns.ClassINET}, 1315 | }, 1316 | Ns: order(otherNSRRs), 1317 | }, ret) 1318 | }) 1319 | 1320 | t.Run("NoExactRecord", func(t *testing.T) { 1321 | assertMissing(t, proto, addr, "ip4.newdns.256dpi.com.", "CNAME", dns.RcodeSuccess) 1322 | assertMissing(t, proto, addr, "ip6.newdns.256dpi.com.", "CNAME", dns.RcodeSuccess) 1323 | assertMissing(t, proto, addr, "ip4.newdns.256dpi.com.", "AAAA", dns.RcodeSuccess) 1324 | assertMissing(t, proto, addr, "ip6.newdns.256dpi.com.", "A", dns.RcodeSuccess) 1325 | assertMissing(t, proto, addr, "mail.newdns.256dpi.com.", "A", dns.RcodeSuccess) 1326 | assertMissing(t, proto, addr, "text.newdns.256dpi.com.", "A", dns.RcodeSuccess) 1327 | assertMissing(t, proto, addr, "ip4.newdns.256dpi.com.", "NS", dns.RcodeSuccess) 1328 | // unsupported types 1329 | assertMissing(t, proto, addr, "ip4.newdns.256dpi.com.", "SRV", dns.RcodeSuccess) 1330 | assertMissing(t, proto, addr, "ip6.newdns.256dpi.com.", "SRV", dns.RcodeSuccess) 1331 | assertMissing(t, proto, addr, "ip4.newdns.256dpi.com.", "PTR", dns.RcodeSuccess) 1332 | assertMissing(t, proto, addr, "ip6.newdns.256dpi.com.", "PTR", dns.RcodeSuccess) 1333 | }) 1334 | 1335 | t.Run("NoExistingRecord", func(t *testing.T) { 1336 | assertMissing(t, proto, addr, "missing.newdns.256dpi.com.", "A", dns.RcodeNameError) 1337 | assertMissing(t, proto, addr, "missing.newdns.256dpi.com.", "AAAA", dns.RcodeNameError) 1338 | assertMissing(t, proto, addr, "missing.newdns.256dpi.com.", "CNAME", dns.RcodeNameError) 1339 | assertMissing(t, proto, addr, "missing.newdns.256dpi.com.", "MX", dns.RcodeNameError) 1340 | assertMissing(t, proto, addr, "missing.newdns.256dpi.com.", "TXT", dns.RcodeNameError) 1341 | assertMissing(t, proto, addr, "missing.newdns.256dpi.com.", "NS", dns.RcodeNameError) 1342 | // unsupported types 1343 | assertMissing(t, proto, addr, "missing.newdns.256dpi.com.", "SRV", dns.RcodeNameError) 1344 | assertMissing(t, proto, addr, "missing.newdns.256dpi.com.", "PTR", dns.RcodeNameError) 1345 | }) 1346 | 1347 | t.Run("TruncatedResponse", func(t *testing.T) { 1348 | ret, err := Query(proto, addr, "long.newdns.256dpi.com.", "TXT", nil) 1349 | assert.NoError(t, err) 1350 | 1351 | if proto == "udp" { 1352 | equalJSON(t, &dns.Msg{ 1353 | MsgHdr: dns.MsgHdr{ 1354 | Response: true, 1355 | Authoritative: true, 1356 | Truncated: true, 1357 | }, 1358 | Question: []dns.Question{ 1359 | {Name: "long.newdns.256dpi.com.", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}, 1360 | }, 1361 | }, ret) 1362 | } else { 1363 | equalJSON(t, &dns.Msg{ 1364 | MsgHdr: dns.MsgHdr{ 1365 | Response: true, 1366 | Authoritative: true, 1367 | }, 1368 | Question: []dns.Question{ 1369 | {Name: "long.newdns.256dpi.com.", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}, 1370 | }, 1371 | Answer: []dns.RR{ 1372 | &dns.TXT{ 1373 | Hdr: dns.RR_Header{ 1374 | Name: "long.newdns.256dpi.com.", 1375 | Rrtype: dns.TypeTXT, 1376 | Class: dns.ClassINET, 1377 | Ttl: 300, 1378 | Rdlength: 256, 1379 | }, 1380 | Txt: []string{ 1381 | "gyK4oL9X8Zn3b6TwmUIYAgQx43rBOWMqJWR3wGMGNaZgajnhd2u9JaIbGwNo6gzZunyKYRxID3mKLmYUCcIrNYuo8R4UkijZeshwqEAM2EWnjNsB1hJHOlu6VyRKW13rsFUJedOSqc7YjjUoxm9c3mF28tEXmc3GVsC476wJ2ciSbp7ujDjQ032SQRD6kpayzFX8GncS5KXP8mLK2ZIqK2U4fUmYEpTPQMmp7w24GKkfGJzE4JfMBxSybDUScLq", 1382 | }, 1383 | }, 1384 | &dns.TXT{ 1385 | Hdr: dns.RR_Header{ 1386 | Name: "long.newdns.256dpi.com.", 1387 | Rrtype: dns.TypeTXT, 1388 | Class: dns.ClassINET, 1389 | Ttl: 300, 1390 | Rdlength: 256, 1391 | }, 1392 | Txt: []string{ 1393 | "upNh05zi9flqN2puI9eIGgAgl3gwc65l3WjFdnE3u55dhyUyIoKbOlc1mQJPULPkn1V5TTG9rLBB8AzNfeL8jvwO8h0mzmJhPH8n6dkgI546jB8Z0g0MRJxN5VNSixjFjdR8vtUp6EWlVi7QSe9SYInghV0M17zZ8mXSHwTfYZaPH54ng22mSWzVbRX2tlUPLTNRB5CHrEtxliyhhQlRey98P5G0eo35FUXdqzOSJ3HGqDssBWQAxK3I9feOjbE", 1394 | }, 1395 | }, 1396 | &dns.TXT{ 1397 | Hdr: dns.RR_Header{ 1398 | Name: "long.newdns.256dpi.com.", 1399 | Rrtype: dns.TypeTXT, 1400 | Class: dns.ClassINET, 1401 | Ttl: 300, 1402 | Rdlength: 256, 1403 | }, 1404 | Txt: []string{ 1405 | "z4e6ycRMp6MP3WvWQMxIAOXglxANbj3oB0xD8BffktO4eo3VCR0s6TyGHKixvarOFJU0fqNkXeFOeI7sTXH5X0iXZukfLgnGTxLXNC7KkVFwtVFsh1P0IUNXtNBlOVWrVbxkS62ezbLpENNkiBwbkCvcTjwF2kyI0curAt9JhhJFb3AAq0q1iHWlJLn1KSrev9PIsY3alndDKjYTPxAojxzGKdK3A7rWLJ8Uzb3Z5OhLwP7jTKqbWVUocJRFLYp", 1406 | }, 1407 | }, 1408 | }, 1409 | Ns: nsRRs, 1410 | }, ret) 1411 | } 1412 | }) 1413 | 1414 | t.Run("CaseTransfer", func(t *testing.T) { 1415 | ret, err := Query(proto, addr, "Ip4.NeWDnS.256dpi.com.", "A", nil) 1416 | assert.NoError(t, err) 1417 | equalJSON(t, &dns.Msg{ 1418 | MsgHdr: dns.MsgHdr{ 1419 | Response: true, 1420 | Authoritative: true, 1421 | }, 1422 | Question: []dns.Question{ 1423 | {Name: "Ip4.NeWDnS.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 1424 | }, 1425 | Answer: []dns.RR{ 1426 | &dns.A{ 1427 | Hdr: dns.RR_Header{ 1428 | Name: "Ip4.NeWDnS.256dpi.com.", 1429 | Rrtype: dns.TypeA, 1430 | Class: dns.ClassINET, 1431 | Ttl: 300, 1432 | Rdlength: 4, 1433 | }, 1434 | A: net.ParseIP("1.2.3.4"), 1435 | }, 1436 | }, 1437 | Ns: []dns.RR{ 1438 | &dns.NS{ 1439 | Hdr: dns.RR_Header{ 1440 | Name: "NeWDnS.256dpi.com.", 1441 | Rrtype: dns.TypeNS, 1442 | Class: dns.ClassINET, 1443 | Ttl: 172800, 1444 | Rdlength: 23, 1445 | }, 1446 | Ns: awsNS[0], 1447 | }, 1448 | &dns.NS{ 1449 | Hdr: dns.RR_Header{ 1450 | Name: "NeWDnS.256dpi.com.", 1451 | Rrtype: dns.TypeNS, 1452 | Class: dns.ClassINET, 1453 | Ttl: 172800, 1454 | Rdlength: 19, 1455 | }, 1456 | Ns: awsNS[1], 1457 | }, 1458 | &dns.NS{ 1459 | Hdr: dns.RR_Header{ 1460 | Name: "NeWDnS.256dpi.com.", 1461 | Rrtype: dns.TypeNS, 1462 | Class: dns.ClassINET, 1463 | Ttl: 172800, 1464 | Rdlength: 25, 1465 | }, 1466 | Ns: awsNS[2], 1467 | }, 1468 | &dns.NS{ 1469 | Hdr: dns.RR_Header{ 1470 | Name: "NeWDnS.256dpi.com.", 1471 | Rrtype: dns.TypeNS, 1472 | Class: dns.ClassINET, 1473 | Ttl: 172800, 1474 | Rdlength: 22, 1475 | }, 1476 | Ns: awsNS[3], 1477 | }, 1478 | }, 1479 | }, ret) 1480 | }) 1481 | 1482 | t.Run("DomainWithSpace", func(t *testing.T) { 1483 | assertMissing(t, proto, addr, "\\ ip4.newdns.256dpi.com.", "NULL", dns.RcodeNameError) 1484 | }) 1485 | 1486 | t.Run("EDNSSuccess", func(t *testing.T) { 1487 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "A", func(msg *dns.Msg) { 1488 | msg.SetEdns0(1337, false) 1489 | }) 1490 | assert.NoError(t, err) 1491 | equalJSON(t, &dns.Msg{ 1492 | MsgHdr: dns.MsgHdr{ 1493 | Response: true, 1494 | Authoritative: true, 1495 | }, 1496 | Question: []dns.Question{ 1497 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 1498 | }, 1499 | Answer: []dns.RR{ 1500 | &dns.A{ 1501 | Hdr: dns.RR_Header{ 1502 | Name: "newdns.256dpi.com.", 1503 | Rrtype: dns.TypeA, 1504 | Class: dns.ClassINET, 1505 | Ttl: 300, 1506 | Rdlength: 4, 1507 | }, 1508 | A: net.ParseIP("1.2.3.4"), 1509 | }, 1510 | }, 1511 | Ns: nsRRs, 1512 | Extra: []dns.RR{ 1513 | &dns.OPT{ 1514 | Hdr: dns.RR_Header{ 1515 | Name: ".", 1516 | Rrtype: dns.TypeOPT, 1517 | Class: 4096, 1518 | Ttl: 0, 1519 | Rdlength: 0, 1520 | }, 1521 | }, 1522 | }, 1523 | }, ret) 1524 | }) 1525 | 1526 | t.Run("EDNSError", func(t *testing.T) { 1527 | ret, err := Query(proto, addr, "missing.newdns.256dpi.com.", "A", func(msg *dns.Msg) { 1528 | msg.SetEdns0(1337, false) 1529 | }) 1530 | assert.NoError(t, err) 1531 | equalJSON(t, &dns.Msg{ 1532 | MsgHdr: dns.MsgHdr{ 1533 | Response: true, 1534 | Authoritative: true, 1535 | Rcode: dns.RcodeNameError, 1536 | }, 1537 | Question: []dns.Question{ 1538 | {Name: "missing.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 1539 | }, 1540 | Ns: []dns.RR{ 1541 | &dns.SOA{ 1542 | Hdr: dns.RR_Header{ 1543 | Name: "newdns.256dpi.com.", 1544 | Rrtype: dns.TypeSOA, 1545 | Class: dns.ClassINET, 1546 | Ttl: 900, 1547 | Rdlength: 66, 1548 | }, 1549 | Ns: awsPrimaryNS, 1550 | Mbox: "awsdns-hostmaster.amazon.com.", 1551 | Serial: 1, 1552 | Refresh: 7200, 1553 | Retry: 900, 1554 | Expire: 1209600, 1555 | Minttl: 300, 1556 | }, 1557 | }, 1558 | Extra: []dns.RR{ 1559 | &dns.OPT{ 1560 | Hdr: dns.RR_Header{ 1561 | Name: ".", 1562 | Rrtype: dns.TypeOPT, 1563 | Class: 4096, 1564 | Ttl: 0, 1565 | Rdlength: 0, 1566 | }, 1567 | }, 1568 | }, 1569 | }, ret) 1570 | }) 1571 | 1572 | t.Run("EDNSBadVersion", func(t *testing.T) { 1573 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "A", func(msg *dns.Msg) { 1574 | msg.SetEdns0(1337, false) 1575 | msg.Extra[0].(*dns.OPT).SetVersion(2) 1576 | }) 1577 | assert.NoError(t, err) 1578 | equalJSON(t, &dns.Msg{ 1579 | MsgHdr: dns.MsgHdr{ 1580 | Response: true, 1581 | Authoritative: true, 1582 | Rcode: dns.RcodeBadVers, 1583 | }, 1584 | Question: []dns.Question{ 1585 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 1586 | }, 1587 | Extra: []dns.RR{ 1588 | &dns.OPT{ 1589 | Hdr: dns.RR_Header{ 1590 | Name: ".", 1591 | Rrtype: dns.TypeOPT, 1592 | Class: 4096, 1593 | Ttl: ret.Extra[0].Header().Ttl, // see below 1594 | Rdlength: 0, 1595 | }, 1596 | }, 1597 | }, 1598 | }, ret) 1599 | 1600 | // the AWS servers sometimes return a bit-flipped TTL value 1601 | ttl := ret.Extra[0].Header().Ttl 1602 | if !local { 1603 | assert.True(t, ttl == 0x1008000 || ttl == 0x1000000, ttl) 1604 | } else { 1605 | assert.Equal(t, uint32(dns.RcodeBadVers<<20), ttl) 1606 | } 1607 | }) 1608 | 1609 | t.Run("EDNSLongResponse", func(t *testing.T) { 1610 | ret, err := Query(proto, addr, "long.newdns.256dpi.com.", "TXT", func(msg *dns.Msg) { 1611 | msg.SetEdns0(1337, false) 1612 | }) 1613 | assert.NoError(t, err) 1614 | equalJSON(t, &dns.Msg{ 1615 | MsgHdr: dns.MsgHdr{ 1616 | Response: true, 1617 | Authoritative: true, 1618 | }, 1619 | Question: []dns.Question{ 1620 | {Name: "long.newdns.256dpi.com.", Qtype: dns.TypeTXT, Qclass: dns.ClassINET}, 1621 | }, 1622 | Answer: []dns.RR{ 1623 | &dns.TXT{ 1624 | Hdr: dns.RR_Header{ 1625 | Name: "long.newdns.256dpi.com.", 1626 | Rrtype: dns.TypeTXT, 1627 | Class: dns.ClassINET, 1628 | Ttl: 300, 1629 | Rdlength: 256, 1630 | }, 1631 | Txt: []string{ 1632 | "gyK4oL9X8Zn3b6TwmUIYAgQx43rBOWMqJWR3wGMGNaZgajnhd2u9JaIbGwNo6gzZunyKYRxID3mKLmYUCcIrNYuo8R4UkijZeshwqEAM2EWnjNsB1hJHOlu6VyRKW13rsFUJedOSqc7YjjUoxm9c3mF28tEXmc3GVsC476wJ2ciSbp7ujDjQ032SQRD6kpayzFX8GncS5KXP8mLK2ZIqK2U4fUmYEpTPQMmp7w24GKkfGJzE4JfMBxSybDUScLq", 1633 | }, 1634 | }, 1635 | &dns.TXT{ 1636 | Hdr: dns.RR_Header{ 1637 | Name: "long.newdns.256dpi.com.", 1638 | Rrtype: dns.TypeTXT, 1639 | Class: dns.ClassINET, 1640 | Ttl: 300, 1641 | Rdlength: 256, 1642 | }, 1643 | Txt: []string{ 1644 | "upNh05zi9flqN2puI9eIGgAgl3gwc65l3WjFdnE3u55dhyUyIoKbOlc1mQJPULPkn1V5TTG9rLBB8AzNfeL8jvwO8h0mzmJhPH8n6dkgI546jB8Z0g0MRJxN5VNSixjFjdR8vtUp6EWlVi7QSe9SYInghV0M17zZ8mXSHwTfYZaPH54ng22mSWzVbRX2tlUPLTNRB5CHrEtxliyhhQlRey98P5G0eo35FUXdqzOSJ3HGqDssBWQAxK3I9feOjbE", 1645 | }, 1646 | }, 1647 | &dns.TXT{ 1648 | Hdr: dns.RR_Header{ 1649 | Name: "long.newdns.256dpi.com.", 1650 | Rrtype: dns.TypeTXT, 1651 | Class: dns.ClassINET, 1652 | Ttl: 300, 1653 | Rdlength: 256, 1654 | }, 1655 | Txt: []string{ 1656 | "z4e6ycRMp6MP3WvWQMxIAOXglxANbj3oB0xD8BffktO4eo3VCR0s6TyGHKixvarOFJU0fqNkXeFOeI7sTXH5X0iXZukfLgnGTxLXNC7KkVFwtVFsh1P0IUNXtNBlOVWrVbxkS62ezbLpENNkiBwbkCvcTjwF2kyI0curAt9JhhJFb3AAq0q1iHWlJLn1KSrev9PIsY3alndDKjYTPxAojxzGKdK3A7rWLJ8Uzb3Z5OhLwP7jTKqbWVUocJRFLYp", 1657 | }, 1658 | }, 1659 | }, 1660 | Ns: nsRRs, 1661 | Extra: []dns.RR{ 1662 | &dns.OPT{ 1663 | Hdr: dns.RR_Header{ 1664 | Name: ".", 1665 | Rrtype: dns.TypeOPT, 1666 | Class: 4096, 1667 | Ttl: 0, 1668 | Rdlength: 0, 1669 | }, 1670 | }, 1671 | }, 1672 | }, ret) 1673 | }) 1674 | 1675 | t.Run("RecursionDesired", func(t *testing.T) { 1676 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "A", func(msg *dns.Msg) { 1677 | msg.RecursionDesired = true 1678 | }) 1679 | assert.NoError(t, err) 1680 | equalJSON(t, &dns.Msg{ 1681 | MsgHdr: dns.MsgHdr{ 1682 | Response: true, 1683 | Authoritative: true, 1684 | RecursionDesired: true, 1685 | }, 1686 | Question: []dns.Question{ 1687 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 1688 | }, 1689 | Answer: []dns.RR{ 1690 | &dns.A{ 1691 | Hdr: dns.RR_Header{ 1692 | Name: "newdns.256dpi.com.", 1693 | Rrtype: dns.TypeA, 1694 | Class: dns.ClassINET, 1695 | Ttl: 300, 1696 | Rdlength: 4, 1697 | }, 1698 | A: net.ParseIP("1.2.3.4"), 1699 | }, 1700 | }, 1701 | Ns: nsRRs, 1702 | }, ret) 1703 | }) 1704 | 1705 | t.Run("UnsupportedMessage", func(t *testing.T) { 1706 | _, err := Query(proto, addr, "newdns.256dpi.com.", "A", func(msg *dns.Msg) { 1707 | msg.Response = true 1708 | }) 1709 | assert.True(t, isIOError(err), err) 1710 | }) 1711 | 1712 | t.Run("UnsupportedOpcode", func(t *testing.T) { 1713 | _, err := Query(proto, addr, "newdns.256dpi.com.", "A", func(msg *dns.Msg) { 1714 | msg.Opcode = dns.OpcodeNotify 1715 | }) 1716 | assert.True(t, isIOError(err), err) 1717 | }) 1718 | 1719 | t.Run("UnsupportedClass", func(t *testing.T) { 1720 | _, err := Query(proto, addr, "newdns.256dpi.com.", "A", func(msg *dns.Msg) { 1721 | msg.Question[0].Qclass = dns.ClassANY 1722 | }) 1723 | assert.True(t, isIOError(err), err) 1724 | }) 1725 | 1726 | t.Run("IgnorePayload", func(t *testing.T) { 1727 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "A", func(msg *dns.Msg) { 1728 | msg.Answer = []dns.RR{ 1729 | &dns.A{ 1730 | Hdr: dns.RR_Header{ 1731 | Name: "newdns.256dpi.com.", 1732 | Rrtype: dns.TypeA, 1733 | Class: dns.ClassINET, 1734 | Ttl: 300, 1735 | Rdlength: 4, 1736 | }, 1737 | A: net.ParseIP("1.2.3.4"), 1738 | }, 1739 | &dns.AAAA{ 1740 | Hdr: dns.RR_Header{ 1741 | Name: "newdns.256dpi.com.", 1742 | Rrtype: dns.TypeAAAA, 1743 | Class: dns.ClassINET, 1744 | Ttl: 300, 1745 | Rdlength: 4, 1746 | }, 1747 | AAAA: net.ParseIP("1:2:3:4::"), 1748 | }, 1749 | } 1750 | msg.Ns = []dns.RR{ 1751 | nsRRs[0], 1752 | } 1753 | msg.Extra = []dns.RR{ 1754 | &dns.TXT{ 1755 | Hdr: dns.RR_Header{ 1756 | Name: "newdns.256dpi.com.", 1757 | Rrtype: dns.TypeTXT, 1758 | Class: dns.ClassINET, 1759 | Ttl: 300, 1760 | Rdlength: 4, 1761 | }, 1762 | Txt: []string{"baz"}, 1763 | }, 1764 | } 1765 | }) 1766 | assert.NoError(t, err) 1767 | equalJSON(t, &dns.Msg{ 1768 | MsgHdr: dns.MsgHdr{ 1769 | Response: true, 1770 | Authoritative: true, 1771 | }, 1772 | Question: []dns.Question{ 1773 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 1774 | }, 1775 | Answer: []dns.RR{ 1776 | &dns.A{ 1777 | Hdr: dns.RR_Header{ 1778 | Name: "newdns.256dpi.com.", 1779 | Rrtype: dns.TypeA, 1780 | Class: dns.ClassINET, 1781 | Ttl: 300, 1782 | Rdlength: 4, 1783 | }, 1784 | A: net.ParseIP("1.2.3.4"), 1785 | }, 1786 | }, 1787 | Ns: nsRRs, 1788 | }, ret) 1789 | }) 1790 | 1791 | t.Run("MultipleQuestions", func(t *testing.T) { 1792 | _, err := Query(proto, addr, "newdns.256dpi.com.", "A", func(msg *dns.Msg) { 1793 | msg.Question = append(msg.Question, dns.Question{ 1794 | Name: "newdns.256dpi.com.", 1795 | Qtype: dns.TypeA, 1796 | Qclass: dns.ClassINET, 1797 | }) 1798 | }) 1799 | assert.True(t, isIOError(err), err) 1800 | }) 1801 | 1802 | t.Run("UnsupportedType", func(t *testing.T) { 1803 | assertMissing(t, proto, addr, "missing.newdns.256dpi.com.", "NULL", dns.RcodeNameError) 1804 | }) 1805 | 1806 | t.Run("NonAuthoritativeZone", func(t *testing.T) { 1807 | ret, err := Query(proto, addr, "foo.256dpi.com.", "A", nil) 1808 | assert.NoError(t, err) 1809 | equalJSON(t, &dns.Msg{ 1810 | MsgHdr: dns.MsgHdr{ 1811 | Response: true, 1812 | Authoritative: false, 1813 | Rcode: dns.RcodeRefused, 1814 | }, 1815 | Question: []dns.Question{ 1816 | {Name: "foo.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, 1817 | }, 1818 | }, ret) 1819 | }) 1820 | } 1821 | 1822 | func additionalTests(t *testing.T, proto, addr string) { 1823 | t.Run("UnsupportedAny", func(t *testing.T) { 1824 | ret, err := Query(proto, addr, "newdns.256dpi.com.", "ANY", nil) 1825 | assert.NoError(t, err) 1826 | equalJSON(t, &dns.Msg{ 1827 | MsgHdr: dns.MsgHdr{ 1828 | Response: true, 1829 | Authoritative: true, 1830 | Rcode: dns.RcodeNotImplemented, 1831 | }, 1832 | Question: []dns.Question{ 1833 | {Name: "newdns.256dpi.com.", Qtype: dns.TypeANY, Qclass: dns.ClassINET}, 1834 | }, 1835 | }, ret) 1836 | }) 1837 | } 1838 | 1839 | func assertMissing(t *testing.T, proto, addr, name, typ string, code int) { 1840 | qt := dns.StringToType[typ] 1841 | 1842 | ret, err := Query(proto, addr, name, typ, nil) 1843 | assert.NoError(t, err) 1844 | equalJSON(t, &dns.Msg{ 1845 | MsgHdr: dns.MsgHdr{ 1846 | Response: true, 1847 | Authoritative: true, 1848 | Rcode: code, 1849 | }, 1850 | Question: []dns.Question{ 1851 | {Name: name, Qtype: qt, Qclass: dns.ClassINET}, 1852 | }, 1853 | Ns: []dns.RR{ 1854 | &dns.SOA{ 1855 | Hdr: dns.RR_Header{ 1856 | Name: "newdns.256dpi.com.", 1857 | Rrtype: dns.TypeSOA, 1858 | Class: dns.ClassINET, 1859 | Ttl: 900, 1860 | Rdlength: 66, 1861 | }, 1862 | Ns: awsPrimaryNS, 1863 | Mbox: "awsdns-hostmaster.amazon.com.", 1864 | Serial: 1, 1865 | Refresh: 7200, 1866 | Retry: 900, 1867 | Expire: 1209600, 1868 | Minttl: 300, 1869 | }, 1870 | }, 1871 | }, ret) 1872 | } 1873 | 1874 | func resolverTests(t *testing.T, fallback string) { 1875 | var dialer net.Dialer 1876 | 1877 | resolver := net.Resolver{ 1878 | PreferGo: true, 1879 | Dial: func(ctx context.Context, network, address string) (conn net.Conn, err error) { 1880 | return dialer.DialContext(ctx, network, fallback) 1881 | }, 1882 | } 1883 | 1884 | ctx := context.Background() 1885 | 1886 | t.Run("LookupHost", func(t *testing.T) { 1887 | addrs, err := resolver.LookupHost(ctx, "newdns.256dpi.com") 1888 | assert.NoError(t, err) 1889 | sort.Strings(addrs) 1890 | assert.Equal(t, []string{"1.2.3.4", "1:2:3:4::"}, addrs) 1891 | }) 1892 | 1893 | t.Run("LookupCNAME", func(t *testing.T) { 1894 | cname, err := resolver.LookupCNAME(ctx, "ref4.newdns.256dpi.com") 1895 | assert.NoError(t, err) 1896 | assert.Equal(t, "ip4.newdns.256dpi.com.", cname) 1897 | }) 1898 | 1899 | t.Run("LookupTXT", func(t *testing.T) { 1900 | txt, err := resolver.LookupTXT(ctx, "newdns.256dpi.com") 1901 | assert.NoError(t, err) 1902 | assert.Equal(t, []string{"baz", "foobar"}, txt) 1903 | }) 1904 | 1905 | t.Run("LookupMX", func(t *testing.T) { 1906 | mx, err := resolver.LookupMX(ctx, "ref4m.newdns.256dpi.com") 1907 | assert.NoError(t, err) 1908 | assert.Equal(t, []*net.MX{ 1909 | {Host: "ip4.newdns.256dpi.com.", Pref: 7}, 1910 | }, mx) 1911 | }) 1912 | 1913 | t.Run("LookupNS", func(t *testing.T) { 1914 | ns, err := resolver.LookupNS(ctx, "other.newdns.256dpi.com") 1915 | assert.Error(t, err) // zone is not served by server 1916 | assert.Nil(t, ns) 1917 | }) 1918 | } 1919 | -------------------------------------------------------------------------------- /set.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Set is a set of records. 9 | type Set struct { 10 | // The FQDN of the set. 11 | Name string 12 | 13 | // The type of the record. 14 | Type Type 15 | 16 | // The records in the set. 17 | Records []Record 18 | 19 | // The TTL of the set. 20 | // 21 | // Default: 5m. 22 | TTL time.Duration 23 | } 24 | 25 | // Validate will validate the set and ensure defaults. 26 | func (s *Set) Validate() error { 27 | // check name 28 | if !IsDomain(s.Name, true) { 29 | return fmt.Errorf("invalid name: %s", s.Name) 30 | } 31 | 32 | // check type 33 | if !s.Type.supported() { 34 | return fmt.Errorf("unsupported type: %d", s.Type) 35 | } 36 | 37 | // check records 38 | if len(s.Records) == 0 { 39 | return fmt.Errorf("missing records") 40 | } 41 | 42 | // check CNAME records 43 | if s.Type == CNAME && len(s.Records) > 1 { 44 | return fmt.Errorf("multiple CNAME records") 45 | } 46 | 47 | // validate records 48 | for _, record := range s.Records { 49 | err := record.Validate(s.Type) 50 | if err != nil { 51 | return fmt.Errorf("invalid record: %w", err) 52 | } 53 | } 54 | 55 | // check for duplicate addresses if not TXT 56 | if len(s.Records) > 1 && s.Type != TXT { 57 | for i := 0; i < len(s.Records)-1; i++ { 58 | if s.Records[i].Address == s.Records[i+1].Address { 59 | return fmt.Errorf("duplicate address: %s", s.Records[i].Address) 60 | } 61 | } 62 | } 63 | 64 | // set default ttl 65 | if s.TTL == 0 { 66 | s.TTL = 5 * time.Minute 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /set_test.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSetValidate(t *testing.T) { 10 | table := []struct { 11 | set Set 12 | err string 13 | }{ 14 | { 15 | set: Set{ 16 | Name: "foo", 17 | }, 18 | err: "invalid name: foo", 19 | }, 20 | { 21 | set: Set{ 22 | Name: "example.com.", 23 | }, 24 | err: "unsupported type: 0", 25 | }, 26 | { 27 | set: Set{ 28 | Name: "example.com.", 29 | Type: A, 30 | }, 31 | err: "missing records", 32 | }, 33 | { 34 | set: Set{ 35 | Name: "example.com.", 36 | Type: A, 37 | Records: []Record{ 38 | {Address: "foo"}, 39 | }, 40 | }, 41 | err: "invalid record: invalid IPv4 address: foo", 42 | }, 43 | { 44 | set: Set{ 45 | Name: "example.com.", 46 | Type: TXT, 47 | Records: []Record{ 48 | {}, 49 | }, 50 | }, 51 | err: "invalid record: missing data", 52 | }, 53 | { 54 | set: Set{ 55 | Name: "example.com.", 56 | Type: CNAME, 57 | Records: []Record{ 58 | {}, 59 | {}, 60 | }, 61 | }, 62 | err: "multiple CNAME records", 63 | }, 64 | { 65 | set: Set{ 66 | Name: "example.com.", 67 | Type: A, 68 | Records: []Record{ 69 | {Address: "1.2.3.4"}, 70 | {Address: "1.2.3.4"}, 71 | }, 72 | }, 73 | err: "duplicate address: 1.2.3.4", 74 | }, 75 | } 76 | 77 | for i, item := range table { 78 | err := item.set.Validate() 79 | if err != nil { 80 | assert.Equal(t, item.err, err.Error(), i) 81 | } else { 82 | assert.Equal(t, item.err, "", item) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "math" 5 | "strings" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | // IsDomain returns whether the name is a valid domain and if requested also 12 | // fully qualified. 13 | func IsDomain(name string, fqdn bool) bool { 14 | _, ok := dns.IsDomainName(name) 15 | return ok && (!fqdn || dns.IsFqdn(name)) 16 | } 17 | 18 | // InZone returns whether the provided name is part of the provided zone. Will 19 | // always return false if the provided domains are not valid. 20 | func InZone(zone, name string) bool { 21 | // check domains 22 | if !IsDomain(zone, false) || !IsDomain(name, false) { 23 | return false 24 | } 25 | 26 | return dns.IsSubDomain(zone, name) 27 | } 28 | 29 | // TrimZone will remove the zone from the specified name. 30 | func TrimZone(zone, name string) string { 31 | // return immediately if not in zone 32 | if !InZone(zone, name) { 33 | return name 34 | } 35 | 36 | // count zone labels 37 | count := dns.CountLabel(zone) 38 | 39 | // get segments 40 | labels := dns.SplitDomainName(name) 41 | 42 | // get new labels 43 | newLabels := labels[0 : len(labels)-count] 44 | 45 | // join name 46 | newName := strings.Join(newLabels, ".") 47 | 48 | return newName 49 | } 50 | 51 | // NormalizeDomain will normalize the provided domain name by removing space 52 | // around the name and lowercase it if requested. 53 | func NormalizeDomain(name string, lower, makeFQDN, removeFQDN bool) string { 54 | // remove spaces 55 | name = strings.TrimSpace(name) 56 | 57 | // lowercase if requested 58 | if lower { 59 | name = strings.ToLower(name) 60 | } 61 | 62 | // make FQDN if requested 63 | if makeFQDN { 64 | name = dns.Fqdn(name) 65 | } 66 | 67 | // remove FQDN if requested 68 | if removeFQDN && dns.IsFqdn(name) { 69 | name = name[:len(name)-1] 70 | } 71 | 72 | return name 73 | } 74 | 75 | // SplitDomain will split the provided domain either in separate labels or 76 | // hierarchical labels. The latter allows walking a domain up to the root. 77 | func SplitDomain(name string, hierarchical bool) []string { 78 | // normalize name 79 | name = NormalizeDomain(name, false, false, true) 80 | 81 | // return nil if empty 82 | if name == "" { 83 | return nil 84 | } 85 | 86 | // split in labels 87 | if !hierarchical { 88 | return dns.SplitDomainName(name) 89 | } 90 | 91 | // prepare list 92 | var list []string 93 | 94 | // walk domain 95 | for off, end := 0, false; !end; off, end = dns.NextLabel(name, off) { 96 | list = append(list, name[off:]) 97 | } 98 | 99 | return list 100 | } 101 | 102 | // TransferCase will transfer the case from the source name to the destination. 103 | // For the source "foo.AAA.com." and destination "aaa.com" the function will 104 | // return "AAA.com". The source must be either a child or the same as the 105 | // destination. 106 | func TransferCase(source, destination string) string { 107 | // get lower variants 108 | lowSource := strings.ToLower(source) 109 | lowDestination := strings.ToLower(destination) 110 | 111 | // get index of destination in source 112 | index := strings.Index(lowSource, lowDestination) 113 | if index < 0 { 114 | return destination 115 | } 116 | 117 | // take shared part from source 118 | return source[index:] 119 | } 120 | 121 | func emailToDomain(email string) string { 122 | // split on at 123 | parts := strings.Split(email, "@") 124 | 125 | // replace dots in username 126 | parts[0] = strings.ReplaceAll(parts[0], ".", "\\.") 127 | 128 | // join domain 129 | name := parts[0] + "." + parts[1] 130 | 131 | return dns.Fqdn(name) 132 | } 133 | 134 | func toSeconds(d time.Duration) uint32 { 135 | return uint32(math.Ceil(d.Seconds())) 136 | } 137 | -------------------------------------------------------------------------------- /tools_test.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIsDomain(t *testing.T) { 10 | assert.True(t, IsDomain("example.com", false)) 11 | assert.False(t, IsDomain("example.com", true)) 12 | assert.True(t, IsDomain("example.com.", true)) 13 | assert.True(t, IsDomain(" example.com.", true)) 14 | assert.False(t, IsDomain("", false)) 15 | assert.True(t, IsDomain("x", false)) 16 | assert.True(t, IsDomain(".", false)) 17 | } 18 | 19 | func TestInZone(t *testing.T) { 20 | assert.True(t, InZone("example.com.", "foo.example.com.")) 21 | assert.True(t, InZone("example.com", "foo.example.com")) 22 | assert.True(t, InZone("example.com", "example.com")) 23 | assert.True(t, InZone(".", "com")) 24 | assert.True(t, InZone(".", ".")) 25 | assert.False(t, InZone("", ".")) 26 | assert.False(t, InZone("", "")) 27 | assert.False(t, InZone("foo.example.com", "example.com")) 28 | } 29 | 30 | func TestTrimZone(t *testing.T) { 31 | assert.Equal(t, "foo", TrimZone("example.com.", "foo.example.com.")) 32 | assert.Equal(t, "foo", TrimZone("example.com", "foo.example.com")) 33 | assert.Equal(t, "", TrimZone("example.com", "example.com")) 34 | assert.Equal(t, "example.com", TrimZone("foo.example.com", "example.com")) 35 | } 36 | 37 | func TestNormalizeDomain(t *testing.T) { 38 | assert.Equal(t, "", NormalizeDomain("", false, false, false)) 39 | assert.Equal(t, ".", NormalizeDomain("", false, true, false)) 40 | assert.Equal(t, "foo", NormalizeDomain(" foo", false, false, false)) 41 | assert.Equal(t, "foo", NormalizeDomain("foo ", false, false, false)) 42 | assert.Equal(t, "foo", NormalizeDomain(" fOO ", true, false, false)) 43 | assert.Equal(t, "foo.", NormalizeDomain(" fOO ", true, true, false)) 44 | assert.Equal(t, "foo", NormalizeDomain(" fOO. ", true, false, true)) 45 | } 46 | 47 | func TestSplitDomain(t *testing.T) { 48 | assert.Equal(t, []string(nil), SplitDomain("", false)) 49 | assert.Equal(t, []string(nil), SplitDomain(".", false)) 50 | assert.Equal(t, []string{"foo"}, SplitDomain("foo", false)) 51 | assert.Equal(t, []string{"foo", "bar"}, SplitDomain("foo.bar", false)) 52 | 53 | assert.Equal(t, []string(nil), SplitDomain("", true)) 54 | assert.Equal(t, []string(nil), SplitDomain(".", true)) 55 | assert.Equal(t, []string{"foo"}, SplitDomain("foo", true)) 56 | assert.Equal(t, []string{"foo.bar", "bar"}, SplitDomain("foo.bar", true)) 57 | } 58 | 59 | func TestTransferCase(t *testing.T) { 60 | table := []struct { 61 | src string 62 | dst string 63 | out string 64 | }{ 65 | { 66 | src: "example.com", 67 | dst: "example.com", 68 | out: "example.com", 69 | }, 70 | { 71 | src: "EXAmple.com", 72 | dst: "example.com", 73 | out: "EXAmple.com", 74 | }, 75 | { 76 | src: "FOO.com", 77 | dst: "bar.com", 78 | out: "bar.com", 79 | }, 80 | { 81 | src: "foo.EXAmple.com", 82 | dst: "example.com", 83 | out: "EXAmple.com", 84 | }, 85 | { 86 | src: "foo.EXAmple.com", 87 | dst: "bar.example.com", 88 | out: "bar.example.com", 89 | }, 90 | } 91 | 92 | for i, item := range table { 93 | assert.Equal(t, item.out, TransferCase(item.src, item.dst), i) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import "github.com/miekg/dns" 4 | 5 | // Type denotes the DNS record type. 6 | type Type uint16 7 | 8 | const ( 9 | // A records return IPV4 addresses. 10 | A = Type(dns.TypeA) 11 | 12 | // AAAA records return IPV6 addresses. 13 | AAAA = Type(dns.TypeAAAA) 14 | 15 | // CNAME records return other DNS names. 16 | CNAME = Type(dns.TypeCNAME) 17 | 18 | // MX records return mails servers with their priorities. The target mail 19 | // servers must itself be returned with an A or AAAA record. 20 | MX = Type(dns.TypeMX) 21 | 22 | // TXT records return arbitrary text data. 23 | TXT = Type(dns.TypeTXT) 24 | 25 | // NS records delegate names to other name servers. 26 | NS = Type(dns.TypeNS) 27 | ) 28 | 29 | func (t Type) supported() bool { 30 | switch t { 31 | case A, AAAA, CNAME, MX, TXT, NS: 32 | return true 33 | default: 34 | return false 35 | } 36 | } 37 | 38 | func typeInList(list []Type, needle Type) bool { 39 | for _, t := range list { 40 | if t == needle { 41 | return true 42 | } 43 | } 44 | 45 | return false 46 | } 47 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "sort" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/miekg/dns" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func run(s *Server, addr string, fn func()) { 16 | defer s.Close() 17 | 18 | go func() { 19 | err := s.Run(addr) 20 | if err != nil { 21 | panic(err) 22 | } 23 | }() 24 | 25 | time.Sleep(100 * time.Millisecond) 26 | 27 | fn() 28 | } 29 | 30 | func serve(handler dns.Handler, addr string, fn func()) { 31 | closer := make(chan struct{}) 32 | defer close(closer) 33 | 34 | go func() { 35 | err := Run(addr, handler, Accept(nil), closer) 36 | if err != nil { 37 | panic(err) 38 | } 39 | }() 40 | 41 | time.Sleep(100 * time.Millisecond) 42 | 43 | fn() 44 | } 45 | 46 | func equalJSON(t *testing.T, a, b interface{}) { 47 | buf := new(bytes.Buffer) 48 | 49 | e := json.NewEncoder(buf) 50 | e.SetIndent("", " ") 51 | 52 | _ = e.Encode(a) 53 | aa := buf.String() 54 | 55 | buf.Reset() 56 | _ = e.Encode(b) 57 | bb := buf.String() 58 | 59 | assert.JSONEq(t, aa, bb) 60 | } 61 | 62 | func order(rrs []dns.RR) []dns.RR { 63 | cpy := make([]dns.RR, len(rrs)) 64 | copy(cpy, rrs) 65 | 66 | sort.Slice(cpy, func(i, j int) bool { 67 | var ai string 68 | switch rr := cpy[i].(type) { 69 | case *dns.NS: 70 | ai = rr.Ns 71 | rr.Hdr.Rdlength = 0 72 | } 73 | 74 | var aj string 75 | switch rr := cpy[j].(type) { 76 | case *dns.NS: 77 | aj = rr.Ns 78 | rr.Hdr.Rdlength = 0 79 | } 80 | 81 | return ai < aj 82 | }) 83 | 84 | return cpy 85 | } 86 | 87 | func isIOError(err error) bool { 88 | if err == nil { 89 | return false 90 | } 91 | 92 | if strings.Contains(err.Error(), "i/o timeout") { 93 | return true 94 | } 95 | 96 | if strings.Contains(err.Error(), "connection reset by peer") { 97 | return true 98 | } 99 | 100 | return false 101 | } 102 | -------------------------------------------------------------------------------- /zone.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // Zone describes a single authoritative DNS zone. 10 | type Zone struct { 11 | // The FQDN of the zone e.g. "example.com.". 12 | Name string 13 | 14 | // The FQDN of the master mame server responsible for this zone. The FQDN 15 | // must be returned as A and AAAA record by the parent zone. 16 | MasterNameServer string 17 | 18 | // A list of FQDNs to all authoritative name servers for this zone. The 19 | // FQDNs must be returned as A and AAAA records by the parent zone. It is 20 | // required to announce at least two distinct name servers per zone. 21 | AllNameServers []string 22 | 23 | // The email address of the administrator e.g. "hostmaster@example.com". 24 | // 25 | // Default: "hostmaster@NAME". 26 | AdminEmail string 27 | 28 | // The refresh interval. 29 | // 30 | // Default: 6h. 31 | Refresh time.Duration 32 | 33 | // The retry interval for the zone. 34 | // 35 | // Default: 1h. 36 | Retry time.Duration 37 | 38 | // The expiration interval of the zone. 39 | // 40 | // Default: 72h. 41 | Expire time.Duration 42 | 43 | // The TTL for the SOA record. 44 | // 45 | // Default: 15m. 46 | SOATTL time.Duration 47 | 48 | // The TTL for NS records. 49 | // 50 | // Default: 48h. 51 | NSTTL time.Duration 52 | 53 | // The minimum TTL for all records. Either this value, or the SOATTL if lower, 54 | // is used to determine the "negative caching TTL" which is the duration 55 | // caches are allowed to cache missing records (NXDOMAIN). 56 | // 57 | // Default: 5min. 58 | MinTTL time.Duration 59 | 60 | // The handler that responds to requests for this zone. The returned sets 61 | // must not be altered going forward. 62 | Handler func(name string) ([]Set, error) 63 | } 64 | 65 | // Validate will validate the zone and ensure the documented defaults. 66 | func (z *Zone) Validate() error { 67 | // check name 68 | if !IsDomain(z.Name, true) { 69 | return fmt.Errorf("name not fully qualified: %s", z.Name) 70 | } 71 | 72 | // check master name server 73 | if !IsDomain(z.MasterNameServer, true) { 74 | return fmt.Errorf("master server not full qualified: %s", z.MasterNameServer) 75 | } 76 | 77 | // check name server count 78 | if len(z.AllNameServers) < 1 { 79 | return fmt.Errorf("missing name servers") 80 | } 81 | 82 | // check name servers 83 | var includesMaster bool 84 | for _, ns := range z.AllNameServers { 85 | if !IsDomain(ns, true) { 86 | return fmt.Errorf("name server not fully qualified: %s", ns) 87 | } 88 | 89 | if ns == z.MasterNameServer { 90 | includesMaster = true 91 | } 92 | } 93 | 94 | // check master inclusion 95 | if !includesMaster { 96 | return fmt.Errorf("master name server not listed as name server: %s", z.MasterNameServer) 97 | } 98 | 99 | // set default admin email 100 | if z.AdminEmail == "" { 101 | z.AdminEmail = fmt.Sprintf("hostmaster@%s", z.Name) 102 | } 103 | 104 | // check admin email 105 | if !IsDomain(emailToDomain(z.AdminEmail), true) { 106 | return fmt.Errorf("admin email cannot be converted to a domain name: %s", z.AdminEmail) 107 | } 108 | 109 | // set default refresh 110 | if z.Refresh == 0 { 111 | z.Refresh = 6 * time.Hour 112 | } 113 | 114 | // set default retry 115 | if z.Retry == 0 { 116 | z.Retry = time.Hour 117 | } 118 | 119 | // set default expire 120 | if z.Expire == 0 { 121 | z.Expire = 72 * time.Hour 122 | } 123 | 124 | // set default SOA TTL 125 | if z.SOATTL == 0 { 126 | z.SOATTL = 15 * time.Minute 127 | } 128 | 129 | // set default NS TTL 130 | if z.NSTTL == 0 { 131 | z.NSTTL = 48 * time.Hour 132 | } 133 | 134 | // set default min TTL 135 | if z.MinTTL == 0 { 136 | z.MinTTL = 5 * time.Minute 137 | } 138 | 139 | // check retry 140 | if z.Retry >= z.Refresh { 141 | return fmt.Errorf("retry must be less than refresh: %d", z.Retry) 142 | } 143 | 144 | // check expire 145 | if z.Expire < z.Refresh+z.Retry { 146 | return fmt.Errorf("expire must be bigger than the sum of refresh and retry: %d", z.Expire) 147 | } 148 | 149 | return nil 150 | } 151 | 152 | // Lookup will lookup the specified name in the zone and return results for the 153 | // specified record types. If no results are returned, the second return value 154 | // indicates if there are other results for the specified name. 155 | func (z *Zone) Lookup(name string, needle ...Type) ([]Set, bool, error) { 156 | // check name 157 | if !IsDomain(name, true) { 158 | return nil, false, fmt.Errorf("invalid name: %s", name) 159 | } 160 | 161 | // normalize name 162 | name = NormalizeDomain(name, true, false, false) 163 | 164 | // check name 165 | if !InZone(z.Name, name) { 166 | return nil, false, fmt.Errorf("name does not belong to zone: %s", name) 167 | } 168 | 169 | // prepare result 170 | var result []Set 171 | 172 | for i := 0; ; i++ { 173 | // get sets 174 | sets, err := z.Handler(TrimZone(z.Name, name)) 175 | if err != nil { 176 | return nil, false, fmt.Errorf("zone handler error: %w", err) 177 | } 178 | 179 | // return immediately if initial set is empty 180 | if i == 0 && len(sets) == 0 { 181 | return nil, false, nil 182 | } 183 | 184 | // prepare counters 185 | counters := map[Type]int{ 186 | A: 0, 187 | AAAA: 0, 188 | CNAME: 0, 189 | MX: 0, 190 | TXT: 0, 191 | } 192 | 193 | // validate sets 194 | for _, set := range sets { 195 | // validate set 196 | err = set.Validate() 197 | if err != nil { 198 | return nil, false, fmt.Errorf("invalid set: %w", err) 199 | } 200 | 201 | // check relationship 202 | if !InZone(z.Name, set.Name) { 203 | return nil, false, fmt.Errorf("set does not belong to zone: %s", set.Name) 204 | } 205 | 206 | // increment counter 207 | counters[set.Type]++ 208 | } 209 | 210 | // check counters 211 | for _, counter := range counters { 212 | if counter > 1 { 213 | return nil, false, errors.New("multiple sets for same type") 214 | } 215 | } 216 | 217 | // check apex CNAME 218 | if counters[CNAME] > 0 && name == z.Name { 219 | return nil, false, fmt.Errorf("invalid CNAME set at apex: %s", name) 220 | } 221 | 222 | // check CNAME is stand-alone 223 | if counters[CNAME] > 0 && (len(sets) > 1) { 224 | return nil, false, fmt.Errorf("other sets with CNAME set: %s", name) 225 | } 226 | 227 | // check if CNAME and query is not CNAME 228 | if counters[CNAME] > 0 && !typeInList(needle, CNAME) { 229 | // add set to result 230 | result = append(result, sets[0]) 231 | 232 | // get normalized address 233 | address := NormalizeDomain(sets[0].Records[0].Address, true, false, false) 234 | 235 | // continue lookup with CNAME address if address is in zone 236 | if InZone(z.Name, address) { 237 | name = address 238 | continue 239 | } 240 | 241 | return result, false, nil 242 | } 243 | 244 | // add matching set 245 | for _, set := range sets { 246 | if typeInList(needle, set.Type) { 247 | // add set to result 248 | result = append(result, set) 249 | 250 | break 251 | } 252 | } 253 | 254 | // return if there are no matches, but indicate that there are sets 255 | // available for other types 256 | if len(result) == 0 { 257 | return nil, true, nil 258 | } 259 | 260 | return result, false, nil 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /zone_test.go: -------------------------------------------------------------------------------- 1 | package newdns 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestZoneValidate(t *testing.T) { 11 | table := []struct { 12 | zne Zone 13 | err string 14 | }{ 15 | { 16 | zne: Zone{ 17 | Name: "foo", 18 | }, 19 | err: "name not fully qualified: foo", 20 | }, 21 | { 22 | zne: Zone{ 23 | Name: "example.com.", 24 | MasterNameServer: "foo", 25 | }, 26 | err: "master server not full qualified: foo", 27 | }, 28 | { 29 | zne: Zone{ 30 | Name: "example.com.", 31 | MasterNameServer: "n1.example.com.", 32 | }, 33 | err: "missing name servers", 34 | }, 35 | { 36 | zne: Zone{ 37 | Name: "example.com.", 38 | MasterNameServer: "n1.example.com.", 39 | AllNameServers: []string{ 40 | "foo", 41 | }, 42 | }, 43 | err: "name server not fully qualified: foo", 44 | }, 45 | { 46 | zne: Zone{ 47 | Name: "example.com.", 48 | MasterNameServer: "n2.example.com.", 49 | AllNameServers: []string{ 50 | "n1.example.com.", 51 | }, 52 | }, 53 | err: "master name server not listed as name server: n2.example.com.", 54 | }, 55 | { 56 | zne: Zone{ 57 | Name: "example.com.", 58 | MasterNameServer: "n1.example.com.", 59 | AllNameServers: []string{ 60 | "n1.example.com.", 61 | }, 62 | }, 63 | }, 64 | { 65 | zne: Zone{ 66 | Name: "example.com.", 67 | MasterNameServer: "n1.example.com.", 68 | AllNameServers: []string{ 69 | "n1.example.com.", 70 | }, 71 | AdminEmail: "foo@bar..example.com", 72 | }, 73 | err: "admin email cannot be converted to a domain name: foo@bar..example.com", 74 | }, 75 | { 76 | zne: Zone{ 77 | Name: "example.com.", 78 | MasterNameServer: "n1.example.com.", 79 | AllNameServers: []string{ 80 | "n1.example.com.", 81 | }, 82 | Refresh: 1, 83 | Retry: 2, 84 | }, 85 | err: "retry must be less than refresh: 2", 86 | }, 87 | { 88 | zne: Zone{ 89 | Name: "example.com.", 90 | MasterNameServer: "n1.example.com.", 91 | AllNameServers: []string{ 92 | "n1.example.com.", 93 | }, 94 | Expire: 1, 95 | Retry: 2, 96 | }, 97 | err: "expire must be bigger than the sum of refresh and retry: 1", 98 | }, 99 | } 100 | 101 | for i, item := range table { 102 | err := item.zne.Validate() 103 | if err != nil { 104 | assert.EqualValues(t, item.err, err.Error(), i) 105 | } else { 106 | assert.Equal(t, item.err, "", item) 107 | } 108 | } 109 | } 110 | 111 | func TestZoneLookup(t *testing.T) { 112 | zone := Zone{ 113 | Name: "example.com.", 114 | MasterNameServer: "ns1.example.com.", 115 | AllNameServers: []string{ 116 | "ns1.example.com.", 117 | "ns2.example.com.", 118 | }, 119 | Handler: func(name string) ([]Set, error) { 120 | if name == "error" { 121 | return nil, io.EOF 122 | } 123 | 124 | if name == "invalid1" { 125 | return []Set{ 126 | {Name: "foo"}, 127 | }, nil 128 | } 129 | 130 | if name == "invalid2" { 131 | return []Set{ 132 | {Name: "foo.", Type: A, Records: []Record{{Address: "1.2.3.4"}}}, 133 | }, nil 134 | } 135 | 136 | if name == "multiple" { 137 | return []Set{ 138 | {Name: "foo.example.com.", Type: A, Records: []Record{{Address: "1.2.3.4"}}}, 139 | {Name: "foo.example.com.", Type: A, Records: []Record{{Address: "1.2.3.4"}}}, 140 | }, nil 141 | } 142 | 143 | if name == "" { 144 | return []Set{ 145 | {Name: "example.com.", Type: CNAME, Records: []Record{{Address: "cool.com."}}}, 146 | }, nil 147 | } 148 | 149 | if name == "cname" { 150 | return []Set{ 151 | {Name: "cname.example.com.", Type: A, Records: []Record{{Address: "1.2.3.4"}}}, 152 | {Name: "cname.example.com.", Type: CNAME, Records: []Record{{Address: "cool.com."}}}, 153 | }, nil 154 | } 155 | 156 | return nil, nil 157 | }, 158 | } 159 | 160 | err := zone.Validate() 161 | assert.NoError(t, err) 162 | 163 | table := []struct { 164 | name string 165 | err string 166 | }{ 167 | { 168 | name: "foo", 169 | err: "invalid name: foo", 170 | }, 171 | { 172 | name: "foo.", 173 | err: "name does not belong to zone: foo.", 174 | }, 175 | { 176 | name: "error.example.com.", 177 | err: "zone handler error: EOF", 178 | }, 179 | { 180 | name: "invalid1.example.com.", 181 | err: "invalid set: invalid name: foo", 182 | }, 183 | { 184 | name: "invalid2.example.com.", 185 | err: "set does not belong to zone: foo.", 186 | }, 187 | { 188 | name: "multiple.example.com.", 189 | err: "multiple sets for same type", 190 | }, 191 | { 192 | name: "example.com.", 193 | err: "invalid CNAME set at apex: example.com.", 194 | }, 195 | { 196 | name: "cname.example.com.", 197 | err: "other sets with CNAME set: cname.example.com.", 198 | }, 199 | } 200 | 201 | for i, item := range table { 202 | res, exists, err := zone.Lookup(item.name, A) 203 | assert.Equal(t, item.err, err.Error(), i) 204 | assert.False(t, exists, i) 205 | assert.Nil(t, res, i) 206 | } 207 | } 208 | --------------------------------------------------------------------------------