bindy/bind9/records/
ns.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! NS record management.
5
6use super::super::types::RndcKeyData;
7use super::{
8    build_authenticated_client, build_delete_rrset_record, build_record_fqdn, effective_record_ttl,
9    rrset_ttl_matches, should_update_record,
10};
11use anyhow::Result;
12use hickory_net::client::ClientHandle;
13use hickory_proto::op::ResponseCode;
14use hickory_proto::rr::{rdata, DNSClass, Name, RData, Record, RecordType};
15use std::str::FromStr;
16use tracing::info;
17
18/// Compare existing DNS `RRset` with the desired nameserver and TTL.
19///
20/// # Arguments
21///
22/// * `existing_records` - Records currently in DNS (from query)
23/// * `nameserver` - Desired nameserver host from the spec
24/// * `desired_ttl` - Effective TTL from the spec
25///
26/// # Returns
27///
28/// `true` if the existing `RRset` matches the desired state exactly (no changes
29/// needed), `false` if an update is required (rdata or TTL differ).
30fn compare_ns_rrset(existing_records: &[Record], nameserver: &str, desired_ttl: u32) -> bool {
31    if existing_records.len() != 1 {
32        return false;
33    }
34    if !rrset_ttl_matches(existing_records, desired_ttl) {
35        return false;
36    }
37    let RData::NS(existing_ns) = &existing_records[0].data else {
38        return false;
39    };
40    existing_ns.0.to_string() == nameserver
41}
42
43/// Add an NS record using dynamic DNS update (RFC 2136) with `RRset` synchronization.
44///
45/// NS records managed here are used for delegations. If the existing `RRset`
46/// differs from the desired state, the entire NS `RRset` for the name is
47/// deleted and recreated so stale delegation rdata never lingers.
48///
49/// # Errors
50///
51/// Returns an error if the DNS update fails or the server rejects it.
52#[allow(clippy::too_many_arguments)]
53pub async fn add_ns_record(
54    zone_name: &str,
55    name: &str,
56    nameserver: &str,
57    ttl: Option<i32>,
58    server: &str,
59    key_data: &RndcKeyData,
60) -> Result<()> {
61    let ttl_value = effective_record_ttl(ttl);
62    let should_update = should_update_record(
63        zone_name,
64        name,
65        RecordType::NS,
66        "NS",
67        server,
68        |existing_records| compare_ns_rrset(existing_records, nameserver, ttl_value),
69    )
70    .await?;
71
72    if !should_update {
73        return Ok(());
74    }
75
76    let zone = Name::from_str(zone_name)?;
77    let fqdn = build_record_fqdn(zone_name, name)?;
78    let ns_name = Name::from_str(nameserver)?;
79
80    let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::NS(rdata::NS(ns_name)));
81    record.dns_class = DNSClass::IN;
82
83    info!(
84        "Adding NS record: {} -> {} (TTL: {})",
85        fqdn, nameserver, ttl_value
86    );
87
88    let mut client = build_authenticated_client(server, key_data).await?;
89
90    // Step 1: delete existing RRset (ignore errors — may not exist).
91    let delete_record = build_delete_rrset_record(&fqdn, RecordType::NS);
92    let _ = client.delete_rrset(delete_record, zone.clone()).await;
93
94    // Step 2: append the desired record to create the new RRset.
95    let response = client.append(record, zone, false).await?;
96
97    match response.metadata.response_code {
98        ResponseCode::NoError => {
99            info!("Successfully added NS record: {} -> {}", name, nameserver);
100            Ok(())
101        }
102        code => Err(anyhow::anyhow!(
103            "DNS update failed with response code: {code:?}"
104        )),
105    }
106}
107
108#[cfg(test)]
109#[path = "ns_tests.rs"]
110mod ns_tests;