bindy/bind9/records/
srv.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! SRV record management.
5
6use super::super::types::{RndcKeyData, SRVRecordData};
7use super::{build_authenticated_client, build_record_fqdn, should_update_record};
8use anyhow::{Context, 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 SRV record using dynamic DNS update (RFC 2136).
18///
19/// # Errors
20///
21/// Returns an error if:
22/// - DNS server connection fails
23/// - TSIG signer creation fails
24/// - DNS update is rejected by the server
25/// - Invalid domain name or target
26#[allow(clippy::too_many_arguments)]
27pub async fn add_srv_record(
28    zone_name: &str,
29    name: &str,
30    srv_data: &SRVRecordData,
31    server: &str,
32    key_data: &RndcKeyData,
33) -> Result<()> {
34    let srv_data_for_comparison = srv_data.clone();
35    let priority_u16 = u16::try_from(srv_data.priority)
36        .context(format!("Invalid SRV priority: {}", srv_data.priority))?;
37    let weight_u16 = u16::try_from(srv_data.weight)
38        .context(format!("Invalid SRV weight: {}", srv_data.weight))?;
39    let port_u16 =
40        u16::try_from(srv_data.port).context(format!("Invalid SRV port: {}", srv_data.port))?;
41
42    let should_update = should_update_record(
43        zone_name,
44        name,
45        RecordType::SRV,
46        "SRV",
47        server,
48        |existing_records| {
49            if existing_records.len() == 1 {
50                if let RData::SRV(existing_srv) = &existing_records[0].data {
51                    let priority_match = existing_srv.priority
52                        == u16::try_from(srv_data_for_comparison.priority).unwrap_or(0);
53                    let weight_match = existing_srv.weight
54                        == u16::try_from(srv_data_for_comparison.weight).unwrap_or(0);
55                    let port_match = existing_srv.port
56                        == u16::try_from(srv_data_for_comparison.port).unwrap_or(0);
57                    let target_match =
58                        existing_srv.target.to_string() == srv_data_for_comparison.target;
59                    return priority_match && weight_match && port_match && target_match;
60                }
61            }
62            false
63        },
64    )
65    .await?;
66
67    if !should_update {
68        return Ok(());
69    }
70
71    let ttl_value = u32::try_from(srv_data.ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
72        .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
73
74    let zone =
75        Name::from_str(zone_name).context(format!("Invalid zone name for SRV: {zone_name}"))?;
76    let fqdn = build_record_fqdn(zone_name, name)?;
77    let target_name = Name::from_str(&srv_data.target).context(format!(
78        "Invalid target for SRV record: {}",
79        srv_data.target
80    ))?;
81
82    let record_data = rdata::SRV::new(priority_u16, weight_u16, port_u16, target_name);
83    let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::SRV(record_data));
84    record.dns_class = DNSClass::IN;
85
86    let mut client = build_authenticated_client(server, key_data).await?;
87    let response = client
88        .append(record, zone, false)
89        .await
90        .context(format!("Failed to send SRV record update for {fqdn}"))?;
91
92    match response.metadata.response_code {
93        ResponseCode::NoError => {
94            info!(
95                "Successfully added SRV record: {} -> {}:{} (priority: {}, weight: {}, TTL: {})",
96                fqdn, srv_data.target, srv_data.port, srv_data.priority, srv_data.weight, ttl_value
97            );
98            Ok(())
99        }
100        code => {
101            anyhow::bail!("DNS server rejected SRV record update for {fqdn}: {code:?}");
102        }
103    }
104}
105
106#[cfg(test)]
107#[path = "srv_tests.rs"]
108mod srv_tests;