crdhcpc 0.1.1

Standalone DHCP Client for Linux with DHCPv4, DHCPv6, PXE, and Dynamic DNS support
Documentation
//! Dynamic DNS updates
//!
//! Implements RFC 4702 (DHCPv4 Client FQDN) and RFC 4704 (DHCPv6 Client FQDN)

use super::*;
use crate::security::{sanitize_hostname, sanitize_domain_name};

/// Dynamic DNS updater
pub struct DdnsUpdater {
    config: DdnsConfig,
}

impl DdnsUpdater {
    pub fn new(config: DdnsConfig) -> Self {
        Self { config }
    }

    /// Update DNS for DHCPv4 lease
    /// Security: hostname and domain are validated per RFC 1123 before use
    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(());
        }

        // Validate and sanitize hostname
        let safe_hostname = sanitize_hostname(hostname)
            .ok_or_else(|| DhcpClientError::SecurityViolation(
                format!("Invalid hostname for DDNS: {}", hostname)
            ))?;

        // Validate and sanitize domain if provided
        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);

        // Forward update (A record)
        if self.config.update_forward {
            self.update_a_record(&fqdn, ip_address).await?;
        }

        // Reverse update (PTR record)
        if self.config.update_reverse {
            self.update_ptr_record(ip_address, &fqdn).await?;
        }

        Ok(())
    }

    /// Update DNS for DHCPv6 lease
    /// Security: hostname and domain are validated per RFC 1123 before use
    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(());
        }

        // Validate and sanitize hostname
        let safe_hostname = sanitize_hostname(hostname)
            .ok_or_else(|| DhcpClientError::SecurityViolation(
                format!("Invalid hostname for DDNS: {}", hostname)
            ))?;

        // Validate and sanitize domain if provided
        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);

        // Forward update (AAAA records)
        if self.config.update_forward {
            for addr in ip_addresses {
                self.update_aaaa_record(&fqdn, *addr).await?;
            }
        }

        // Reverse update (PTR records)
        if self.config.update_reverse {
            for addr in ip_addresses {
                self.update_ptr_record_v6(*addr, &fqdn).await?;
            }
        }

        Ok(())
    }

    /// Update A record (IPv4 forward)
    async fn update_a_record(&self, fqdn: &str, ip_address: Ipv4Addr) -> Result<()> {
        debug!("Updating A record: {} -> {}", fqdn, ip_address);

        // In real implementation:
        // 1. Create DNS UPDATE message
        // 2. Add TSIG if configured
        // 3. Send to DNS server (from config or DHCP)
        // 4. Wait for response

        // Integration with unbound or use trust-dns-client

        Ok(())
    }

    /// Update AAAA record (IPv6 forward)
    async fn update_aaaa_record(&self, fqdn: &str, ip_address: Ipv6Addr) -> Result<()> {
        debug!("Updating AAAA record: {} -> {}", fqdn, ip_address);
        Ok(())
    }

    /// Update PTR record (IPv4 reverse)
    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(())
    }

    /// Update PTR record (IPv6 reverse)
    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(())
    }

    /// Delete DNS records
    /// Security: hostname and domain are validated per RFC 1123 before use
    pub async fn delete(&self, hostname: &str, domain: Option<&str>) -> Result<()> {
        if !self.config.enabled {
            return Ok(());
        }

        // Validate and sanitize hostname
        let safe_hostname = sanitize_hostname(hostname)
            .ok_or_else(|| DhcpClientError::SecurityViolation(
                format!("Invalid hostname for DDNS delete: {}", hostname)
            ))?;

        // Validate and sanitize domain if provided
        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);

        // Send DNS DELETE message

        Ok(())
    }

    /// Convert IPv4 address to reverse DNS name
    fn ipv4_to_reverse(&self, addr: Ipv4Addr) -> String {
        let octets = addr.octets();
        format!("{}.{}.{}.{}.in-addr.arpa",
                octets[3], octets[2], octets[1], octets[0])
    }

    /// Convert IPv6 address to reverse DNS name
    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());
    }
}