├── .github └── workflows │ └── go.yml ├── .vscode └── launch.json ├── CODEOWNERS ├── LICENSE ├── README.md ├── dnsserver-go.gif ├── dnsserver.go ├── lookupdb.go ├── names.json └── utils.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: go build -v ./... 34 | 35 | - name: Test 36 | run: go test -v ./... 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "env": {}, 14 | "args": [] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. Unless a later match takes precedence, 6 | * @dlorch 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, Daniel Lorch 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple DNS Server implemented in Go 2 | =================================== 3 | 4 | The Domain Name System (DNS) consists of multiple elements: Authoritative 5 | DNS Servers store and provide DNS record information, Recursive DNS servers 6 | (also referred to as caching DNS servers) are the "middlemen" that recursively 7 | look up information on behalf of an end-user. See 8 | [Authoritative vs. Recursive DNS Servers: What's The Difference] for an overview. 9 | 10 | This project provides a subset of the functionality of an **Authoritative 11 | DNS Server** as a study project. If you need a production-grade DNS Server in Go, 12 | have a look at [CoreDNS]. For DNS library support, see [Go DNS] or 13 | [package dnsmessage]. 14 | 15 | Featured on [r/golang] and [go-nuts]. 16 | 17 | ![Simple DNS Server implemented in Go](https://raw.githubusercontent.com/dlorch/dnsserver/master/dnsserver-go.gif) 18 | 19 | Run 20 | --- 21 | 22 | ``` 23 | $ go run . & 24 | Listening at: :1053 25 | 26 | $ dig example.com @localhost -p 1053 27 | Received request from [::1]:63282 28 | 29 | ; <<>> DiG 9.10.6 <<>> example.com @localhost -p 1053 30 | ;; global options: +cmd 31 | ;; Got answer: 32 | ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17060 33 | ;; flags: qr; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 34 | 35 | ;; QUESTION SECTION: 36 | ;example.com. IN A 37 | 38 | ;; ANSWER SECTION: 39 | example.com. 31337 IN A 3.1.3.7 40 | 41 | ;; Query time: 0 msec 42 | ;; SERVER: ::1#1053(::1) 43 | ;; WHEN: Mon Jun 17 17:02:43 CEST 2019 44 | ;; MSG SIZE rcvd: 56 45 | ``` 46 | 47 | Concepts 48 | -------- 49 | 50 | * Go structs and methods ([Structs Instead of Classes - OOP in Go]) 51 | * Goroutines ([Rob Pike - 'Concurrency Is Not Parallelism']) 52 | * Go slices (Go's dynamic lists) 53 | * Efficiently writing to and reading from structs using binary.Read() and binary.Write() respectively 54 | * DNS protocol ([RFC 1035: Domain Names - Implementation and Specification]) 55 | 56 | TODO 57 | ---- 58 | 59 | * Implement more record types (CNAME, MX, TXT, AAAA, ...) according to section 3.2.2. of [RFC 1035: Domain Names - Implementation and Specification] 60 | * Implement [DNS Message Compression] according to section 4.1.4. of [RFC 1035: Domain Names - Implementation and Specification] (thank you [knome] for pointing this out) 61 | 62 | Links 63 | ----- 64 | 65 | * [RFC 1035: Domain Names - Implementation and Specification] 66 | * [DNS Query Message Format] 67 | * [Wireshark] 68 | * [Structs Instead of Classes - OOP in Go] 69 | * [Rob Pike - 'Concurrency Is Not Parallelism'] 70 | 71 | [Authoritative vs. Recursive DNS Servers: What's The Difference]: http://social.dnsmadeeasy.com/blog/authoritative-vs-recursive-dns-servers-whats-the-difference/ 72 | [CoreDNS]: https://coredns.io/ 73 | [Go DNS]: https://github.com/miekg/dns 74 | [package dnsmessage]: https://godoc.org/golang.org/x/net/dns/dnsmessage 75 | [r/golang]: https://www.reddit.com/r/golang/comments/c3n7hl/simple_dns_server_implemented_in_go/ 76 | [go-nuts]: https://groups.google.com/d/msgid/golang-nuts/9d6801ae-5725-4152-83cf-33e63219da70%40googlegroups.com 77 | [DNS Message Compression]: http://www.tcpipguide.com/free/t_DNSNameNotationandMessageCompressionTechnique-2.htm 78 | [knome]: https://www.reddit.com/r/golang/comments/c3n7hl/simple_dns_server_implemented_in_go/erseh68?utm_source=share&utm_medium=web2x 79 | [RFC 1035: Domain Names - Implementation and Specification]: https://www.ietf.org/rfc/rfc1035.txt 80 | [DNS Query Message Format]: http://www.firewall.cx/networking-topics/protocols/domain-name-system-dns/160-protocols-dns-query.html 81 | [Wireshark]: https://www.wireshark.org/ 82 | [Structs Instead of Classes - OOP in Go]: https://golangbot.com/structs-instead-of-classes/ 83 | [Rob Pike - 'Concurrency Is Not Parallelism']: https://www.youtube.com/watch?v=cN_DpYBzKso 84 | -------------------------------------------------------------------------------- /dnsserver-go.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlorch/dnsserver/4e1eb1f7bfb85e7f46cfd355c284906f40c0b21d/dnsserver-go.gif -------------------------------------------------------------------------------- /dnsserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | Simple DNS Server implemented in Go 5 | 6 | BSD 2-Clause License 7 | 8 | Copyright (c) 2019, Daniel Lorch 9 | All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without 12 | modification, are permitted provided that the following conditions are met: 13 | 14 | 1. Redistributions of source code must retain the above copyright notice, this 15 | list of conditions and the following disclaimer. 16 | 17 | 2. Redistributions in binary form must reproduce the above copyright notice, 18 | this list of conditions and the following disclaimer in the documentation 19 | and/or other materials provided with the distribution. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | 33 | import ( 34 | "bytes" 35 | "encoding/binary" 36 | "fmt" 37 | "net" 38 | "os" 39 | "strings" 40 | ) 41 | 42 | // DNSHeader describes the request/response DNS header 43 | type DNSHeader struct { 44 | TransactionID uint16 45 | Flags uint16 46 | NumQuestions uint16 47 | NumAnswers uint16 48 | NumAuthorities uint16 49 | NumAdditionals uint16 50 | } 51 | 52 | // DNSResourceRecord describes individual records in the request and response of the DNS payload body 53 | type DNSResourceRecord struct { 54 | DomainName string 55 | Type uint16 56 | Class uint16 57 | TimeToLive uint32 58 | ResourceDataLength uint16 59 | ResourceData []byte 60 | } 61 | 62 | // Type and Class values for DNSResourceRecord 63 | const ( 64 | TypeA uint16 = 1 // a host address 65 | ClassINET uint16 = 1 // the Internet 66 | FlagResponse uint16 = 1 << 15 67 | UDPMaxMessageSizeBytes uint = 512 // RFC1035 68 | ) 69 | 70 | // Pretend to look up values in a database 71 | func dbLookup(queryResourceRecord DNSResourceRecord) ([]DNSResourceRecord, []DNSResourceRecord, []DNSResourceRecord) { 72 | var answerResourceRecords = make([]DNSResourceRecord, 0) 73 | var authorityResourceRecords = make([]DNSResourceRecord, 0) 74 | var additionalResourceRecords = make([]DNSResourceRecord, 0) 75 | 76 | names, err := GetNames() 77 | if err != nil { 78 | return answerResourceRecords, authorityResourceRecords, additionalResourceRecords 79 | } 80 | 81 | if queryResourceRecord.Type != TypeA || queryResourceRecord.Class != ClassINET { 82 | return answerResourceRecords, authorityResourceRecords, additionalResourceRecords 83 | } 84 | 85 | for _, name := range names { 86 | if strings.Contains(queryResourceRecord.DomainName, name.Name) { 87 | fmt.Println(queryResourceRecord.DomainName, "resolved to", name.Address) 88 | answerResourceRecords = append(answerResourceRecords, DNSResourceRecord{ 89 | DomainName: name.Name, 90 | Type: TypeA, 91 | Class: ClassINET, 92 | TimeToLive: 31337, 93 | ResourceData: name.Address[12:16], // ipv4 address 94 | ResourceDataLength: 4, 95 | }) 96 | } 97 | } 98 | 99 | return answerResourceRecords, authorityResourceRecords, additionalResourceRecords 100 | } 101 | 102 | // RFC1035: "Domain names in messages are expressed in terms of a sequence 103 | // of labels. Each label is represented as a one octet length field followed 104 | // by that number of octets. Since every domain name ends with the null label 105 | // of the root, a domain name is terminated by a length byte of zero." 106 | func readDomainName(requestBuffer *bytes.Buffer) (string, error) { 107 | var domainName string 108 | 109 | b, err := requestBuffer.ReadByte() 110 | 111 | for ; b != 0 && err == nil; b, err = requestBuffer.ReadByte() { 112 | labelLength := int(b) 113 | labelBytes := requestBuffer.Next(labelLength) 114 | labelName := string(labelBytes) 115 | 116 | if len(domainName) == 0 { 117 | domainName = labelName 118 | } else { 119 | domainName += "." + labelName 120 | } 121 | } 122 | 123 | return domainName, err 124 | } 125 | 126 | // RFC1035: "Domain names in messages are expressed in terms of a sequence 127 | // of labels. Each label is represented as a one octet length field followed 128 | // by that number of octets. Since every domain name ends with the null label 129 | // of the root, a domain name is terminated by a length byte of zero." 130 | func writeDomainName(responseBuffer *bytes.Buffer, domainName string) error { 131 | labels := strings.Split(domainName, ".") 132 | 133 | for _, label := range labels { 134 | labelLength := len(label) 135 | labelBytes := []byte(label) 136 | 137 | responseBuffer.WriteByte(byte(labelLength)) 138 | responseBuffer.Write(labelBytes) 139 | } 140 | 141 | err := responseBuffer.WriteByte(byte(0)) 142 | 143 | return err 144 | } 145 | 146 | func handleDNSClient(requestBytes []byte, serverConn *net.UDPConn, clientAddr *net.UDPAddr) { 147 | /** 148 | * read request 149 | */ 150 | var requestBuffer = bytes.NewBuffer(requestBytes) 151 | var queryHeader DNSHeader 152 | var queryResourceRecords []DNSResourceRecord 153 | 154 | err := binary.Read(requestBuffer, binary.BigEndian, &queryHeader) // network byte order is big endian 155 | 156 | if err != nil { 157 | fmt.Println("Error decoding header: ", err.Error()) 158 | } 159 | 160 | queryResourceRecords = make([]DNSResourceRecord, queryHeader.NumQuestions) 161 | 162 | for idx, _ := range queryResourceRecords { 163 | queryResourceRecords[idx].DomainName, err = readDomainName(requestBuffer) 164 | 165 | if err != nil { 166 | fmt.Println("Error decoding label: ", err.Error()) 167 | } 168 | 169 | queryResourceRecords[idx].Type = binary.BigEndian.Uint16(requestBuffer.Next(2)) 170 | queryResourceRecords[idx].Class = binary.BigEndian.Uint16(requestBuffer.Next(2)) 171 | } 172 | 173 | /** 174 | * lookup values 175 | */ 176 | var answerResourceRecords = make([]DNSResourceRecord, 0) 177 | var authorityResourceRecords = make([]DNSResourceRecord, 0) 178 | var additionalResourceRecords = make([]DNSResourceRecord, 0) 179 | 180 | for _, queryResourceRecord := range queryResourceRecords { 181 | newAnswerRR, newAuthorityRR, newAdditionalRR := dbLookup(queryResourceRecord) 182 | 183 | answerResourceRecords = append(answerResourceRecords, newAnswerRR...) // three dots cause the two lists to be concatenated 184 | authorityResourceRecords = append(authorityResourceRecords, newAuthorityRR...) 185 | additionalResourceRecords = append(additionalResourceRecords, newAdditionalRR...) 186 | } 187 | 188 | /** 189 | * write response 190 | */ 191 | var responseBuffer = new(bytes.Buffer) 192 | var responseHeader DNSHeader 193 | 194 | responseHeader = DNSHeader{ 195 | TransactionID: queryHeader.TransactionID, 196 | Flags: FlagResponse, 197 | NumQuestions: queryHeader.NumQuestions, 198 | NumAnswers: uint16(len(answerResourceRecords)), 199 | NumAuthorities: uint16(len(authorityResourceRecords)), 200 | NumAdditionals: uint16(len(additionalResourceRecords)), 201 | } 202 | 203 | err = Write(responseBuffer, &responseHeader) 204 | 205 | if err != nil { 206 | fmt.Println("Error writing to buffer: ", err.Error()) 207 | } 208 | 209 | for _, queryResourceRecord := range queryResourceRecords { 210 | err = writeDomainName(responseBuffer, queryResourceRecord.DomainName) 211 | 212 | if err != nil { 213 | fmt.Println("Error writing to buffer: ", err.Error()) 214 | } 215 | 216 | Write(responseBuffer, queryResourceRecord.Type) 217 | Write(responseBuffer, queryResourceRecord.Class) 218 | } 219 | 220 | for _, answerResourceRecord := range answerResourceRecords { 221 | err = writeDomainName(responseBuffer, answerResourceRecord.DomainName) 222 | 223 | if err != nil { 224 | fmt.Println("Error writing to buffer: ", err.Error()) 225 | } 226 | 227 | Write(responseBuffer, answerResourceRecord.Type) 228 | Write(responseBuffer, answerResourceRecord.Class) 229 | Write(responseBuffer, answerResourceRecord.TimeToLive) 230 | Write(responseBuffer, answerResourceRecord.ResourceDataLength) 231 | Write(responseBuffer, answerResourceRecord.ResourceData) 232 | } 233 | 234 | for _, authorityResourceRecord := range authorityResourceRecords { 235 | err = writeDomainName(responseBuffer, authorityResourceRecord.DomainName) 236 | 237 | if err != nil { 238 | fmt.Println("Error writing to buffer: ", err.Error()) 239 | } 240 | 241 | Write(responseBuffer, authorityResourceRecord.Type) 242 | Write(responseBuffer, authorityResourceRecord.Class) 243 | Write(responseBuffer, authorityResourceRecord.TimeToLive) 244 | Write(responseBuffer, authorityResourceRecord.ResourceDataLength) 245 | Write(responseBuffer, authorityResourceRecord.ResourceData) 246 | } 247 | 248 | for _, additionalResourceRecord := range additionalResourceRecords { 249 | err = writeDomainName(responseBuffer, additionalResourceRecord.DomainName) 250 | 251 | if err != nil { 252 | fmt.Println("Error writing to buffer: ", err.Error()) 253 | } 254 | 255 | Write(responseBuffer, additionalResourceRecord.Type) 256 | Write(responseBuffer, additionalResourceRecord.Class) 257 | Write(responseBuffer, additionalResourceRecord.TimeToLive) 258 | Write(responseBuffer, additionalResourceRecord.ResourceDataLength) 259 | Write(responseBuffer, additionalResourceRecord.ResourceData) 260 | } 261 | 262 | serverConn.WriteToUDP(responseBuffer.Bytes(), clientAddr) 263 | } 264 | 265 | func main() { 266 | serverAddr, err := net.ResolveUDPAddr("udp", ":1053") 267 | 268 | if err != nil { 269 | fmt.Println("Error resolving UDP address: ", err.Error()) 270 | os.Exit(1) 271 | } 272 | 273 | serverConn, err := net.ListenUDP("udp", serverAddr) 274 | 275 | if err != nil { 276 | fmt.Println("Error listening: ", err.Error()) 277 | os.Exit(1) 278 | } 279 | 280 | fmt.Println("Listening at: ", serverAddr) 281 | 282 | defer serverConn.Close() 283 | 284 | for { 285 | requestBytes := make([]byte, UDPMaxMessageSizeBytes) 286 | 287 | _, clientAddr, err := serverConn.ReadFromUDP(requestBytes) 288 | 289 | if err != nil { 290 | fmt.Println("Error receiving: ", err.Error()) 291 | } else { 292 | fmt.Println("Received request from ", clientAddr) 293 | go handleDNSClient(requestBytes, serverConn, clientAddr) // array is value type (call-by-value), i.e. copied 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /lookupdb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net" 8 | ) 9 | 10 | type NameModel struct { 11 | Name string `json:"name"` 12 | Address string `json:"address"` 13 | } 14 | 15 | type Name struct { 16 | Name string 17 | Address net.IP 18 | } 19 | 20 | func GetNames() ([]Name, error) { 21 | // read file 22 | data, err := ioutil.ReadFile("./names.json") 23 | if err != nil { 24 | fmt.Print(err) 25 | return nil, err 26 | } 27 | // json data 28 | var models []NameModel 29 | 30 | // unmarshall it 31 | err = json.Unmarshal(data, &models) 32 | if err != nil { 33 | fmt.Println("error:", err) 34 | return nil, err 35 | } 36 | 37 | return To(models), nil 38 | 39 | } 40 | 41 | func To(models []NameModel) []Name { 42 | names := make([]Name, 0, len(models)) 43 | for _, value := range models { 44 | names = append(names, Name{ 45 | Name: value.Name, 46 | Address: net.ParseIP(value.Address), 47 | }) 48 | } 49 | return names 50 | } 51 | -------------------------------------------------------------------------------- /names.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "example.com", 4 | "address": "3.1.3.7" 5 | }, 6 | { 7 | "name": "acint.net", 8 | "address": "192.168.0.102" 9 | } 10 | ] -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | func Write(w io.Writer, data interface{}) error { 9 | return binary.Write(w, binary.BigEndian, data) 10 | } 11 | --------------------------------------------------------------------------------