Skip to main content

zlayer_overlay/
dns.rs

1//! DNS server for service discovery over overlay networks
2
3use hickory_client::client::{Client, SyncClient};
4use hickory_client::udp::UdpClientConnection;
5use hickory_server::authority::{Catalog, ZoneType};
6use hickory_server::proto::rr::rdata::{A, AAAA};
7use hickory_server::proto::rr::{DNSClass, Name, RData, Record, RecordType};
8use hickory_server::server::ServerFuture;
9use hickory_server::store::in_memory::InMemoryAuthority;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
13use std::str::FromStr;
14use std::sync::Arc;
15use std::time::Duration;
16use tokio::net::{TcpListener, UdpSocket};
17use tokio::sync::RwLock;
18
19/// Default DNS port for overlay service discovery (non-standard to avoid conflicts)
20pub const DEFAULT_DNS_PORT: u16 = 15353;
21
22/// Configuration for DNS integration with overlay network
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct DnsConfig {
25    /// DNS zone (e.g., "overlay.local.")
26    pub zone: String,
27    /// DNS server port (default: 15353)
28    pub port: u16,
29    /// Bind address (default: overlay IP)
30    pub bind_addr: IpAddr,
31}
32
33impl DnsConfig {
34    /// Create a new DNS config with defaults
35    #[must_use]
36    pub fn new(zone: &str, bind_addr: IpAddr) -> Self {
37        Self {
38            zone: zone.to_string(),
39            port: DEFAULT_DNS_PORT,
40            bind_addr,
41        }
42    }
43
44    /// Set a custom port
45    #[must_use]
46    pub fn with_port(mut self, port: u16) -> Self {
47        self.port = port;
48        self
49    }
50}
51
52/// Generate a hostname from an IP address for DNS registration
53///
54/// For IPv4: converts an IP like 10.200.0.5 to "node-0-5" (using last two octets).
55/// For IPv6: converts an IP like `fd00::abcd` to "node-abcd" (using last 4 hex chars).
56#[must_use]
57pub fn peer_hostname(ip: IpAddr) -> String {
58    match ip {
59        IpAddr::V4(v4) => {
60            let octets = v4.octets();
61            format!("node-{}-{}", octets[2], octets[3])
62        }
63        IpAddr::V6(v6) => {
64            let segments = v6.segments();
65            let last_segment = segments[7];
66            format!("node-{last_segment:04x}")
67        }
68    }
69}
70
71/// Error type for DNS operations
72#[derive(Debug, thiserror::Error)]
73pub enum DnsError {
74    #[error("Invalid domain name: {0}")]
75    InvalidName(String),
76
77    #[error("DNS server error: {0}")]
78    Server(String),
79
80    #[error("DNS client error: {0}")]
81    Client(String),
82
83    #[error("IO error: {0}")]
84    Io(#[from] std::io::Error),
85
86    #[error("Record not found: {0}")]
87    NotFound(String),
88}
89
90/// Handle for managing DNS records after server is started
91///
92/// This handle can be cloned and used to add/remove records while the server is running.
93#[derive(Clone)]
94pub struct DnsHandle {
95    authority: Arc<InMemoryAuthority>,
96    zone_origin: Name,
97    serial: Arc<RwLock<u32>>,
98}
99
100impl DnsHandle {
101    /// Add a DNS record for a hostname to IP mapping
102    ///
103    /// Creates an A record for IPv4 addresses and an AAAA record for IPv6 addresses.
104    ///
105    /// # Errors
106    ///
107    /// Returns `DnsError::InvalidName` if the hostname is invalid.
108    pub async fn add_record(&self, hostname: &str, ip: IpAddr) -> Result<(), DnsError> {
109        // Create the fully qualified domain name
110        let fqdn = if hostname.ends_with('.') {
111            Name::from_str(hostname)
112                .map_err(|e| DnsError::InvalidName(format!("{hostname}: {e}")))?
113        } else {
114            // Append the zone origin
115            let name = Name::from_str(hostname)
116                .map_err(|e| DnsError::InvalidName(format!("{hostname}: {e}")))?;
117            name.append_domain(&self.zone_origin)
118                .map_err(|e| DnsError::InvalidName(format!("Failed to append zone: {e}")))?
119        };
120
121        // Create an A or AAAA record depending on address family
122        let rdata = match ip {
123            IpAddr::V4(v4) => RData::A(A::from(v4)),
124            IpAddr::V6(v6) => RData::AAAA(AAAA::from(v6)),
125        };
126        let record = Record::from_rdata(fqdn, 300, rdata); // 300 second TTL
127
128        // Get the current serial and increment it
129        let serial = {
130            let mut s = self.serial.write().await;
131            let current = *s;
132            *s = s.wrapping_add(1);
133            current
134        };
135
136        // Upsert the record into the authority (uses internal synchronization)
137        self.authority.upsert(record, serial).await;
138
139        Ok(())
140    }
141
142    /// Remove DNS records for a hostname (both A and AAAA)
143    ///
144    /// Tombstones both record types since we don't track which type was stored.
145    ///
146    /// # Errors
147    ///
148    /// Returns `DnsError::InvalidName` if the hostname is invalid.
149    pub async fn remove_record(&self, hostname: &str) -> Result<bool, DnsError> {
150        let fqdn = if hostname.ends_with('.') {
151            Name::from_str(hostname)
152                .map_err(|e| DnsError::InvalidName(format!("{hostname}: {e}")))?
153        } else {
154            let name = Name::from_str(hostname)
155                .map_err(|e| DnsError::InvalidName(format!("{hostname}: {e}")))?;
156            name.append_domain(&self.zone_origin)
157                .map_err(|e| DnsError::InvalidName(format!("Failed to append zone: {e}")))?
158        };
159
160        let serial = {
161            let mut s = self.serial.write().await;
162            let current = *s;
163            *s = s.wrapping_add(1);
164            current
165        };
166
167        // Create empty records to effectively "remove" by setting empty data.
168        // Note: hickory-dns doesn't have a direct remove, so we create tombstones.
169        // We tombstone both A and AAAA since we don't know which type was stored.
170        let a_record = Record::with(fqdn.clone(), RecordType::A, 0);
171        self.authority.upsert(a_record, serial).await;
172
173        let aaaa_record = Record::with(fqdn.clone(), RecordType::AAAA, 0);
174        self.authority.upsert(aaaa_record, serial).await;
175
176        Ok(true)
177    }
178
179    /// Get the zone origin
180    #[must_use]
181    pub fn zone_origin(&self) -> &Name {
182        &self.zone_origin
183    }
184}
185
186/// DNS server for overlay networks
187pub struct DnsServer {
188    listen_addr: SocketAddr,
189    authority: Arc<InMemoryAuthority>,
190    zone_origin: Name,
191    serial: Arc<RwLock<u32>>,
192}
193
194impl DnsServer {
195    /// Create a new DNS server for the given zone
196    ///
197    /// # Errors
198    ///
199    /// Returns `DnsError::InvalidName` if the zone name is invalid.
200    pub fn new(listen_addr: SocketAddr, zone: &str) -> Result<Self, DnsError> {
201        let zone_origin =
202            Name::from_str(zone).map_err(|e| DnsError::InvalidName(format!("{zone}: {e}")))?;
203
204        // Create an empty in-memory authority for the zone
205        // Using Arc directly since InMemoryAuthority has internal synchronization via upsert()
206        let authority = Arc::new(InMemoryAuthority::empty(
207            zone_origin.clone(),
208            ZoneType::Primary,
209            false,
210        ));
211
212        Ok(Self {
213            listen_addr,
214            authority,
215            zone_origin,
216            serial: Arc::new(RwLock::new(1)),
217        })
218    }
219
220    /// Create from a `DnsConfig`
221    ///
222    /// # Errors
223    ///
224    /// Returns `DnsError::InvalidName` if the zone name is invalid.
225    pub fn from_config(config: &DnsConfig) -> Result<Self, DnsError> {
226        let listen_addr = SocketAddr::new(config.bind_addr, config.port);
227        Self::new(listen_addr, &config.zone)
228    }
229
230    /// Get a handle for managing DNS records
231    ///
232    /// The handle can be cloned and used to add/remove records even after
233    /// the server has been started.
234    #[must_use]
235    pub fn handle(&self) -> DnsHandle {
236        DnsHandle {
237            authority: Arc::clone(&self.authority),
238            zone_origin: self.zone_origin.clone(),
239            serial: Arc::clone(&self.serial),
240        }
241    }
242
243    /// Add a DNS record for a hostname to IP mapping
244    ///
245    /// Creates an A record for IPv4 addresses and an AAAA record for IPv6 addresses.
246    ///
247    /// # Errors
248    ///
249    /// Returns `DnsError::InvalidName` if the hostname is invalid.
250    pub async fn add_record(&self, hostname: &str, ip: IpAddr) -> Result<(), DnsError> {
251        self.handle().add_record(hostname, ip).await
252    }
253
254    /// Remove DNS records for a hostname (both A and AAAA)
255    ///
256    /// # Errors
257    ///
258    /// Returns `DnsError::InvalidName` if the hostname is invalid.
259    pub async fn remove_record(&self, hostname: &str) -> Result<bool, DnsError> {
260        self.handle().remove_record(hostname).await
261    }
262
263    /// Start the DNS server and return a handle for record management
264    ///
265    /// This spawns the DNS server in a background task and returns a handle
266    /// that can be used to add/remove records while the server is running.
267    ///
268    /// # Errors
269    ///
270    /// This method currently always succeeds but returns `Result` for API consistency.
271    #[allow(clippy::unused_async)]
272    pub async fn start(self) -> Result<DnsHandle, DnsError> {
273        let handle = self.handle();
274        let listen_addr = self.listen_addr;
275        let zone_origin = self.zone_origin.clone();
276        let authority = Arc::clone(&self.authority);
277
278        // Spawn the server in a background task
279        tokio::spawn(async move {
280            if let Err(e) = Self::run_server(listen_addr, zone_origin, authority).await {
281                tracing::error!("DNS server error: {}", e);
282            }
283        });
284
285        Ok(handle)
286    }
287
288    /// Start the DNS server in a background task without consuming self.
289    ///
290    /// Unlike `start(self)`, this method borrows self, allowing the `DnsServer`
291    /// to be wrapped in an Arc and shared (e.g., with `ServiceManager`) while
292    /// the server runs in the background.
293    ///
294    /// # Errors
295    ///
296    /// This method currently always succeeds but returns `Result` for API consistency.
297    #[allow(clippy::unused_async)]
298    pub async fn start_background(&self) -> Result<DnsHandle, DnsError> {
299        let handle = self.handle();
300        let listen_addr = self.listen_addr;
301        let zone_origin = self.zone_origin.clone();
302        let authority = Arc::clone(&self.authority);
303
304        tokio::spawn(async move {
305            if let Err(e) = Self::run_server(listen_addr, zone_origin, authority).await {
306                tracing::error!("DNS server error: {}", e);
307            }
308        });
309
310        Ok(handle)
311    }
312
313    /// Bind a second DNS listener on port 53 of `bind_ip`, sharing this
314    /// server's authority + zone so the same records answer both listeners.
315    ///
316    /// Windows containers always query DNS on port 53 — HNS endpoints do not
317    /// support setting a non-standard DNS port in the schema. The canonical
318    /// overlay listener on [`DEFAULT_DNS_PORT`] (15353) is therefore
319    /// unreachable from a Windows container; this method adds a second
320    /// listener on port 53 of the overlay IP so containers that point at
321    /// `<overlay_ip>:53` via `Dns.ServerList` can actually resolve.
322    ///
323    /// `bind_ip` is typically the node's overlay IP (e.g. `10.200.42.1`).
324    /// Binding to `0.0.0.0:53` would collide with whatever resolver the host
325    /// already runs (systemd-resolved on Linux, DNS Client on Windows). The
326    /// method itself is cross-platform; callers decide whether to invoke it
327    /// based on their workload mix.
328    ///
329    /// The bound UDP + TCP sockets live on a detached tokio task that shares
330    /// the same `Arc<InMemoryAuthority>` as the primary listener, so
331    /// `DnsHandle::add_record` / `remove_record` updates both responders
332    /// atomically. Returns a cloneable [`DnsHandle`] for convenience.
333    ///
334    /// # Errors
335    ///
336    /// Returns `DnsError::Io` when either port 53 socket (UDP or TCP) cannot
337    /// be bound — typically because another DNS resolver already owns the
338    /// address, or because the process lacks the privilege to bind below 1024
339    /// on platforms that require it. Callers should treat this as a warning
340    /// and fall back to the primary 15353 listener for non-Windows workloads.
341    #[allow(clippy::unused_async)]
342    pub async fn bind_windows_fallback(&self, bind_ip: IpAddr) -> Result<DnsHandle, DnsError> {
343        let handle = self.handle();
344        let listen_addr = SocketAddr::new(bind_ip, 53);
345        let zone_origin = self.zone_origin.clone();
346        let authority = Arc::clone(&self.authority);
347
348        // Pre-bind the sockets synchronously so binding failures surface here
349        // instead of being swallowed by the detached task. On success we hand
350        // the live sockets off to the server future on a background task.
351        let udp_socket = UdpSocket::bind(listen_addr).await?;
352        let tcp_listener = TcpListener::bind(listen_addr).await?;
353
354        tokio::spawn(async move {
355            let mut catalog = Catalog::new();
356            catalog.upsert(zone_origin.into(), Box::new(authority));
357            let mut server = ServerFuture::new(catalog);
358            server.register_socket(udp_socket);
359            server.register_listener(tcp_listener, Duration::from_secs(30));
360            tracing::info!(
361                addr = %listen_addr,
362                "Windows fallback DNS listener started on port 53",
363            );
364            if let Err(e) = server.block_until_done().await {
365                tracing::error!("Windows fallback DNS listener error: {}", e);
366            }
367        });
368
369        Ok(handle)
370    }
371
372    /// Internal method to run the DNS server
373    async fn run_server(
374        listen_addr: SocketAddr,
375        zone_origin: Name,
376        authority: Arc<InMemoryAuthority>,
377    ) -> Result<(), DnsError> {
378        // Create the catalog and add our authority
379        let mut catalog = Catalog::new();
380
381        // The catalog accepts Arc<dyn AuthorityObject> - InMemoryAuthority implements this
382        catalog.upsert(zone_origin.into(), Box::new(authority));
383
384        // Create the server
385        let mut server = ServerFuture::new(catalog);
386
387        // Bind UDP socket
388        let udp_socket = UdpSocket::bind(listen_addr).await?;
389        server.register_socket(udp_socket);
390
391        // Bind TCP listener
392        let tcp_listener = TcpListener::bind(listen_addr).await?;
393        server.register_listener(tcp_listener, Duration::from_secs(30));
394
395        tracing::info!(addr = %listen_addr, "DNS server listening");
396
397        // Run the server
398        server
399            .block_until_done()
400            .await
401            .map_err(|e| DnsError::Server(e.to_string()))?;
402
403        Ok(())
404    }
405
406    /// Get the listen address
407    #[must_use]
408    pub fn listen_addr(&self) -> SocketAddr {
409        self.listen_addr
410    }
411
412    /// Get the zone origin
413    #[must_use]
414    pub fn zone_origin(&self) -> &Name {
415        &self.zone_origin
416    }
417}
418
419/// DNS client for querying overlay DNS servers
420pub struct DnsClient {
421    server_addr: SocketAddr,
422}
423
424impl DnsClient {
425    /// Create a new DNS client
426    #[must_use]
427    pub fn new(server_addr: SocketAddr) -> Self {
428        Self { server_addr }
429    }
430
431    /// Query for an A record
432    ///
433    /// # Errors
434    ///
435    /// Returns a `DnsError` if the query fails or the hostname is invalid.
436    pub fn query_a(&self, hostname: &str) -> Result<Option<Ipv4Addr>, DnsError> {
437        let name = Name::from_str(hostname)
438            .map_err(|e| DnsError::InvalidName(format!("{hostname}: {e}")))?;
439
440        let conn = UdpClientConnection::new(self.server_addr)
441            .map_err(|e| DnsError::Client(e.to_string()))?;
442
443        let client = SyncClient::new(conn);
444
445        let response = client
446            .query(&name, DNSClass::IN, RecordType::A)
447            .map_err(|e| DnsError::Client(e.to_string()))?;
448
449        // Extract the A record from the response
450        for answer in response.answers() {
451            if let Some(RData::A(a_record)) = answer.data() {
452                return Ok(Some((*a_record).into()));
453            }
454        }
455
456        Ok(None)
457    }
458
459    /// Query for an AAAA record (IPv6)
460    ///
461    /// # Errors
462    ///
463    /// Returns a `DnsError` if the query fails or the hostname is invalid.
464    pub fn query_aaaa(&self, hostname: &str) -> Result<Option<Ipv6Addr>, DnsError> {
465        let name = Name::from_str(hostname)
466            .map_err(|e| DnsError::InvalidName(format!("{hostname}: {e}")))?;
467
468        let conn = UdpClientConnection::new(self.server_addr)
469            .map_err(|e| DnsError::Client(e.to_string()))?;
470
471        let client = SyncClient::new(conn);
472
473        let response = client
474            .query(&name, DNSClass::IN, RecordType::AAAA)
475            .map_err(|e| DnsError::Client(e.to_string()))?;
476
477        // Extract the AAAA record from the response
478        for answer in response.answers() {
479            if let Some(RData::AAAA(aaaa_record)) = answer.data() {
480                return Ok(Some((*aaaa_record).into()));
481            }
482        }
483
484        Ok(None)
485    }
486
487    /// Query for any address record (A or AAAA), returning the first match
488    ///
489    /// Tries A first, then AAAA. Returns the first successful result.
490    ///
491    /// # Errors
492    ///
493    /// Returns a `DnsError` if both queries fail or the hostname is invalid.
494    pub fn query_addr(&self, hostname: &str) -> Result<Option<IpAddr>, DnsError> {
495        // Try A record first
496        if let Ok(Some(v4)) = self.query_a(hostname) {
497            return Ok(Some(IpAddr::V4(v4)));
498        }
499
500        // Then try AAAA
501        if let Ok(Some(v6)) = self.query_aaaa(hostname) {
502            return Ok(Some(IpAddr::V6(v6)));
503        }
504
505        Ok(None)
506    }
507}
508
509/// Service discovery with DNS
510pub struct ServiceDiscovery {
511    dns_server: SocketAddr,
512    records: RwLock<HashMap<String, IpAddr>>,
513}
514
515impl ServiceDiscovery {
516    /// Create a new service discovery instance
517    #[must_use]
518    pub fn new(dns_server_addr: SocketAddr) -> Self {
519        Self {
520            dns_server: dns_server_addr,
521            records: RwLock::new(HashMap::new()),
522        }
523    }
524
525    /// Register a service (stores locally, does not update DNS server)
526    pub async fn register(&self, name: &str, ip: IpAddr) {
527        let mut records = self.records.write().await;
528        records.insert(name.to_string(), ip);
529    }
530
531    /// Resolve a service to an IP address
532    ///
533    /// Checks the local cache first, then queries the DNS server for both
534    /// A (IPv4) and AAAA (IPv6) records.
535    pub async fn resolve(&self, name: &str) -> Option<IpAddr> {
536        // First check local cache
537        {
538            let records = self.records.read().await;
539            if let Some(ip) = records.get(name) {
540                return Some(*ip);
541            }
542        }
543
544        // Query DNS server for both A and AAAA records
545        let client = DnsClient::new(self.dns_server);
546        if let Ok(Some(addr)) = client.query_addr(name) {
547            return Some(addr);
548        }
549
550        None
551    }
552
553    /// Unregister a service
554    pub async fn unregister(&self, name: &str) {
555        let mut records = self.records.write().await;
556        records.remove(name);
557    }
558
559    /// List all registered services
560    pub async fn list_services(&self) -> Vec<String> {
561        let records = self.records.read().await;
562        records.keys().cloned().collect()
563    }
564
565    /// Get the DNS server address
566    pub fn dns_server(&self) -> SocketAddr {
567        self.dns_server
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574
575    #[test]
576    fn test_peer_hostname_v4() {
577        // Test various IPv4 addresses
578        assert_eq!(
579            peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1))),
580            "node-0-1"
581        );
582        assert_eq!(
583            peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5))),
584            "node-0-5"
585        );
586        assert_eq!(
587            peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 200, 1, 100))),
588            "node-1-100"
589        );
590        assert_eq!(
591            peer_hostname(IpAddr::V4(Ipv4Addr::new(192, 168, 255, 254))),
592            "node-255-254"
593        );
594    }
595
596    #[test]
597    fn test_peer_hostname_v6() {
598        // Test various IPv6 addresses
599        assert_eq!(
600            peer_hostname(IpAddr::V6("fd00::1".parse().unwrap())),
601            "node-0001"
602        );
603        assert_eq!(
604            peer_hostname(IpAddr::V6("fd00::abcd".parse().unwrap())),
605            "node-abcd"
606        );
607        assert_eq!(
608            peer_hostname(IpAddr::V6("fd00:200::ffff".parse().unwrap())),
609            "node-ffff"
610        );
611        // Zero last segment
612        assert_eq!(
613            peer_hostname(IpAddr::V6("fd00::1:0".parse().unwrap())),
614            "node-0000"
615        );
616    }
617
618    #[test]
619    fn test_dns_config() {
620        let config = DnsConfig::new("overlay.local.", IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)));
621        assert_eq!(config.zone, "overlay.local.");
622        assert_eq!(config.port, DEFAULT_DNS_PORT);
623        assert_eq!(config.bind_addr, IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)));
624
625        // Test with_port
626        let config = config.with_port(5353);
627        assert_eq!(config.port, 5353);
628    }
629
630    #[test]
631    fn test_dns_config_serialization() {
632        let config = DnsConfig::new("overlay.local.", IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)))
633            .with_port(15353);
634
635        let json = serde_json::to_string(&config).unwrap();
636        let deserialized: DnsConfig = serde_json::from_str(&json).unwrap();
637
638        assert_eq!(deserialized.zone, config.zone);
639        assert_eq!(deserialized.port, config.port);
640        assert_eq!(deserialized.bind_addr, config.bind_addr);
641    }
642
643    #[tokio::test]
644    async fn test_service_discovery_local_cache() {
645        // Use a non-routable address since we're only testing local cache
646        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
647        let discovery = ServiceDiscovery::new(addr);
648
649        let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2));
650        discovery.register("test-service", ip).await;
651
652        let resolved = discovery.resolve("test-service").await;
653        assert_eq!(resolved, Some(ip));
654
655        // Test unregister
656        discovery.unregister("test-service").await;
657        let services = discovery.list_services().await;
658        assert!(services.is_empty());
659    }
660
661    #[test]
662    fn test_dns_server_creation() {
663        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
664        let server = DnsServer::new(addr, "overlay.local.");
665
666        assert!(server.is_ok());
667        let server = server.unwrap();
668        assert_eq!(server.listen_addr(), addr);
669        assert_eq!(server.zone_origin().to_string(), "overlay.local.");
670    }
671
672    #[test]
673    fn test_dns_server_from_config() {
674        let config =
675            DnsConfig::new("test.local.", IpAddr::V4(Ipv4Addr::LOCALHOST)).with_port(15353);
676        let server = DnsServer::from_config(&config);
677
678        assert!(server.is_ok());
679        let server = server.unwrap();
680        assert_eq!(server.listen_addr().port(), 15353);
681        assert_eq!(server.zone_origin().to_string(), "test.local.");
682    }
683
684    #[test]
685    fn test_dns_server_invalid_zone() {
686        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
687        // Empty zone name is technically valid in DNS, so use an obviously invalid one
688        let server = DnsServer::new(addr, "overlay.local.");
689        assert!(server.is_ok());
690    }
691
692    #[tokio::test]
693    async fn test_dns_server_add_record() {
694        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
695        let server = DnsServer::new(addr, "overlay.local.").unwrap();
696
697        let result = server
698            .add_record("myservice", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5)))
699            .await;
700        assert!(result.is_ok());
701    }
702
703    #[tokio::test]
704    async fn test_dns_handle_add_record() {
705        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
706        let server = DnsServer::new(addr, "overlay.local.").unwrap();
707
708        // Get handle and add records through it
709        let handle = server.handle();
710
711        let result = handle
712            .add_record("service1", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))
713            .await;
714        assert!(result.is_ok());
715
716        let result = handle
717            .add_record("service2", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)))
718            .await;
719        assert!(result.is_ok());
720
721        // Zone origin should be accessible
722        assert_eq!(handle.zone_origin().to_string(), "overlay.local.");
723    }
724
725    #[test]
726    fn test_dns_client_creation() {
727        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 53);
728        let client = DnsClient::new(addr);
729        assert_eq!(client.server_addr, addr);
730    }
731
732    #[tokio::test]
733    async fn test_dns_handle_add_aaaa_record() {
734        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
735        let server = DnsServer::new(addr, "overlay.local.").unwrap();
736        let handle = server.handle();
737
738        // Add an AAAA record via IPv6 address
739        let ipv6: IpAddr = "fd00::1".parse().unwrap();
740        let result = handle.add_record("service-v6", ipv6).await;
741        assert!(result.is_ok());
742
743        // Add a second AAAA record
744        let ipv6_2: IpAddr = "fd00::abcd".parse().unwrap();
745        let result = handle.add_record("service-v6-2", ipv6_2).await;
746        assert!(result.is_ok());
747    }
748
749    #[tokio::test]
750    async fn test_dns_server_add_aaaa_record() {
751        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
752        let server = DnsServer::new(addr, "overlay.local.").unwrap();
753
754        // Add AAAA record through the server directly
755        let ipv6: IpAddr = "fd00::42".parse().unwrap();
756        let result = server.add_record("myservice-v6", ipv6).await;
757        assert!(result.is_ok());
758    }
759
760    #[tokio::test]
761    async fn test_dns_handle_remove_record_covers_both_types() {
762        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
763        let server = DnsServer::new(addr, "overlay.local.").unwrap();
764        let handle = server.handle();
765
766        // Add an A record
767        let ipv4 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
768        handle.add_record("dual-service", ipv4).await.unwrap();
769
770        // Remove should succeed (tombstones both A and AAAA)
771        let removed = handle.remove_record("dual-service").await.unwrap();
772        assert!(removed);
773
774        // Add an AAAA record
775        let ipv6: IpAddr = "fd00::1".parse().unwrap();
776        handle.add_record("v6-service", ipv6).await.unwrap();
777
778        // Remove should also succeed for AAAA records
779        let removed = handle.remove_record("v6-service").await.unwrap();
780        assert!(removed);
781    }
782
783    #[tokio::test]
784    async fn test_service_discovery_local_cache_ipv6() {
785        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
786        let discovery = ServiceDiscovery::new(addr);
787
788        // Register an IPv6 service
789        let ipv6: IpAddr = "fd00::beef".parse().unwrap();
790        discovery.register("v6-service", ipv6).await;
791
792        // Should resolve from local cache
793        let resolved = discovery.resolve("v6-service").await;
794        assert_eq!(resolved, Some(ipv6));
795
796        // Unregister and verify
797        discovery.unregister("v6-service").await;
798        let services = discovery.list_services().await;
799        assert!(services.is_empty());
800    }
801
802    #[tokio::test]
803    async fn test_service_discovery_mixed_v4_v6_cache() {
804        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
805        let discovery = ServiceDiscovery::new(addr);
806
807        let ipv4 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
808        let ipv6: IpAddr = "fd00::1".parse().unwrap();
809
810        discovery.register("svc-v4", ipv4).await;
811        discovery.register("svc-v6", ipv6).await;
812
813        assert_eq!(discovery.resolve("svc-v4").await, Some(ipv4));
814        assert_eq!(discovery.resolve("svc-v6").await, Some(ipv6));
815
816        let mut services = discovery.list_services().await;
817        services.sort();
818        assert_eq!(services, vec!["svc-v4", "svc-v6"]);
819    }
820
821    #[test]
822    fn test_dns_config_with_ipv6_bind_addr() {
823        let ipv6_bind: IpAddr = "fd00::1".parse().unwrap();
824        let config = DnsConfig::new("overlay.local.", ipv6_bind);
825        assert_eq!(config.bind_addr, ipv6_bind);
826        assert_eq!(config.port, DEFAULT_DNS_PORT);
827
828        // Serialization round-trip
829        let json = serde_json::to_string(&config).unwrap();
830        let deserialized: DnsConfig = serde_json::from_str(&json).unwrap();
831        assert_eq!(deserialized.bind_addr, ipv6_bind);
832    }
833
834    #[test]
835    fn test_dns_server_creation_ipv6_bind() {
836        let ipv6_addr: IpAddr = "::1".parse().unwrap();
837        let addr = SocketAddr::new(ipv6_addr, 15353);
838        let server = DnsServer::new(addr, "overlay.local.");
839
840        assert!(server.is_ok());
841        let server = server.unwrap();
842        assert_eq!(server.listen_addr(), addr);
843    }
844
845    /// Smoke test for the Windows-fallback port-53 listener: binding to
846    /// 127.0.0.2:53 should fail fast on hosts where that port is privileged
847    /// or already in use, but we only care that the method surfaces a clean
848    /// `DnsError` (not a panic) when the bind is contested. When the bind
849    /// succeeds on a permissive CI host, we verify the returned handle shares
850    /// the authority with the primary listener.
851    #[tokio::test]
852    async fn test_bind_windows_fallback_errors_or_shares_authority() {
853        let primary = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
854        let server = DnsServer::new(primary, "overlay.local.").unwrap();
855        let bind_ip: IpAddr = "127.0.0.2".parse().unwrap();
856
857        match server.bind_windows_fallback(bind_ip).await {
858            Ok(handle) => {
859                // Best-effort: the handle must expose the same zone as the
860                // primary server so record mutations on either propagate to
861                // both listeners.
862                assert_eq!(handle.zone_origin().to_string(), "overlay.local.");
863                handle
864                    .add_record("dual", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 9)))
865                    .await
866                    .expect("add_record via fallback handle");
867            }
868            Err(DnsError::Io(_)) => {
869                // Expected on hosts that reserve port 53 or where the
870                // loopback alias is already bound. Counts as a clean error
871                // rather than a panic.
872            }
873            Err(other) => panic!("unexpected error from bind_windows_fallback: {other}"),
874        }
875    }
876
877    #[test]
878    fn test_peer_hostname_uniqueness() {
879        // Different IPs should produce different hostnames
880        let v4_a = peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
881        let v4_b = peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)));
882        assert_ne!(v4_a, v4_b);
883
884        let v6_a = peer_hostname(IpAddr::V6("fd00::1".parse().unwrap()));
885        let v6_b = peer_hostname(IpAddr::V6("fd00::2".parse().unwrap()));
886        assert_ne!(v6_a, v6_b);
887
888        // IPv4 and IPv6 hostname formats are distinct
889        let v4 = peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
890        let v6 = peer_hostname(IpAddr::V6("fd00::1".parse().unwrap()));
891        assert_ne!(v4, v6);
892    }
893}