├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── resolve.go ├── resolve.py └── resolve.sh /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Julia Evans 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### tiny DNS resolver 2 | 3 | This is a command line program that makes DNS queries. There's a version in bash and a version in Go. 4 | 5 | 6 | ### how to run it 7 | 8 | The bash version: 9 | 10 | ``` 11 | bash resolve.sh example.com. 12 | ``` 13 | 14 | The go version: 15 | 16 | ``` 17 | go run resolve.go example.com. 18 | ``` 19 | 20 | The Python version 21 | 22 | ``` 23 | pip install dnspython 24 | python3 resolve.py example.com 25 | ``` 26 | 27 | ### other versions 28 | 29 | * Rust: https://github.com/kmkaplan/tiny-resolver-rs 30 | * Elixir: https://www.bortzmeyer.org/files/tiny-resolver.tar 31 | 32 | ### blog post 33 | 34 | You can read more about how this works in [A toy DNS resolver](https://jvns.ca/blog/2022/02/01/a-dns-resolver-in-80-lines-of-go/) 35 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jvns/resolve 2 | 3 | go 1.14 4 | 5 | require github.com/miekg/dns v1.1.43 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= 2 | github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= 3 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= 4 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 5 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 6 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 7 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g= 8 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 10 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 11 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 12 | -------------------------------------------------------------------------------- /resolve.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "strings" 8 | 9 | "github.com/miekg/dns" 10 | ) 11 | 12 | func resolve(name string) net.IP { 13 | // We always start with a root nameserver 14 | nameserver := net.ParseIP("198.41.0.4") 15 | for { 16 | reply := dnsQuery(name, nameserver) 17 | if ip := getAnswer(reply); ip != nil { 18 | // Best case: we get an answer to our query and we're done 19 | return ip 20 | } else if nsIP := getGlue(reply); nsIP != nil { 21 | // Second best: we get a "glue record" with the *IP address* of another nameserver to query 22 | nameserver = nsIP 23 | } else if domain := getNS(reply); domain != "" { 24 | // Third best: we get the *domain name* of another nameserver to query, which we can look up the IP for 25 | nameserver = resolve(domain) 26 | } else { 27 | // If there's no A record we just panic, this is not a very good 28 | // resolver :) 29 | panic("something went wrong") 30 | } 31 | } 32 | } 33 | 34 | func getAnswer(reply *dns.Msg) net.IP { 35 | for _, record := range reply.Answer { 36 | if record.Header().Rrtype == dns.TypeA { 37 | fmt.Println(" ", record) 38 | return record.(*dns.A).A 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | func getGlue(reply *dns.Msg) net.IP { 45 | for _, record := range reply.Extra { 46 | if record.Header().Rrtype == dns.TypeA { 47 | fmt.Println(" ", record) 48 | return record.(*dns.A).A 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | func getNS(reply *dns.Msg) string { 55 | for _, record := range reply.Ns { 56 | if record.Header().Rrtype == dns.TypeNS { 57 | fmt.Println(" ", record) 58 | return record.(*dns.NS).Ns 59 | } 60 | } 61 | return "" 62 | } 63 | 64 | func dnsQuery(name string, server net.IP) *dns.Msg { 65 | fmt.Printf("dig -r @%s %s\n", server.String(), name) 66 | msg := new(dns.Msg) 67 | msg.SetQuestion(name, dns.TypeA) 68 | c := new(dns.Client) 69 | reply, _, _ := c.Exchange(msg, server.String()+":53") 70 | return reply 71 | } 72 | 73 | func main() { 74 | name := os.Args[1] 75 | if !strings.HasSuffix(name, ".") { 76 | name = name + "." 77 | } 78 | fmt.Println("Result:", resolve(name)) 79 | } 80 | -------------------------------------------------------------------------------- /resolve.py: -------------------------------------------------------------------------------- 1 | import dns.message 2 | import dns.query 3 | import dns.rdatatype 4 | import sys 5 | 6 | def resolve(domain): 7 | # Start at the root nameserver 8 | nameserver = "198.41.0.4" 9 | while True: 10 | reply = query(domain, nameserver) 11 | ip = get_answer(reply) 12 | if ip: 13 | # Best case: we get an answer to our query and we're done 14 | return ip 15 | nameserver_ip = get_glue(reply) 16 | if nameserver_ip: 17 | # Second best: we get a "glue record" with the *IP address* of another nameserver to query 18 | nameserver = nameserver_ip 19 | else: 20 | # Otherwise: we get the *domain name* of another nameserver to query, which we can look up the IP for 21 | nameserver_domain = get_nameserver(reply) 22 | nameserver = resolve(nameserver_domain) 23 | 24 | def query(name, nameserver): 25 | query = dns.message.make_query(name, 'A') 26 | return dns.query.udp(query, nameserver) 27 | 28 | def get_answer(reply): 29 | for record in reply.answer: 30 | if record.rdtype == dns.rdatatype.A: 31 | return record[0].address 32 | 33 | def get_glue(reply): 34 | for record in reply.additional: 35 | if record.rdtype == dns.rdatatype.A: 36 | return record[0].address 37 | 38 | def get_nameserver(reply): 39 | for record in reply.authority: 40 | if record.rdtype == dns.rdatatype.NS: 41 | return record[0].target 42 | 43 | print(resolve(sys.argv[1])) 44 | -------------------------------------------------------------------------------- /resolve.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | get_record() { 4 | grep -E "\s$1\s" | head -n 1 | awk '{print $5}' 5 | } 6 | 7 | lookup () { 8 | echo dig -r "$@" >&2 9 | dig -r +norecurse +noall +authority +answer +additional "$@" 10 | } 11 | 12 | resolve() { 13 | DOMAIN="$1" 14 | # start with a `.` nameserver. That's easy. 15 | NAMESERVER="198.41.0.4" 16 | while true 17 | do 18 | RESPONSE=$(lookup @"$NAMESERVER" "$DOMAIN") 19 | IP=$(echo "$RESPONSE" | grep "$DOMAIN" | get_record "A" ) 20 | GLUEIP=$(echo "$RESPONSE" | get_record "A" | grep -v "$DOMAIN") 21 | NS=$(echo "$RESPONSE" | get_record "NS") 22 | if [ -n "$IP" ]; then 23 | echo "$IP" 24 | return 0 25 | elif [ -n "$GLUEIP" ]; then 26 | NAMESERVER="$GLUEIP" 27 | elif [ -n "$NS" ]; then 28 | NAMESERVER=$(resolve "$NS") 29 | else 30 | echo "No IP found for $DOMAIN" 31 | exit 1 32 | fi 33 | done 34 | } 35 | 36 | resolve "$1" 37 | --------------------------------------------------------------------------------