Dog, a command-line DNS client

Hi all,

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:

  • dog uses its own DNS packet parser. The byteorder crate made it really easy to write code to read values from the packet and parse them into a structure.

  • 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_possible_truncation, cast_lossless, cast_possible_wrap, and cast_sign_loss) to effectively disable that part of Rust! It was good to not be able to accidentally write code that used as anymore.

  • 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 read and write calls 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 Serialize for the DNS record structs without serde_derive being 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.

19 Likes

Very nice project!

1 Like