use super::*;
use crate::security::{sanitize_hostname, sanitize_domain_name};
pub struct DdnsUpdater {
config: DdnsConfig,
}
impl DdnsUpdater {
pub fn new(config: DdnsConfig) -> Self {
Self { config }
}
pub async fn update_v4(
&self,
hostname: &str,
ip_address: Ipv4Addr,
domain: Option<&str>,
) -> Result<()> {
if !self.config.enabled {
debug!("DDNS is disabled, skipping update");
return Ok(());
}
let safe_hostname = sanitize_hostname(hostname)
.ok_or_else(|| DhcpClientError::SecurityViolation(
format!("Invalid hostname for DDNS: {}", hostname)
))?;
let fqdn = if let Some(domain) = domain {
let safe_domain = sanitize_domain_name(domain)
.ok_or_else(|| DhcpClientError::SecurityViolation(
format!("Invalid domain for DDNS: {}", domain)
))?;
format!("{}.{}", safe_hostname, safe_domain)
} else {
safe_hostname
};
info!("Updating DNS: {} -> {}", fqdn, ip_address);
if self.config.update_forward {
self.update_a_record(&fqdn, ip_address).await?;
}
if self.config.update_reverse {
self.update_ptr_record(ip_address, &fqdn).await?;
}
Ok(())
}
pub async fn update_v6(
&self,
hostname: &str,
ip_addresses: &[Ipv6Addr],
domain: Option<&str>,
) -> Result<()> {
if !self.config.enabled {
debug!("DDNS is disabled, skipping update");
return Ok(());
}
let safe_hostname = sanitize_hostname(hostname)
.ok_or_else(|| DhcpClientError::SecurityViolation(
format!("Invalid hostname for DDNS: {}", hostname)
))?;
let fqdn = if let Some(domain) = domain {
let safe_domain = sanitize_domain_name(domain)
.ok_or_else(|| DhcpClientError::SecurityViolation(
format!("Invalid domain for DDNS: {}", domain)
))?;
format!("{}.{}", safe_hostname, safe_domain)
} else {
safe_hostname
};
info!("Updating DNS: {} -> {:?}", fqdn, ip_addresses);
if self.config.update_forward {
for addr in ip_addresses {
self.update_aaaa_record(&fqdn, *addr).await?;
}
}
if self.config.update_reverse {
for addr in ip_addresses {
self.update_ptr_record_v6(*addr, &fqdn).await?;
}
}
Ok(())
}
async fn update_a_record(&self, fqdn: &str, ip_address: Ipv4Addr) -> Result<()> {
debug!("Updating A record: {} -> {}", fqdn, ip_address);
Ok(())
}
async fn update_aaaa_record(&self, fqdn: &str, ip_address: Ipv6Addr) -> Result<()> {
debug!("Updating AAAA record: {} -> {}", fqdn, ip_address);
Ok(())
}
async fn update_ptr_record(&self, ip_address: Ipv4Addr, fqdn: &str) -> Result<()> {
let reverse_name = self.ipv4_to_reverse(ip_address);
debug!("Updating PTR record: {} -> {}", reverse_name, fqdn);
Ok(())
}
async fn update_ptr_record_v6(&self, ip_address: Ipv6Addr, fqdn: &str) -> Result<()> {
let reverse_name = self.ipv6_to_reverse(ip_address);
debug!("Updating PTR record: {} -> {}", reverse_name, fqdn);
Ok(())
}
pub async fn delete(&self, hostname: &str, domain: Option<&str>) -> Result<()> {
if !self.config.enabled {
return Ok(());
}
let safe_hostname = sanitize_hostname(hostname)
.ok_or_else(|| DhcpClientError::SecurityViolation(
format!("Invalid hostname for DDNS delete: {}", hostname)
))?;
let fqdn = if let Some(domain) = domain {
let safe_domain = sanitize_domain_name(domain)
.ok_or_else(|| DhcpClientError::SecurityViolation(
format!("Invalid domain for DDNS delete: {}", domain)
))?;
format!("{}.{}", safe_hostname, safe_domain)
} else {
safe_hostname
};
info!("Deleting DNS records for {}", fqdn);
Ok(())
}
fn ipv4_to_reverse(&self, addr: Ipv4Addr) -> String {
let octets = addr.octets();
format!("{}.{}.{}.{}.in-addr.arpa",
octets[3], octets[2], octets[1], octets[0])
}
fn ipv6_to_reverse(&self, addr: Ipv6Addr) -> String {
let segments = addr.segments();
let mut nibbles = Vec::new();
for segment in segments.iter().rev() {
for i in (0..4).rev() {
nibbles.push(format!("{:x}", (segment >> (i * 4)) & 0xF));
}
}
format!("{}.ip6.arpa", nibbles.join("."))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ipv4_to_reverse() {
let config = DdnsConfig::default();
let updater = DdnsUpdater::new(config);
let addr = Ipv4Addr::new(192, 168, 1, 100);
let reverse = updater.ipv4_to_reverse(addr);
assert_eq!(reverse, "100.1.168.192.in-addr.arpa");
}
#[test]
fn test_ipv6_to_reverse() {
let config = DdnsConfig::default();
let updater = DdnsUpdater::new(config);
let addr = Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1);
let reverse = updater.ipv6_to_reverse(addr);
assert!(reverse.ends_with(".ip6.arpa"));
}
#[tokio::test]
async fn test_ddns_disabled() {
let mut config = DdnsConfig::default();
config.enabled = false;
let updater = DdnsUpdater::new(config);
let result = updater.update_v4("test", Ipv4Addr::new(192, 168, 1, 100), Some("example.com")).await;
assert!(result.is_ok());
}
}