hickory-dns / hickory-dns

A Rust based DNS client, server, and resolver
Other
4.07k stars 463 forks source link

[Question] Possible to send multiple DDNS updates in a single request? #1603

Open chenxiaolong opened 2 years ago

chenxiaolong commented 2 years ago

What is the question?

As I understand it, with dynamic DNS, to atomically replace all A records for a host and add a new one, both the deletion and creation need to be done in the same request (assuming the server supports atomic updates). With nsupdate, I'd do something like this:

nsupdate -d << EOF
server x.x.x.x 53
zone domain.tld
update delete host.domain.tld A
update add host.domain.tld 300 A y.y.y.y
key hmac-sha512:TSIG_NAME TSIG_KEY_BASE64
send
EOF

Looking at a packet capture, that gives me a single request (1 packet) with two entries in the update section:

host.domain.tld: type A, class ANY
host.domain.tld: type A, class IN, addr 8.8.8.8

I was able to mostly implement this using trust-dns thanks to the recent TSIG support, but it seems that each call to eg. SyncClient::delete_rrset/create/append sends a new request. Is there any API for sending multiple updates in a single request?

Thank you!

bluejekyll commented 2 years ago

Does compare and swap facilitate what you want? https://docs.rs/trust-dns-client/latest/trust_dns_client/client/trait.Client.html#method.compare_and_swap

Or do you want to blindly delete all records and add a new set? We could add a replace method that does what you want.

chenxiaolong commented 2 years ago

Thank you, I had overlooked that method. Blind-replace-all semantics is indeed what I'm after. I'll play around and see if query + compare_and_swap can do the trick.

(Looking at the source for compare_and_swap, the low-level trust_dns_client::op::Message API seems pretty straight-forward as well. Seems like I can go that route if needed.)

chenxiaolong commented 2 years ago

I got this working using the low level Message API and it lets me handle both A and AAAA at the same time. I'm not sure if it's worth having a high level wrapper for this--I'm perfectly happy with just using Message.

fn replace_addrs_message(
    zone_origin: Name,
    name: Name,
    ttl: u32,
    addrs: &[IpAddr],
) -> Message {
    // trust_dns_client::client::AsyncClient::MAX_PAYLOAD_LEN is not public
    const MAX_PAYLOAD_LEN: u16 = 1232;

    let mut zone = Query::new();
    zone.set_name(zone_origin)
        .set_query_class(DNSClass::IN)
        .set_query_type(RecordType::SOA);

    let mut message = Message::new();
    message
        .set_id(rand::random())
        .set_message_type(MessageType::Query)
        .set_op_code(OpCode::Update)
        .set_recursion_desired(false);
    message.add_zone(zone);

    for rtype in [RecordType::A, RecordType::AAAA] {
        let mut record = Record::with(name.clone(), rtype, 0);
        record.set_dns_class(DNSClass::ANY);
        message.add_update(record);
    }

    for addr in addrs {
        let rdata = match addr {
            IpAddr::V4(ip) => RData::A(*ip),
            IpAddr::V6(ip) => RData::AAAA(*ip),
        };

        message.add_update(Record::from_rdata(name.clone(), ttl, rdata));
    }

    let edns = message.edns_mut();
    edns.set_max_payload(MAX_PAYLOAD_LEN);
    edns.set_version(0);

    message
}