bindy/bind9/records/
cname.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! CNAME record management.
5
6use super::super::types::RndcKeyData;
7use super::{
8    build_authenticated_client, build_record_fqdn, effective_record_ttl, rrset_ttl_matches,
9    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 CNAME target and TTL.
19///
20/// # Arguments
21///
22/// * `existing_records` - Records currently in DNS (from query)
23/// * `target` - Desired CNAME target 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_cname_rrset(existing_records: &[Record], target: &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::CNAME(existing_cname) = &existing_records[0].data else {
38        return false;
39    };
40    existing_cname.0.to_string() == target
41}
42
43/// Add a CNAME record using dynamic DNS update (RFC 2136).
44///
45/// # Errors
46///
47/// Returns an error if the DNS update fails or the server rejects it.
48#[allow(clippy::too_many_arguments)]
49pub async fn add_cname_record(
50    zone_name: &str,
51    name: &str,
52    target: &str,
53    ttl: Option<i32>,
54    server: &str,
55    key_data: &RndcKeyData,
56) -> Result<()> {
57    let ttl_value = effective_record_ttl(ttl);
58    let should_update = should_update_record(
59        zone_name,
60        name,
61        RecordType::CNAME,
62        "CNAME",
63        server,
64        |existing_records| compare_cname_rrset(existing_records, target, ttl_value),
65    )
66    .await?;
67
68    if !should_update {
69        return Ok(());
70    }
71
72    let zone = Name::from_str(zone_name)?;
73    let fqdn = build_record_fqdn(zone_name, name)?;
74    let target_name = Name::from_str(target)?;
75
76    let mut record = Record::from_rdata(
77        fqdn.clone(),
78        ttl_value,
79        RData::CNAME(rdata::CNAME(target_name)),
80    );
81    record.dns_class = DNSClass::IN;
82
83    info!(
84        "Adding CNAME record: {} -> {} (TTL: {})",
85        record.name, target, ttl_value
86    );
87
88    let mut client = build_authenticated_client(server, key_data).await?;
89    let response = client.append(record, zone, false).await?;
90
91    match response.metadata.response_code {
92        ResponseCode::NoError => {
93            info!("Successfully added CNAME record: {} -> {}", name, target);
94            Ok(())
95        }
96        code => Err(anyhow::anyhow!(
97            "DNS update failed with response code: {code:?}"
98        )),
99    }
100}
101
102#[cfg(test)]
103#[path = "cname_tests.rs"]
104mod cname_tests;