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::{build_authenticated_client, build_record_fqdn, should_update_record};
8use anyhow::Result;
9use hickory_net::client::ClientHandle;
10use hickory_proto::op::ResponseCode;
11use hickory_proto::rr::{rdata, DNSClass, Name, RData, Record, RecordType};
12use std::str::FromStr;
13use tracing::info;
14
15use crate::constants::DEFAULT_DNS_RECORD_TTL_SECS;
16
17/// Add an NS record using dynamic DNS update (RFC 2136).
18///
19/// # Errors
20///
21/// Returns an error if the DNS update fails or the server rejects it.
22#[allow(clippy::too_many_arguments)]
23pub async fn add_ns_record(
24    zone_name: &str,
25    name: &str,
26    nameserver: &str,
27    ttl: Option<i32>,
28    server: &str,
29    key_data: &RndcKeyData,
30) -> Result<()> {
31    let nameserver_for_comparison = nameserver.to_string();
32    let should_update = should_update_record(
33        zone_name,
34        name,
35        RecordType::NS,
36        "NS",
37        server,
38        |existing_records| {
39            if existing_records.len() == 1 {
40                if let RData::NS(existing_ns) = &existing_records[0].data {
41                    return existing_ns.0.to_string() == nameserver_for_comparison;
42                }
43            }
44            false
45        },
46    )
47    .await?;
48
49    if !should_update {
50        return Ok(());
51    }
52
53    let ttl_value = u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
54        .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
55
56    let zone = Name::from_str(zone_name)?;
57    let fqdn = build_record_fqdn(zone_name, name)?;
58    let ns_name = Name::from_str(nameserver)?;
59
60    let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::NS(rdata::NS(ns_name)));
61    record.dns_class = DNSClass::IN;
62
63    info!(
64        "Adding NS record: {} -> {} (TTL: {})",
65        fqdn, nameserver, ttl_value
66    );
67
68    let mut client = build_authenticated_client(server, key_data).await?;
69    let response = client.append(record, zone, false).await?;
70
71    match response.metadata.response_code {
72        ResponseCode::NoError => {
73            info!("Successfully added NS record: {} -> {}", name, nameserver);
74            Ok(())
75        }
76        code => Err(anyhow::anyhow!(
77            "DNS update failed with response code: {code:?}"
78        )),
79    }
80}
81
82#[cfg(test)]
83#[path = "ns_tests.rs"]
84mod ns_tests;