I'm pleased to announce that dog, my command-line DNS client, is good enough to have reached version 0.1.0. It's like
dig, but a bit different, so,
dog. The source code is on GitHub (EUPL-1.2 licence).
Some development notes and shout-outs to the projects and libraries that helped it get made:
Of course, "parse bytes into a structure" is a problem aboslutely ripe for fuzzing, so I threw cargo-fuzz at the parser and it found problems pretty much immediately. There were some overflow panics and some index-out-of-range panics, both of which it was able to reduce down to relatively-simple test cases. Eventually I got it to fuzz for an hour without crashing, and now I'm a lot more confident about the code than I was when I started.
I'd also like to praise cargo-mutagen, which proved very helpful in detecting untested code. Usually, I'd use a code coverage tool to see if I had any code that wasn't covered by tests, and mutagen worked like a more fine-grained coverage tool. I felt like DNS parsing was a problem with a small enough "code footprint" that I could aim for a high coverage percentage, and mutagen helped me get there.
Something I found out very quickly writing packet-parsing code is that if you're casting numbers from one type to another, there's a high chance you're writing code that will overflow when given certain invalid input. To combat this, I used Clippy's cast lints (
cast_sign_loss) to effectively disable that part of Rust! It was good to not be able to accidentally write code that used
It does DNS-over-TLS and HTTPS with the native-tls crate, which was great, because I could use TLS without having to worry about whether I was using openssl or a system-specific library. For most of dog's development, I used hyper and hyper-tls for sending and receiving HTTPS responses, but near the end I swapped it out for an implementation that uses httparse to decode the response data, and manually writes the request HTTP headers and does the
writecalls itself. This meant all four transport types (UDP, TCP, TLS, HTTPS) work in basically the same way.
Of course, I used serde for the JSON output. I tried separating the code into three crates in a workspace: one for the packet parsing, one for the network transports, and one for the user interface parts. JSON output naturally had to go in the UI crate, but I found I couldn't derive
Serializefor the DNS record structs without
serde_derivebeing a dependency of the packet-parsing crate, which didn't feel right, so I did it manually with
json!. This gave me more flexibility at the expense of longer code, but I'm not sure what else I could have done here.
Anyway, writing this in Rust was great. I like being able to compile down to a single small executable (a couple hundred kilobytes), I like having unit testing as part of the language, I like being able to use recent language features and still have a pretty-good guarantee that people can compile the code (it requires v1.45.0), and I like the community of people, libraries, and developer tools that helped me along the way. Cheers, everyone.