rust-DNSoverHTTPS is a DNS Server written in rust that uses Google's DNS-over-HTTPS API for queries. It was developed by David Cao and Graham Mosley as a final project for CIS 198. This project consists of the actual rust-DNSoverHTTPS server and a fork of dns-parser.
Documentation for the project is located at http://gmosley.github.io/rust-DNSoverHTTPS.
Documentation for our fork of dns-parser is located at http://david-cao.github.io/rustdocs/dns_parser/.
For instructions on building/running rust-DNSoverHTTPS see the README.
We feel that over the past 3 weeks, a significant amount of time was spent on the project. Since this was also our first time working with the DNS, we also spent a fair amount of time understanding the protocol.
While we didn't keep track of the exact time spent on the project, we estimate that we spent a combined 15 hours for the first two weeks and a combined 20 hours for the final week.
We were able to create a working DNS server with almost the same average response time as traditional DNS servers for the most common queries! See the results section for more information.
rust-DNSoverHTTPS handles a standard DNS query in the following steps:
- The server listens for incoming UDP packets on port 53. When a packet is received, a new thread is spawned.
- The packet is passed to
worker.rs
, where it is handled to be returned. - In
worker.rs
, the packet is parsed into a DNS packet usingdns-parser
. - If the parsing is successful, a HTTPS request is constructed and sent using
hyper
. - The response is deserialized using serde.
- A DNS answer packet is constructed and sent back to the requestor.
As mentioned above, our work was split between our fork of dns-parser and the actual server itself. This gave us experience working with an already existing codebase and choosing the design for our own code.
dns-parser was chosen to parse DNS packets. Unfortunately, while library was great for parsing packets, it had very little support for building packets. In our fork of dns-parser, the majority of our work was in builder.rs
. We implemented many functions including new_response
and add_question
. We also implemented DNS packet compression.
main.rs
- Simple implementation of a DNS server. Spawns a thread for each connection.worker.rs
- Takes in a packet, parses it, sends it to the Google API, and builds a response to return.structs.rs
- Structs used for serde deserialization of Google API responses.errors.rs
- Enum of possible errors that can occur.
We found that on average our DNS server had under 100 ms response time for queries when built with the --release
flag. This is only slightly slower than the mean response time of the University of Pennsylvania's DNS Servers. Our server also has the security benefits of HTTPS over DNS.
Benchmarking was done using namebench
rust-DNSoverHTTPS, Google Public DNS, and the University of Pennsylvania's DNS were tested. Benchmarking was run on OSX using AirPennNet. The DNS cache was cleared before benchmarking.
./namebench.py -H -q 500 127.0.0.1 8.8.8.8 128.91.49.1
DNS Server | ms |
---|---|
Penn DNS | 2.66790 |
Google Public DNS | 5.82314 |
rust-DNSoverHTTPS | 46.1969 |
DNS Server | ms |
---|---|
Google Public DNS | 50.15 |
Penn DNS | 64.97 |
rust-DNSoverHTTPS | 84.02 |
rust-DNSoverHTTPS is not a full DNS Server, but rather a proof of concept of using DNS-over-HTTPS.
rust-DNSoverHTTPS supports the following record types:
- A
- AAAA
- CNAME
- PTR
While rust-DNSoverHTTPS only supports 4 record types (~10% of the protocol) there are no noticeable problems when browsing the web.
Additionally rust-DNSoverHTTPS will probably fail most DNS health checks since queries like hostname.bind and id.server are unsupported.
We had a great time working on rust-DNSoverHTTPS. We gained a huge amount experience with Rust and the DNS protocol.
It worked! There were a few times during the project that we were not sure if we could get everything working. Fortunately, we did. We are also proud of our implementation DNS Compression. We use a HashMap to to store labels -> offsets. When we're writing a new name, we check if it already exists in the HashMap. If it does, the byte offset is written instead of the entire name.
The main goal of our project was to support the minimum amount of features of the protocol needed to browse the web using our DNS server. We assumed that we would only need to implement A and CNAME record types. Because of this we didn't implement a system that made it easy to support various record types. This made adding later types more difficult. Specifically, in add_answer
we pass in a vec of bytes, which we compute ourselves based on the type. In the future, we could modify dns_parser::rrdata
to use it for record type serialization (instead of just deserialization).