This is my experience on prototyping a DNS server in Golang. After going through this blog you will have a nice idea on how DNS works underhood and you should be able to build something like this.
Well…… mostly better than this !!!.
In this blog, We will be covering
- Basics of DNS
- Internal DNS Query
- External DNS Query
- DNS Zone types and Resource Record
- More DNS Zone types
- Structure of DNS Query and Answers
Building DNS Server in Golang
- Server to handle UDP Packets
- Extracting DNS Message from UDP Packets
- Handling based on the URL and query
In the end, we should be able to run a DNS Server and it can possibly serve request to your DNS lookups
Basics of DNS:
DNS stands for Domain Name System.
A key thing DNS server do is, it helps the requester to find the IP Address of a site, provided the domain name of the site.
A simple analogy would be Phone directory, where you can search for a Phone number using the name of the contact or use the phone number to check who the contact is.
DNS is more like phone directory but in this case, the contact name will be site name and phone number will be an IP Address
What about hosts_file?
Ans: Since there are myriad of sites hosted, it would be challenging to store lookup data in a single file and use the data from it. And also if any of the new sites is added, then the local hosts file has to be updated, which would be often the case.
Well, if there are only genuine and authentic sites there is a possibility that we could have just used hosts file but the internet is loaded with meaningless sites, so DNS helps to locate all possible machines in the network.
Order of DNS Query:
- When DNS tries to resolve a name to an IP Address, it first checks in Host file (/etc/hosts)
The hosts file is the mapping of IP Address to Domain Name
- DNS Cache will be checked after hosts file and the duration the DNS answers cached will be different from OS.
- When the Answer is not found, DNS will start to look for answers out of the local machine, hence the DNS Servers.
Internal DNS Query:
When a DNS Answers are from hosts file or OS DNS cache they refer to internal DNS Query.
External DNS Query:
When a DNS lookup answer is not found locally, the DNS client will be looking for answers outside the local system.
To start with this let us take look at what is
- Root Hints
- Authoritative and Non-Authoritative answers
Consider my workstation and my DNS server are on the same corp.com. When I type in on my workstation’s browser www.confidential.corp.com, the DNS server has all the records for the corp.com DNS zone. So it can hand me the IP address for www.confidential.corp.com, so I can communicate with that IP address. Getting the response from my own corporate DNS server ie: the server who has the information in its zone, not its cache but in a DNS zone file, that’s what makes it authoritative. And the reason you might be interested in an authoritative response versus a non-authoritative is if it comes from the server where the DNS zone(the data used in lookup process) lives, it is considered to be the most reliable data.
In the example below, “corp.com” DNS server has data for all domains ending with “corp.com”. ie: “confidential.corp.com”.
So when we get the data from the server who has the data within it, it is considered to be safe and authoritative.
When our DNS Servers don’t have an answer for that Query, they will ask the root hints servers and root hints provide answers or send possible server list (.com, .net…etc)that can resolve our query and this keeps on going until we find the DNS Server which can resolve the query (careers.github.com). So once the DNS Server for resolving “careers.github.com” is found, the answer will be made and sent to the DNS query requester(our Corp.com DNS Server) and it will be cached there for future references. Since the answer is not from our DNS Server, it will be considered a non-authoritative response.
Recursive vs Non-recursive query:
when the DNS Query is made we have the liberty to specify the recursiveness of the query. So if the DNS Server does not have the answer it will not forward it to further DNS Servers. This might be useful when we only need an authoritative answer.
Recursive DNS query risks:
- DOS attacks
- DNS cache poisoning
- Unauthorized use of resources
- Root name server performance degradation
Root hints are persisted data in our OS, which will have the mapping of Root DNS Server name and their IP Address. When the answers are not found in our DNS Server (corp.com) it will use the root hints to get the root DNS Servers.
You can check it in ubuntu machine by running :
dig . ns
ICANN( The Internet Corporation for Assigned Names and Numbers) manages the DNS Root Servers.
Hierarchy of DNS Servers:
The root DNS Server contains the top level domains like .com, .org, .io ..etc. And the top level domains contains the subdomains and so on.
dig google.com +trace will give a good idea of how the IP for google.com is fetched by going through root DNS Servers to Google DNS Server.
DNS Zone types:
Forward lookup zone: Hostname to IP Address lookup
Reverse lookup zone: IP Address to hostname lookup
The lookup zones should contain the data/resource records for serving the DNS queries. The resource records can be stored in DNS server as Zone file.
The zone file contains mappings between domain names and IP addresses and other resources, organized in the form of text representations of resource records (RR). A zone file may be either a DNS master file, authoritatively describing a zone, or it may be used to list the contents of a DNS cache.
A zone file is a sequence of entries for resource records. Each line is a text description that defines a single resource record (RR). The description consists of several fields separated by white space (spaces or tabs) as follows:
name | ttl | record class | record type | record data
Few prime Resource records that can be placed in lookup zone are:
A and AAAA:
One of the most common resource records out there is a host record. An A record is an IPv4 host record. An AAAA, well, that’s an IPv6 host record.
dig google.com -t A will get all A (IPv4) records for the hostname
The DNS server should be maintaining those records and serve according to the query from DNS client.
CNAME: (Canonical name)
CNAME provides alias name to any server.
Consider you have multiple subdomains(mail.corp.com, docs.corp.com) for your corp server and calling on all the subdomains should go to main corp server, you can use CNAME record since the CNAME record points to another record and it will not point to any IP.
MX Record (Mail exchange records):
Mail exchange records play a crucial role in sending and receiving emails.
How does our mail server know where to send the mail data (inter mail transfer [gmail to yahoo] )?
Ans: MX Record
Consider Bob (email@example.com) is sending mail to Alan (firstname.lastname@example.org).
- Bob writes a mail with “to address” to “email@example.com” and send the mail. The data reached Gmail mail server and it should be redirected to yahoo mail server, so Alan could receive his mail from there.
- GMail server reads the “to address details“ and finds out the data has to be sent to yahoo exchange server and
Our Gmail server does not know the yahoo mail exchange server address, so it requests DNS server for yahoo with type MX.
3. The DNS checks in resource records and should be sending an MX type result in the response.
4. The GMail server knows the location of yahoo mail exchange servers and it can send the data
Now that we covered how the Terminology of a DNS Query and DNS Zone Types and Resource Records, we can start looking more deep on how the DNS client and server communicate (message format they use for communication)
DNS Message format:
This is how a DNS message looks while requesting for data from DNS server or when a DNS server sends the response to the client.
Based on the request and response the content of the message will be filled appropriately.
For more information: https://tools.ietf.org/html/rfc1035#section-4.1.1
For better understanding, the DNS message is split in 2 octets(16 bits per row) so each cell is one bit.
In reality, this will be a continuous message.
ID: A 16-bit identifier assigned by the program that generates any kind of query. This identifier is copied the corresponding reply and can be used by the requester to match up replies to outstanding queries.
ie: ID will be generated by the DNS client which initiates the query lookup and when the response comes the response can be mapped to query using the ID
QR: A one-bit field that specifies whether this message is a query (0), or a response (1).
A four-bit field that specifies kind of query in this message. This value is set by the originator of a query and copied into the response. eg: standard query or inverse query
AA : Authoritative Answer, this bit is valid in responses, and specifies that the responding name server is an authority for the domain name in question section
TC: Is the message truncated due to the large size
RD: Is the recursion desired (can be set when making the query)
RA: Denotes is the recursion available in response from DNS Server
Z: Reserved for future purpose
RCODE: Response Code, 4-bit field is set as part of responses
0 — No error condition
1- Format error
2 — Server failure
3 — Name Error
4 — Not Implemented
5 — Refused
QDCOUNT: 16-bit field, denoting the number of questions in the request
ANCOUNT: 16-bit field, denoting the number of answers in response
NSCOUNT: 16-bit field, denoting the number of name server resource records in the authority records
ARCOUNT: 16-bit field, denoting the number of resource records in the additional records section
The Questions(lookup request) will be placed in DNS Question section
For better understanding, the DNS Question is split in 2 octets(16 bits per row) so each cell is one bit.
In reality, this will be a continuous message.
QNAME: This fields contains the domain names that we wish for resolving.
The domain name will be represented as a sequence of labels. Each label is represented as a one octet length field followed by that number of octets.
The domain name terminates with the zero-length octet for the null label of the root.
A quick example for the domain name: “example.com”
Step 1: the domain name “example.com” consists of two sections “example” and “com”
Step 2: “example” and “com” will be URL encoded to “69 88 65 77 80 76 69” and “99 111 109” respectively. This will be called as labels
Step 3: The label will be preceded by an integer byte containing the number of bytes in the section
ie: “example” will “7 69 88 65 77 80 76 69”
Step 4: Each value in the label can be converted to single octet value
ie: (7) (69) (88)
00000111 01000101 01011000 ………..
The final data can be placed in the QNAME question section.
QTYPE: this 2-bit value specifies the type of Query
QCLASS: specifies the class of Query, mostly IN(internet)
DNS Answers, Additional and Authority records:
The answer, authority, and additional sections all share the same resource record format
NAME: a domain name to which this resource record pertains.
TYPE: specifies the type of query. eg. Standard or inverse query
TTL: this 32-bit field denotes the amount of time the resource record(answer) can be cached. Zero values denote the resource record should not be cached.
RDLENGTH: Response Data Length, a 16-bit field denotes the length of octets in Response Data field.
RDATA: a variable length string of octets that describes the resource. The format of this information varies according to the TYPE and CLASS of the resource record. For example, the if the TYPE is A (IPv4) and the CLASS is IN,
the RDATA field is a 4 octet ARPA Internet address.
Now that we know how the DNS Message (Queries and Answers) has to be structured we can try and implement a mock DNS Server.
Building DNS Server in Golang:
Generally, the DNS Queries and Answers will be passed as UDP message since they are lightweight. The query and answer from DNS server will be mapped using the ID in the DNS Header.
TCP will be used when sending a large answer as DNS Message, eg: Zone transfers, where the entire content of DNS Server is copied to another DNS Server.
To create a DNS Server we have to listen to a particular port for incoming queries. By default DNS Server runs on port 53.
Listening on a port:
When we receive data as UDP message we have to decode the DNS message from it. As the DNS Message structure is complex, it is hard to write DNS encoding and decoding on our own. We can instead use an already existing opensource library for it or we can build a library for more tailored need.
In this example, we will be using Google Packets.
we can now listen on the UDP port and get UDP message and use Google packets to decode it to DNS Message.
Before implementing serveDNS function we might need few records to serve DNS query.
The records are initialised at the beginning of the main function.
The program constantly listens on port “8090” and serves query using the server DNS function.
The serveDNS function gets the connection, client Address and the query request as parameters. The request message is then used to mock response message and the values that have to change in response is then changed.
Then the response message is serialized to UDP message format and write back to the client using the client address obtained on receiving the DNS message as UDP package.
Run this program and you should be able to resolve DNS Queries for the records specified in the main function.
ie: running the below command should bring DNS Response from the DNS server we created.
nslookup google.com localhost -port=8090
The serveDNS function can be optimized and the entire flow can be made into a library, so we can easily create a DNS server.
A prototype that I developed for fun — https://github.com/openmohan/lightdns
Thanks for reading, share and clap if you like the content :-)
Reference materials and credits to:
DNS library in Go. Contribute to miekg/dns development by creating an account on GitHub.