How to avoid TCP connection reset with hickory_client

I'm doing simple, synchronous DNS AXFR requests using hickory_client. These are necessarily over TCP.

What I've noticed is that sometimes, especially with large zones, the DNS server (BIND in this case) will log a connection reset after my program is finished.

Here's a minimal example that demonstrates the issue:

use anyhow::anyhow;
use anyhow::Result;
use hickory_client::client::{Client, SyncClient};
use hickory_client::op::DnsResponse;
use hickory_client::rr::{DNSClass, Name, RecordType};
use hickory_client::tcp::TcpClientConnection;
use std::net::SocketAddr;

fn main() -> Result<()> {
    let address: SocketAddr = "127.0.0.1:53".parse()?;
    let conn = TcpClientConnection::new(address)?;
    let client = SyncClient::new(conn);

    let response: DnsResponse = client.query(
        &Name::from_utf8("example.com").unwrap(),
        DNSClass::IN,
        RecordType::AXFR,
    )?;

    if !response.contains_answer() {
        return Err(anyhow!("AXFR returned no answers: {response:?}"));
    }

    let answers = response.answers();

    let answers = answers
        .iter()
        .filter(|rec| matches!(rec.record_type(), RecordType::A | RecordType::AAAA));

    for record in answers {
        let rr_name = record.name().to_utf8();

        let rr_data = match record.data() {
            Some(data) => data,
            None => continue,
        };

        let rr_addr = match rr_data.ip_addr() {
            Some(addr) => addr,
            None => continue,
        };

        println!("{} -> {}", rr_name, rr_addr);
    }

    Ok(())
}

This works fine, but sometimes the name server reports that the connection was reset:

26-Nov-2024 02:30:56.328 xfer-out: info: client @0x7f6a2cda9010 127.0.0.1#33124 (example.com): transfer of 'example.com/IN': AXFR started (serial 2024111830)
26-Nov-2024 02:30:56.336 xfer-out: error: client @0x7f6a2cda9010 127.0.0.1#33124 (example.com): transfer of 'example.com/IN': send: connection reset

I assume this is because the server tried to send some more data but my client had already closed the stream.

I'd like to understand if I am doing something wrong here and how I could close the TCP connection gracefully. I feel like I am missing something obvious, because I am a Rust beginner, so I feel reluctant to join hickory-dns's Discord to ask about this.

This was due to me mistakenly believing that client.query() was the correct method to use here. In fact I should be using zone_transfer which I am embarrassed to have not seen as it's on the same page.

What was going wrong here was:

  • According to RFC a zone transfer is SOA record, zone content, and then the SOA record again.
  • BIND name server was sending the initial SOA record and all records in the zone in the first response message.
  • Above code was closing TCP connection after processing the first message. Since server had more to send (last SOA record) this was logged as a reset connection. I didn't pick it up as a serious error because, well, I had all the zone content!

My error became more obvious when used against other DNS server implementations. PowerDNS, for example, seems to send SOA, zone content and final SOA as three separate messages. This code was getting just the single SOA record from such servers!

Both problems fixed by using the correct method which returns an iterator of responses.

2 Likes