Abusing DNS, Part 3: What do you want?

Now that we have a server, it is time to build the client. In Part 2 we laid out the ground work of our project and built a server that could respond to various DNS queries (check out Part 1 if that is gobbledygook). As usual you can jump right to the source code on github TODO.
As a recap here is where we left our client:
dns-kv$ cargo run --bin client
Compiling dns-kv v0.1.0 (/home/evan/dns-kv)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/client`
Hello, world!
Let's fix that and make a command line tool that is able to talk to our new DNS server and ask it for a TXT record. Sounds like fun doesn't it?
First we are going to need another library that will assist us in parsing out command line argument (clap). Clap is awesome and helps us with most the things we need to make a fancy CLI app. Sound good? Right about now you are probably asking...
How do I get the clap?

Lucky for us rust, more specifically cargo makes that very simple for us. From your project folder run the following command and you are good to go.
dns-kv$ cargo add clap -F cargo
Updating crates.io index
Adding clap v4.5.31 to dependencies
Features:
+ cargo
+ color
...
Updating crates.io index
Locking 13 packages to latest compatible versions
Adding anstream v0.6.18
...
dns-kv$
That is it, now we have a very powerful tool to enable us to create CLI applications. For this example we are going to use the builder pattern.
Parse away
Just like dig
or nslookup
our CLI is going to need to know what domain we are looking up and what server to talk to... Let's parse some arguments.
let matches = command!() // requires `cargo` feature
.arg(arg!([server] "DNS Server").default_value("127.0.0.1:5353"))
.arg(arg!(-g --get <NAME> "Get the domain").required(true))
.get_matches();
let domain = matches
.get_one::<String>("get").expect("domain is required");
let server = matches
.get_one::<String>("server").expect("server has default");
Cool, what does that do?
First we use the command
macro to create our Command struct we then add our arguments. server
which is the DNS server to query and will default to 127.0.0.1:5353
which is perfect as that is where our server currently is. Next we add the argument for the domain we would like to get which for now is required
... you know what let me just show you.
dns-kv$ cargo run --bin client -- -h
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/client -h`
Usage: client --get <NAME> [server]
Arguments:
[server] DNS Server to query [default: 127.0.0.1:5353]
Options:
-g, --get <NAME> Get the domain
-h, --help Print help
-V, --version Print version
Oh that is right, one of the great things about having clap in your project is you get built in help. Okay thats enough of that I promise.
What do you want?

Now the CLI knows the domain and server we would like to talk to, it just has to ask. For now the CLI will assume we would like a TXT record.
fn txt_query_record(domain: &str) -> Result<Vec<u8>> {
let mut pkt = Packet::new_query(1);
let q = Question::new(
Name::new_unchecked(domain),
TYPE::TXT.into(),
CLASS::IN.into(),
false,
);
pkt.set_flags(PacketFlag::RECURSION_DESIRED);
pkt.questions.push(q);
Ok(pkt.build_bytes_vec()?)
}
The txt_query_record
creates a DNS query packet that is asking for a TXT
record for the domain we specified.
let query = txt_query_record(domain)?;
let sock = UdpSocket::bind("0.0.0.0:0").await?;
sock.send_to(&query, server).await?;
let mut buf = [0; 4096];
let (size, _) = sock.recv_from(&mut buf).await?;
let data = buf[..size].to_vec();
With that query in hand the CLI binds to an open UDP port 0.0.0.0:0
let's the kernel pick the port we will send data from. Sends the query packet to the server and reads the response.
let packet = Packet::parse(&data)?;
tracing::info!("{:#?}", packet.answers);
Finally the cli parses the response from the packet and prints out the response.
dns-kv$ cargo run --bin client -- -g domain.com
Compiling dns-kv v0.1.0 (/home/evan/repos/dns-kv)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.59s
Running `target/debug/client -g domain.com`
2025-03-05T22:19:50.479792Z INFO client: [
ResourceRecord {
name: Name(
"domain.com",
"12",
),
class: IN,
ttl: 2,
rdata: TXT(
TXT {
strings: [
CharacterString {
data: "AAAA",
},
],
size: 5,
},
),
cache_flush: false,
},
Lets go
Well that is coming along nicely if I don't say so myself. If you made it this far, you might was well click that subscribe button over there. Come yell at my on bluesky or linkedin. Next week, we will start turning dns-kv into a tool that will let us transfer data.