├── README.md ├── cmd └── dns-resolver │ └── main.go ├── go.mod ├── go.sum └── pkg └── dns ├── resolver.go └── resolver_test.go /README.md: -------------------------------------------------------------------------------- 1 | # DNS Server & Resolver 2 | 3 | This project is a DNS server and resolver implemented in the Go programming language. It provides the functionality to act as a DNS server, receiving DNS queries and responding with the appropriate DNS records. Additionally, it can function as a DNS resolver, allowing users to query external DNS servers for DNS records. 4 | 5 | ![ezgif com-gif-maker](https://github.com/Manthan109/dns/assets/42516515/d881ef8b-601b-4c06-b44e-d4a876893392) 6 | 7 | 8 | ## Features 9 | 10 | - DNS server: Accepts incoming DNS queries and responds with the appropriate DNS records. 11 | - DNS resolver: Sends DNS queries to external DNS servers and receives the corresponding DNS records. 12 | 13 | ## Prerequisites 14 | 15 | To use this project, you need to have the following installed: 16 | 17 | - Go (version 1.19) 18 | - Git 19 | 20 | ## Getting Started 21 | 22 | Follow the steps below to get started with the DNS server and resolver: 23 | 24 | 1. Clone the repository using Git: 25 | 26 | ```bash 27 | git clone https://github.com/Manthan109/dns.git 28 | ``` 29 | 30 | 2. Change to the project directory: 31 | 32 | ```bash 33 | cd dns 34 | ``` 35 | 36 | 3. Run the project: 37 | 38 | ```bash 39 | go run cmd/dns-resolver/main.go 40 | ``` 41 | 42 | ## Usage 43 | Open a new tab in the terminal and type 44 | ```bash 45 | dig @127.0.0.1 46 | ``` 47 | example 48 | ```bash 49 | dig @127.0.0.1 google.com 50 | ``` 51 | 52 | ## Project Notes 53 | This is a simple DNS server and resolver that works only on UDP for now and IPv4 addresses (TypeA records). All the other missed out features like caching have been done intentionally to reduce the scope of the project. 54 | 55 | ## DNS Working 56 | ![Untitled-2023-06-05-2230](https://github.com/Manthan109/dns/assets/42516515/f9a106d2-7d5d-4b2e-8ab8-e6e4e0cecf49) 57 | 58 | ## Logical Flow 59 | ![logical-flow](https://github.com/Manthan109/dns/assets/42516515/43edf0bd-3692-4661-b70d-400ed32d7993) 60 | 61 | ## References 62 | 1. https://amriunix.com/post/deep-dive-into-dns-messages/ 63 | 2. https://www.ques10.com/p/10908/explain-dns-message-format-with-neat-diagram-1/ 64 | 3. https://medium.com/@openmohan/dns-basics-and-building-simple-dns-server-in-go-6cb8e1cfe461 65 | 4. https://github.com/wardviaene/golang-for-devops-course 66 | 67 | -------------------------------------------------------------------------------- /cmd/dns-resolver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/Manthan109/dns/pkg/dns" 8 | ) 9 | 10 | func main() { 11 | fmt.Println("Starting DNS sever ...") 12 | packetConn, err := net.ListenPacket("udp", ":53") 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer packetConn.Close() 17 | 18 | for { 19 | buf := make([]byte, 512) 20 | bytesRead, addr, err := packetConn.ReadFrom(buf) 21 | if err != nil { 22 | fmt.Printf("Read error from %s: %s", addr.String(), err) 23 | continue 24 | } 25 | go dns.HandlePacket(packetConn, addr, buf[:bytesRead]) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Manthan109/dns 2 | 3 | go 1.19 4 | 5 | require golang.org/x/net v0.2.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= 2 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 3 | -------------------------------------------------------------------------------- /pkg/dns/resolver.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "bufio" 5 | "crypto/rand" 6 | "fmt" 7 | "math/big" 8 | "net" 9 | "strings" 10 | 11 | "golang.org/x/net/dns/dnsmessage" 12 | ) 13 | 14 | const ROOT_SERVERS = "198.41.0.4,199.9.14.201,192.33.4.12,199.7.91.13,192.203.230.10,192.5.5.241,192.112.36.4,198.97.190.53" 15 | 16 | func HandlePacket(pc net.PacketConn, addr net.Addr, buf []byte) { 17 | if err := handlePacket(pc, addr, buf); err != nil { 18 | fmt.Printf("handlePacket error %s: %s\n", addr.String(), err) 19 | } 20 | } 21 | 22 | func handlePacket(pc net.PacketConn, addr net.Addr, buf []byte) error { 23 | parse := dnsmessage.Parser{} 24 | header, err := parse.Start(buf) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | // Getting the question from the packet 30 | question, err := parse.Question() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | response, err := dnsQuery(getRootServers(), question) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | response.Header.ID = header.ID 41 | // Encoding the message 42 | responseBuffer, err := response.Pack() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // Writing back to the client 48 | _, err = pc.WriteTo(responseBuffer, addr) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func dnsQuery(servers []net.IP, question dnsmessage.Question) (*dnsmessage.Message, error) { 57 | fmt.Printf("Question: %+v\n", question) 58 | 59 | for i := 0; i < 3; i++ { 60 | // getting the answer and header from the server 61 | dnsAnswer, header, err := outgoingDnsQuery(servers, question) 62 | if err != nil { 63 | return nil, err 64 | } 65 | // parsing the answers 66 | parsedAnswers, err := dnsAnswer.AllAnswers() 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | // checking if we have received the final IP address of the question 72 | if header.Authoritative { 73 | return &dnsmessage.Message{ 74 | Header: dnsmessage.Header{Response: true}, 75 | Answers: parsedAnswers, 76 | }, nil 77 | } 78 | // getting the authorities attached to the answer packet 79 | authorities, err := dnsAnswer.AllAuthorities() 80 | if err != nil { 81 | return nil, err 82 | } 83 | if len(authorities) == 0 { 84 | return &dnsmessage.Message{ 85 | Header: dnsmessage.Header{RCode: dnsmessage.RCodeNameError}}, 86 | nil 87 | } 88 | 89 | // getting the nameservers from the authorities that could give us the IP address for the question 90 | nameservers := make([]string, len(authorities)) 91 | for k, authority := range authorities { 92 | if authority.Header.Type == dnsmessage.TypeNS { 93 | nameservers[k] = authority.Body.(*dnsmessage.NSResource).NS.String() 94 | } 95 | } 96 | 97 | // getting the additionals attached to the answer packet 98 | additionals, err := dnsAnswer.AllAdditionals() 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | // We try to find next set of ipv4 addresses to ask the question 104 | newResolverServersFound := false 105 | // We edit the server param given to dnsQuery function so in the next loop we have new set of 106 | // servers to ask the question 107 | servers = []net.IP{} 108 | for _, additional := range additionals { 109 | if additional.Header.Type == dnsmessage.TypeA { 110 | for _, nameserver := range nameservers { 111 | if additional.Header.Name.String() == nameserver { 112 | newResolverServersFound = true 113 | servers = append(servers, additional.Body.(*dnsmessage.AResource).A[:]) 114 | } 115 | } 116 | } 117 | } 118 | 119 | // if no new ipv4 address is found then we start querying for the nameserver as the question 120 | // and find its ipv4 address 121 | if !newResolverServersFound { 122 | for _, nameserver := range nameservers { 123 | if !newResolverServersFound { 124 | response, err := dnsQuery(getRootServers(), 125 | dnsmessage.Question{ 126 | Name: dnsmessage.MustNewName(nameserver), 127 | Type: dnsmessage.TypeA, 128 | Class: dnsmessage.ClassINET}) 129 | if err != nil { 130 | fmt.Printf("Warning: lookup of nameserver %s failed: %s\n", nameserver, err) 131 | } else { 132 | newResolverServersFound = true 133 | for _, answer := range response.Answers { 134 | if answer.Header.Type == dnsmessage.TypeA { 135 | servers = append(servers, answer.Body.(*dnsmessage.AResource).A[:]) 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | return &dnsmessage.Message{Header: dnsmessage.Header{RCode: dnsmessage.RCodeServerFailure}}, nil 145 | } 146 | 147 | func getRootServers() []net.IP { 148 | rootServers := []net.IP{} 149 | for _, server := range strings.Split(ROOT_SERVERS, ",") { 150 | rootServers = append(rootServers, net.ParseIP(server)) 151 | } 152 | return rootServers 153 | } 154 | 155 | func outgoingDnsQuery(servers []net.IP, question dnsmessage.Question) (*dnsmessage.Parser, *dnsmessage.Header, error) { 156 | fmt.Printf("New outgoing dns query for %s, servers: %+v\n", question.Name.String(), servers) 157 | // preparing the dns message to send to the servers 158 | max_value := ^uint16(0) 159 | randomID, err := rand.Int(rand.Reader, big.NewInt(int64(max_value))) 160 | if err != nil { 161 | return nil, nil, err 162 | } 163 | message := dnsmessage.Message{ 164 | Header: dnsmessage.Header{ 165 | ID: uint16(randomID.Int64()), 166 | Response: false, 167 | OpCode: dnsmessage.OpCode(0), 168 | }, 169 | Questions: []dnsmessage.Question{question}, 170 | } 171 | buf, err := message.Pack() 172 | if err != nil { 173 | return nil, nil, err 174 | } 175 | // establishing a connection with the servers 176 | var conn net.Conn 177 | for _, server := range servers { 178 | conn, err = net.Dial("udp", server.String()+":53") 179 | if err == nil { 180 | break 181 | } 182 | } 183 | if conn == nil { 184 | return nil, nil, fmt.Errorf("failed to make connection to servers: %s", err) 185 | } 186 | // sending the message encoded to the server 187 | _, err = conn.Write(buf) 188 | if err != nil { 189 | return nil, nil, err 190 | } 191 | 192 | // reading the answer 193 | answer := make([]byte, 512) 194 | lenOfMessage, err := bufio.NewReader(conn).Read(answer) 195 | if err != nil { 196 | return nil, nil, err 197 | } 198 | 199 | conn.Close() 200 | 201 | var parse dnsmessage.Parser 202 | 203 | header, err := parse.Start(answer[:lenOfMessage]) 204 | if err != nil { 205 | return nil, nil, err 206 | } 207 | 208 | // Basic sanity checks 209 | questions, err := parse.AllQuestions() 210 | if err != nil { 211 | return nil, nil, err 212 | } 213 | 214 | if len(questions) != len(message.Questions) { 215 | return nil, nil, fmt.Errorf("answer packet doesn't have the same amount of questions") 216 | } 217 | 218 | err = parse.SkipAllQuestions() 219 | if err != nil { 220 | return nil, nil, err 221 | } 222 | 223 | return &parse, &header, nil 224 | } 225 | -------------------------------------------------------------------------------- /pkg/dns/resolver_test.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | "net" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "golang.org/x/net/dns/dnsmessage" 12 | ) 13 | 14 | type MockPacketConn struct{} 15 | 16 | func (m *MockPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { 17 | return 0, nil 18 | } 19 | 20 | func (m *MockPacketConn) Close() error { 21 | return nil 22 | } 23 | 24 | func (m *MockPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { 25 | return 0, nil, nil 26 | } 27 | 28 | func (m *MockPacketConn) LocalAddr() net.Addr { 29 | return nil 30 | } 31 | 32 | func (m *MockPacketConn) SetDeadline(t time.Time) error { 33 | return nil 34 | } 35 | 36 | func (m *MockPacketConn) SetReadDeadline(t time.Time) error { 37 | return nil 38 | } 39 | 40 | func (m *MockPacketConn) SetWriteDeadline(t time.Time) error { 41 | return nil 42 | } 43 | 44 | func TestHandlePacket(t *testing.T) { 45 | urls := []string{"www.google.com.", "www.amazon.com."} 46 | for _, url := range urls { 47 | max := ^uint16(0) 48 | randomID, err := rand.Int(rand.Reader, big.NewInt(int64(max))) 49 | if err != nil { 50 | t.Fatalf("randomID error %s", err) 51 | } 52 | message := dnsmessage.Message{ 53 | Header: dnsmessage.Header{ 54 | RCode: dnsmessage.RCode(0), 55 | ID: uint16(randomID.Int64()), 56 | OpCode: dnsmessage.OpCode(0), 57 | Response: false, 58 | AuthenticData: false, 59 | RecursionDesired: false, 60 | }, 61 | Questions: []dnsmessage.Question{ 62 | { 63 | Name: dnsmessage.MustNewName(url), 64 | Type: dnsmessage.TypeA, 65 | Class: dnsmessage.ClassINET, 66 | }, 67 | }, 68 | } 69 | 70 | buf, err := message.Pack() 71 | if err != nil { 72 | t.Fatalf("Message pack error %s", err) 73 | } 74 | err = handlePacket(&MockPacketConn{}, &net.IPAddr{IP: net.ParseIP("127.0.0.1")}, buf) 75 | if err != nil { 76 | t.Fatalf("Server error %s", err) 77 | } 78 | 79 | } 80 | } 81 | 82 | func TestOutgoingDnsQuery(t *testing.T) { 83 | question := dnsmessage.Question{ 84 | Name: dnsmessage.MustNewName("com."), 85 | Type: dnsmessage.TypeNS, 86 | Class: dnsmessage.ClassINET, 87 | } 88 | rootServers := strings.Split(ROOT_SERVERS, ",") 89 | if len(rootServers) == 0 { 90 | t.Fatalf("no root servers found") 91 | } 92 | 93 | server := []net.IP{net.ParseIP(rootServers[0])} 94 | answer, header, err := outgoingDnsQuery(server, question) 95 | if err != nil { 96 | t.Fatalf("outgoingDnsQuery error %s", err) 97 | } 98 | if header == nil { 99 | t.Fatalf("no header found") 100 | } 101 | if answer == nil { 102 | t.Fatalf("no answer found") 103 | } 104 | if header.RCode != dnsmessage.RCodeSuccess { 105 | t.Fatalf("response wasn't successful") 106 | } 107 | err = answer.SkipAllAnswers() 108 | if err != nil { 109 | t.Fatalf("SkipAllAnswers error %s", err) 110 | } 111 | 112 | authorities, err := answer.AllAuthorities() 113 | if err != nil { 114 | t.Fatalf("error getting answers") 115 | } 116 | if len(authorities) == 0 { 117 | t.Fatalf("No answers received") 118 | } 119 | } 120 | --------------------------------------------------------------------------------