bindy/bind9/records/
srv.rs1use super::super::types::{RndcKeyData, SRVRecordData};
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::{Context, 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
18fn compare_srv_rrset(
34 existing_records: &[Record],
35 priority: u16,
36 weight: u16,
37 port: u16,
38 target: &str,
39 desired_ttl: u32,
40) -> bool {
41 if existing_records.len() != 1 {
42 return false;
43 }
44 if !rrset_ttl_matches(existing_records, desired_ttl) {
45 return false;
46 }
47 let RData::SRV(existing_srv) = &existing_records[0].data else {
48 return false;
49 };
50 existing_srv.priority == priority
51 && existing_srv.weight == weight
52 && existing_srv.port == port
53 && existing_srv.target.to_string() == target
54}
55
56#[allow(clippy::too_many_arguments)]
69pub async fn add_srv_record(
70 zone_name: &str,
71 name: &str,
72 srv_data: &SRVRecordData,
73 server: &str,
74 key_data: &RndcKeyData,
75) -> Result<()> {
76 let priority_u16 = u16::try_from(srv_data.priority)
77 .context(format!("Invalid SRV priority: {}", srv_data.priority))?;
78 let weight_u16 = u16::try_from(srv_data.weight)
79 .context(format!("Invalid SRV weight: {}", srv_data.weight))?;
80 let port_u16 =
81 u16::try_from(srv_data.port).context(format!("Invalid SRV port: {}", srv_data.port))?;
82 let ttl_value = effective_record_ttl(srv_data.ttl);
83
84 let should_update = should_update_record(
85 zone_name,
86 name,
87 RecordType::SRV,
88 "SRV",
89 server,
90 |existing_records| {
91 compare_srv_rrset(
92 existing_records,
93 priority_u16,
94 weight_u16,
95 port_u16,
96 &srv_data.target,
97 ttl_value,
98 )
99 },
100 )
101 .await?;
102
103 if !should_update {
104 return Ok(());
105 }
106
107 let zone =
108 Name::from_str(zone_name).context(format!("Invalid zone name for SRV: {zone_name}"))?;
109 let fqdn = build_record_fqdn(zone_name, name)?;
110 let target_name = Name::from_str(&srv_data.target).context(format!(
111 "Invalid target for SRV record: {}",
112 srv_data.target
113 ))?;
114
115 let record_data = rdata::SRV::new(priority_u16, weight_u16, port_u16, target_name);
116 let mut record = Record::from_rdata(fqdn.clone(), ttl_value, RData::SRV(record_data));
117 record.dns_class = DNSClass::IN;
118
119 let mut client = build_authenticated_client(server, key_data).await?;
120
121 let delete_record = build_delete_rrset_record(&fqdn, RecordType::SRV);
123 let _ = client.delete_rrset(delete_record, zone.clone()).await;
124
125 let response = client
127 .append(record, zone, false)
128 .await
129 .context(format!("Failed to send SRV record update for {fqdn}"))?;
130
131 match response.metadata.response_code {
132 ResponseCode::NoError => {
133 info!(
134 "Successfully added SRV record: {} -> {}:{} (priority: {}, weight: {}, TTL: {})",
135 fqdn, srv_data.target, srv_data.port, srv_data.priority, srv_data.weight, ttl_value
136 );
137 Ok(())
138 }
139 code => {
140 anyhow::bail!("DNS server rejected SRV record update for {fqdn}: {code:?}");
141 }
142 }
143}
144
145#[cfg(test)]
146#[path = "srv_tests.rs"]
147mod srv_tests;