[GH-ISSUE #2619] hickory_client SyncClient AXFR request against particular server only receives a single record (SOA) #1028

Open
opened 2026-03-16 01:19:03 +03:00 by kerem · 7 comments
Owner

Originally created by @grifferz on GitHub (Nov 26, 2024).
Original GitHub issue: https://github.com/hickory-dns/hickory-dns/issues/2619

What is the question?
I mentioned this on Discord and so far got the feedback that my code looks correct, but I am very new to both Rust and hickory-dns so I still think I have probably misunderstood.

I have the following code to do a simple synchronous AXFR query. It works fine except for against a particular server. Against that one it only receives an answer with a single record, which is an SOA.

When I perform the same AXFR on the command line using dig that works fine.

I have put the zone contents on another server and this code doing AXFR against that works fine, so maybe it is something odd that this server is doing. But as I say, dig copes with it.

The server is not mine so I cannot easily allow you to AXFR from it. I have replicated the behaviour against this server from many different places though, and the code is very simple, so I can't see how you would get different results with the same code.

Am I doing something wrong? If not then I suppose my next step is packet dump of the query from my code and from dig.

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 = "85.119.82.49:53".parse()?;
    let conn = TcpClientConnection::new(address)?;
    let client = SyncClient::new(conn);

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

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

    println!("DnsResponse:");
    println!("{response:?}\n");

    let answers = response.answers().iter();

    println!("Answers:");
    for record in answers {
        match record.record_type() {
            RecordType::A | RecordType::AAAA => {
                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);
            }
            _ => {
                println!("{record:?}");
            }
        };
    }

    Ok(())
}

Output:

DnsResponse:
DnsResponse { message: Message { header: Header { id: 1685, message_type: Response, op_code: Query, authoritative: true, truncation: false, recursion_desired: true, recursion_available: false, authentic_data: false, checking_disabled: false, response_code: NoError, query_count: 1, answer_count: 1, name_server_count: 0, additional_count: 1 }, queries: [Query { name: Name("caboose.org.uk."), query_type: AXFR, query_class: IN }], answers: [Record { name_labels: Name("caboose.org.uk."), rr_type: SOA, dns_class: IN, ttl: 3600, rdata: Some(SOA(SOA { mname: Name("oelph.caboose.org.uk."), rname: Name("postmaster.caboose.org.uk."), serial: 2021022102, refresh: 14400, retry: 3600, expire: 604800, minimum: 3600 })) }], name_servers: [], additionals: [], signature: [], edns: Some(Edns { rcode_high: 0, version: 0, dnssec_ok: false, max_payload: 1232, options: OPT { options: {} } }) }, buffer: [6, 149, 133, 0, 0, 1, 0, 1, 0, 0, 0, 1, 7, 99, 97, 98, 111, 111, 115, 101, 3, 111, 114, 103, 2, 117, 107, 0, 0, 252, 0, 1, 192, 12, 0, 6, 0, 1, 0, 0, 14, 16, 0, 41, 5, 111, 101, 108, 112, 104, 192, 12, 10, 112, 111, 115, 116, 109, 97, 115, 116, 101, 114, 192, 12, 120, 118, 89, 150, 0, 0, 56, 64, 0, 0, 14, 16, 0, 9, 58, 128, 0, 0, 14, 16, 0, 0, 41, 4, 208, 0, 0, 0, 0, 0, 0] }

Answers:
Record { name_labels: Name("caboose.org.uk."), rr_type: SOA, dns_class: IN, ttl: 3600, rdata: Some(SOA(SOA { mname: Name("oelph.caboose.org.uk."), rname: Name("postmaster.caboose.org.uk."), serial: 2021022102, refresh: 14400, retry: 3600, expire: 604800, minimum: 3600 })) }

dig output:

$ dig @85.119.82.49 caboose.org.uk axfr

; <<>> DiG 9.18.28-1~deb12u2-Debian <<>> @85.119.82.49 caboose.org.uk axfr
; (1 server found)
;; global options: +cmd
caboose.org.uk.         3600    IN      SOA     oelph.caboose.org.uk. postmaster.caboose.org.uk. 2021022102 14400 3600 604800 3600
2020._domainkey.caboose.org.uk. 300 IN  TXT     "v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxuCPFbYTWS9tnnZc8uTmh6ef23zVa28wx3/rNBwwr9PysG9ekJlCIW6xj6E7EV6bjKGkbTlvvIiAXv6jtDHAHE2/EczvGj8y/Cls5MM2wgMAXL/9AAD+vjFtsjQNzy+ZyHq8UqR1DDaUnbocrPCOghV5U3WWCIzO7yvHRxHzdO+5ARwKgIGf+f/" "OQW2CgXrtdfUDaPRD+5mqZgzyXqbx2ThowGYjuAm0R3fyAyCsPONKvZxtOHrQRsquaPVt1qKbZ54k9uk4yit1On+buJRF7Y4dSdMshx46oY88yrKkA1hN/jO+bf+iQLvA8Q62XHKvxWEHJYqIKIZMJkbyNHJHUwIDAQAB"
_dmarc.caboose.org.uk.  3600    IN      TXT     "v=DMARC1; p=quarantine; rua=mailto:postmaster@caboose.org.uk;"
caboose.org.uk.         300     IN      A       85.119.82.49
caboose.org.uk.         300     IN      AAAA    2001:ba8:1f1:f0e9::2
caboose.org.uk.         3600    IN      MX      10 mail.caboose.org.uk.
caboose.org.uk.         3600    IN      NS      a.authns.bitfolk.co.uk.
caboose.org.uk.         3600    IN      NS      b.authns.bitfolk.com.
caboose.org.uk.         3600    IN      NS      c.authns.bitfolk.com.
caboose.org.uk.         3600    IN      TXT     "google-site-verification=7iSQAZnvmMXuKp74SBc3cvcJjn7vhSOmm-VeTIg5rJ0"
caboose.org.uk.         3600    IN      TXT     "v=spf1 include:gmail.com mx ~all"
mail.caboose.org.uk.    300     IN      A       85.119.82.49
mail.caboose.org.uk.    300     IN      AAAA    2001:ba8:1f1:f0e9::2
oelph.caboose.org.uk.   3600    IN      A       85.119.82.49
oelph.caboose.org.uk.   3600    IN      AAAA    2001:ba8:1f1:f0e9::2
orourke.se._report._dmarc.caboose.org.uk. 3600 IN TXT "v=DMARC1;"
strictureblog.org.uk._report._dmarc.caboose.org.uk. 3600 IN TXT "v=DMARC1;"
www.caboose.org.uk.     300     IN      A       85.119.82.49
www.caboose.org.uk.     300     IN      AAAA    2001:ba8:1f1:f0e9::2
caboose.org.uk.         3600    IN      SOA     oelph.caboose.org.uk. postmaster.caboose.org.uk. 2021022102 14400 3600 604800 3600
;; Query time: 19 msec
;; SERVER: 85.119.82.49#53(85.119.82.49) (TCP)
;; WHEN: Tue Nov 26 18:04:54 GMT 2024
;; XFR size: 20 records (messages 3, bytes 1267)

I note that the dig output says "messages 3" and am wondering if that has anything to do with it. When I do an axfr against other servers with dig I get "messages 1"

Am I just missing how to access these extra messages?

Originally created by @grifferz on GitHub (Nov 26, 2024). Original GitHub issue: https://github.com/hickory-dns/hickory-dns/issues/2619 What is the question? I mentioned this on Discord and so far got the feedback that my code looks correct, but I am very new to both Rust and hickory-dns so I still think I have probably misunderstood. I have the following code to do a simple synchronous AXFR query. It works fine except for against a particular server. Against that one it only receives an answer with a single record, which is an SOA. When I perform the same AXFR on the command line using dig that works fine. I have put the zone contents on another server and this code doing AXFR against that works fine, so maybe it is something odd that this server is doing. But as I say, dig copes with it. The server is not mine so I cannot easily allow you to AXFR from it. I have replicated the behaviour against this server from many different places though, and the code is very simple, so I can't see how you would get different results with the same code. Am I doing something wrong? If not then I suppose my next step is packet dump of the query from my code and from dig. ```rust 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 = "85.119.82.49:53".parse()?; let conn = TcpClientConnection::new(address)?; let client = SyncClient::new(conn); let response: DnsResponse = client.query( &Name::from_utf8("caboose.org.uk").unwrap(), DNSClass::IN, RecordType::AXFR, )?; if !response.contains_answer() { return Err(anyhow!("AXFR returned no answers: {response:?}")); } println!("DnsResponse:"); println!("{response:?}\n"); let answers = response.answers().iter(); println!("Answers:"); for record in answers { match record.record_type() { RecordType::A | RecordType::AAAA => { 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); } _ => { println!("{record:?}"); } }; } Ok(()) } ``` Output: ``` DnsResponse: DnsResponse { message: Message { header: Header { id: 1685, message_type: Response, op_code: Query, authoritative: true, truncation: false, recursion_desired: true, recursion_available: false, authentic_data: false, checking_disabled: false, response_code: NoError, query_count: 1, answer_count: 1, name_server_count: 0, additional_count: 1 }, queries: [Query { name: Name("caboose.org.uk."), query_type: AXFR, query_class: IN }], answers: [Record { name_labels: Name("caboose.org.uk."), rr_type: SOA, dns_class: IN, ttl: 3600, rdata: Some(SOA(SOA { mname: Name("oelph.caboose.org.uk."), rname: Name("postmaster.caboose.org.uk."), serial: 2021022102, refresh: 14400, retry: 3600, expire: 604800, minimum: 3600 })) }], name_servers: [], additionals: [], signature: [], edns: Some(Edns { rcode_high: 0, version: 0, dnssec_ok: false, max_payload: 1232, options: OPT { options: {} } }) }, buffer: [6, 149, 133, 0, 0, 1, 0, 1, 0, 0, 0, 1, 7, 99, 97, 98, 111, 111, 115, 101, 3, 111, 114, 103, 2, 117, 107, 0, 0, 252, 0, 1, 192, 12, 0, 6, 0, 1, 0, 0, 14, 16, 0, 41, 5, 111, 101, 108, 112, 104, 192, 12, 10, 112, 111, 115, 116, 109, 97, 115, 116, 101, 114, 192, 12, 120, 118, 89, 150, 0, 0, 56, 64, 0, 0, 14, 16, 0, 9, 58, 128, 0, 0, 14, 16, 0, 0, 41, 4, 208, 0, 0, 0, 0, 0, 0] } Answers: Record { name_labels: Name("caboose.org.uk."), rr_type: SOA, dns_class: IN, ttl: 3600, rdata: Some(SOA(SOA { mname: Name("oelph.caboose.org.uk."), rname: Name("postmaster.caboose.org.uk."), serial: 2021022102, refresh: 14400, retry: 3600, expire: 604800, minimum: 3600 })) } ``` dig output: ``` $ dig @85.119.82.49 caboose.org.uk axfr ; <<>> DiG 9.18.28-1~deb12u2-Debian <<>> @85.119.82.49 caboose.org.uk axfr ; (1 server found) ;; global options: +cmd caboose.org.uk. 3600 IN SOA oelph.caboose.org.uk. postmaster.caboose.org.uk. 2021022102 14400 3600 604800 3600 2020._domainkey.caboose.org.uk. 300 IN TXT "v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxuCPFbYTWS9tnnZc8uTmh6ef23zVa28wx3/rNBwwr9PysG9ekJlCIW6xj6E7EV6bjKGkbTlvvIiAXv6jtDHAHE2/EczvGj8y/Cls5MM2wgMAXL/9AAD+vjFtsjQNzy+ZyHq8UqR1DDaUnbocrPCOghV5U3WWCIzO7yvHRxHzdO+5ARwKgIGf+f/" "OQW2CgXrtdfUDaPRD+5mqZgzyXqbx2ThowGYjuAm0R3fyAyCsPONKvZxtOHrQRsquaPVt1qKbZ54k9uk4yit1On+buJRF7Y4dSdMshx46oY88yrKkA1hN/jO+bf+iQLvA8Q62XHKvxWEHJYqIKIZMJkbyNHJHUwIDAQAB" _dmarc.caboose.org.uk. 3600 IN TXT "v=DMARC1; p=quarantine; rua=mailto:postmaster@caboose.org.uk;" caboose.org.uk. 300 IN A 85.119.82.49 caboose.org.uk. 300 IN AAAA 2001:ba8:1f1:f0e9::2 caboose.org.uk. 3600 IN MX 10 mail.caboose.org.uk. caboose.org.uk. 3600 IN NS a.authns.bitfolk.co.uk. caboose.org.uk. 3600 IN NS b.authns.bitfolk.com. caboose.org.uk. 3600 IN NS c.authns.bitfolk.com. caboose.org.uk. 3600 IN TXT "google-site-verification=7iSQAZnvmMXuKp74SBc3cvcJjn7vhSOmm-VeTIg5rJ0" caboose.org.uk. 3600 IN TXT "v=spf1 include:gmail.com mx ~all" mail.caboose.org.uk. 300 IN A 85.119.82.49 mail.caboose.org.uk. 300 IN AAAA 2001:ba8:1f1:f0e9::2 oelph.caboose.org.uk. 3600 IN A 85.119.82.49 oelph.caboose.org.uk. 3600 IN AAAA 2001:ba8:1f1:f0e9::2 orourke.se._report._dmarc.caboose.org.uk. 3600 IN TXT "v=DMARC1;" strictureblog.org.uk._report._dmarc.caboose.org.uk. 3600 IN TXT "v=DMARC1;" www.caboose.org.uk. 300 IN A 85.119.82.49 www.caboose.org.uk. 300 IN AAAA 2001:ba8:1f1:f0e9::2 caboose.org.uk. 3600 IN SOA oelph.caboose.org.uk. postmaster.caboose.org.uk. 2021022102 14400 3600 604800 3600 ;; Query time: 19 msec ;; SERVER: 85.119.82.49#53(85.119.82.49) (TCP) ;; WHEN: Tue Nov 26 18:04:54 GMT 2024 ;; XFR size: 20 records (messages 3, bytes 1267) ``` I note that the dig output says "messages 3" and am wondering if that has anything to do with it. When I do an axfr against other servers with dig I get "messages 1" Am I just missing how to access these extra messages?
Author
Owner

@grifferz commented on GitHub (Nov 26, 2024):

Perhaps it could also be significant that this server is PowerDNS whereas others I have tested against are bind9

<!-- gh-comment-id:2501834695 --> @grifferz commented on GitHub (Nov 26, 2024): Perhaps it could also be significant that this server is PowerDNS whereas others I have tested against are bind9
Author
Owner

@marcus0x62 commented on GitHub (Nov 26, 2024):

A packet capture of the AXFR transaction with dig and a separate packet capture of the AXFR using your code would be helpful to troubleshoot this.

<!-- gh-comment-id:2501842923 --> @marcus0x62 commented on GitHub (Nov 26, 2024): A packet capture of the AXFR transaction with dig and a separate packet capture of the AXFR using your code would be helpful to troubleshoot this.
Author
Owner

@grifferz commented on GitHub (Nov 26, 2024):

I've just done that. From a quick glance I see that both times two responses come back. My code seems to only see the one with a single record in it and then itself tears down the TCP connection.

Separately I had also noticed that when it works my code provokes a "connection reset" log on the DNS server because it's closing the connection before the server has finished sending everything so I was going to ask about that too, but in this case it seems it is closing before actually seeing all the responses it asked for.

Anyway:

https://strugglers.net/~andy/tmp/hickory.pcap
https://strugglers.net/~andy/tmp/dig.pcap

Unless you want me to export these in some other format for here?

<!-- gh-comment-id:2501945123 --> @grifferz commented on GitHub (Nov 26, 2024): I've just done that. From a quick glance I see that both times two responses come back. My code seems to only see the one with a single record in it and then itself tears down the TCP connection. Separately I had also noticed that when it works my code provokes a "connection reset" log on the DNS server because it's closing the connection before the server has finished sending everything so I was going to ask about that too, but in this case it seems it is closing before actually seeing all the responses it asked for. Anyway: https://strugglers.net/~andy/tmp/hickory.pcap https://strugglers.net/~andy/tmp/dig.pcap Unless you want me to export these in some other format for here?
Author
Owner

@grifferz commented on GitHub (Nov 27, 2024):

I've found another PowerDNS operator who was willing to allow me to AXFR and that behaves the same: my code above gets a single response that contains just an SOA record. dig gets everything and says "messages 3" at the end.

<!-- gh-comment-id:2502264290 --> @grifferz commented on GitHub (Nov 27, 2024): I've found another PowerDNS operator who was willing to allow me to AXFR and that behaves the same: my code above gets a single response that contains just an SOA record. dig gets everything and says "messages 3" at the end.
Author
Owner

@bluejekyll commented on GitHub (Nov 27, 2024):

For zone transfers you want to use this method on the client, query expects only a single response. hopefully this just works, https://docs.rs/hickory-client/latest/hickory_client/client/trait.Client.html#method.zone_transfer

<!-- gh-comment-id:2504675716 --> @bluejekyll commented on GitHub (Nov 27, 2024): For zone transfers you want to use this method on the client, query expects only a single response. hopefully this just works, https://docs.rs/hickory-client/latest/hickory_client/client/trait.Client.html#method.zone_transfer
Author
Owner

@grifferz commented on GitHub (Nov 27, 2024):

hopefully this just works, https://docs.rs/hickory-client/latest/hickory_client/client/trait.Client.html#method.zone_transfer

Yes, that works as I expect now, thank you!

How I got to where I was:

  • I thought I'd start with a simple synchronous client request so that's why hickory_client
  • I read down the method list as far as query and saw that its parameter query_type was of type RecordType
  • As I did find a RecordType::AXFR I thought that was what I should use and tried it
  • It worked almost all the time (seems most zone transfers fit in one response) so I thought that was correct!

I should have read further and seen zone_transfer.

Maybe the documentation of query could say that it is for single responses, not for things like AXFR which may have multiple responses? I don't know if you'd want to be bolder than that and explicitly deny query_type of AXFR there.

<!-- gh-comment-id:2504803440 --> @grifferz commented on GitHub (Nov 27, 2024): > hopefully this just works, https://docs.rs/hickory-client/latest/hickory_client/client/trait.Client.html#method.zone_transfer Yes, that works as I expect now, thank you! How I got to where I was: - I thought I'd start with a simple synchronous client request so that's why hickory_client - I read down the method list as far as `query` and saw that its parameter `query_type` was of type `RecordType` - As I did find a `RecordType::AXFR` I thought that was what I should use and tried it - It worked almost all the time (seems most zone transfers fit in one response) so I thought that was correct! I should have read further and seen `zone_transfer`. Maybe the documentation of `query` could say that it is for single responses, not for things like AXFR which may have multiple responses? I don't know if you'd want to be bolder than that and explicitly deny `query_type` of AXFR there.
Author
Owner

@bluejekyll commented on GitHub (Mar 2, 2025):

I think maybe we should have an error on query with AXFR, that probably makes sense.

<!-- gh-comment-id:2692900317 --> @bluejekyll commented on GitHub (Mar 2, 2025): I think maybe we should have an error on query with AXFR, that probably makes sense.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/hickory-dns#1028
No description provided.