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        self.bind_secondary(SocketAddr::new(bind_ip, 53)).await
344    }
345
346    /// Bind an additional DNS listener on an arbitrary `listen_addr`, sharing
347    /// this server's authority + zone so the same records answer on both the
348    /// primary listener and this one.
349    ///
350    /// Unlike [`bind_windows_fallback`](Self::bind_windows_fallback) (which is
351    /// hard-wired to port 53 for Windows HNS containers), this lets the caller
352    /// pick a **non-privileged** port — required on macOS where an unprivileged
353    /// daemon cannot bind below 1024. The VZ-Linux path uses this to expose the
354    /// overlay resolver on `<node_overlay_ip>:<dns_port>` so a tiny in-guest
355    /// relay can forward the guest's port-53 queries to it.
356    ///
357    /// # Errors
358    ///
359    /// Returns `DnsError::Io` when either the UDP or TCP socket cannot be bound.
360    #[allow(clippy::unused_async)]
361    pub async fn bind_secondary(&self, listen_addr: SocketAddr) -> Result<DnsHandle, DnsError> {
362        let handle = self.handle();
363        let zone_origin = self.zone_origin.clone();
364        let authority = Arc::clone(&self.authority);
365
366        // Pre-bind the sockets synchronously so binding failures surface here
367        // instead of being swallowed by the detached task. On success we hand
368        // the live sockets off to the server future on a background task.
369        let udp_socket = UdpSocket::bind(listen_addr).await?;
370        let tcp_listener = TcpListener::bind(listen_addr).await?;
371
372        tokio::spawn(async move {
373            let mut catalog = Catalog::new();
374            catalog.upsert(zone_origin.into(), Box::new(authority));
375            let mut server = ServerFuture::new(catalog);
376            server.register_socket(udp_socket);
377            server.register_listener(tcp_listener, Duration::from_secs(30));
378            tracing::info!(
379                addr = %listen_addr,
380                "secondary DNS listener started",
381            );
382            if let Err(e) = server.block_until_done().await {
383                tracing::error!("secondary DNS listener error: {}", e);
384            }
385        });
386
387        Ok(handle)
388    }
389
390    /// Internal method to run the DNS server
391    async fn run_server(
392        listen_addr: SocketAddr,
393        zone_origin: Name,
394        authority: Arc<InMemoryAuthority>,
395    ) -> Result<(), DnsError> {
396        // Create the catalog and add our authority
397        let mut catalog = Catalog::new();
398
399        // The catalog accepts Arc<dyn AuthorityObject> - InMemoryAuthority implements this
400        catalog.upsert(zone_origin.into(), Box::new(authority));
401
402        // Create the server
403        let mut server = ServerFuture::new(catalog);
404
405        // Bind UDP socket
406        let udp_socket = UdpSocket::bind(listen_addr).await?;
407        server.register_socket(udp_socket);
408
409        // Bind TCP listener
410        let tcp_listener = TcpListener::bind(listen_addr).await?;
411        server.register_listener(tcp_listener, Duration::from_secs(30));
412
413        tracing::info!(addr = %listen_addr, "DNS server listening");
414
415        // Run the server
416        server
417            .block_until_done()
418            .await
419            .map_err(|e| DnsError::Server(e.to_string()))?;
420
421        Ok(())
422    }
423
424    /// Get the listen address
425    #[must_use]
426    pub fn listen_addr(&self) -> SocketAddr {
427        self.listen_addr
428    }
429
430    /// Get the zone origin
431    #[must_use]
432    pub fn zone_origin(&self) -> &Name {
433        &self.zone_origin
434    }
435}
436
437/// DNS client for querying overlay DNS servers
438pub struct DnsClient {
439    server_addr: SocketAddr,
440}
441
442impl DnsClient {
443    /// Create a new DNS client
444    #[must_use]
445    pub fn new(server_addr: SocketAddr) -> Self {
446        Self { server_addr }
447    }
448
449    /// Query for an A record
450    ///
451    /// # Errors
452    ///
453    /// Returns a `DnsError` if the query fails or the hostname is invalid.
454    pub fn query_a(&self, hostname: &str) -> Result<Option<Ipv4Addr>, DnsError> {
455        let name = Name::from_str(hostname)
456            .map_err(|e| DnsError::InvalidName(format!("{hostname}: {e}")))?;
457
458        let conn = UdpClientConnection::new(self.server_addr)
459            .map_err(|e| DnsError::Client(e.to_string()))?;
460
461        let client = SyncClient::new(conn);
462
463        let response = client
464            .query(&name, DNSClass::IN, RecordType::A)
465            .map_err(|e| DnsError::Client(e.to_string()))?;
466
467        // Extract the A record from the response
468        for answer in response.answers() {
469            if let Some(RData::A(a_record)) = answer.data() {
470                return Ok(Some((*a_record).into()));
471            }
472        }
473
474        Ok(None)
475    }
476
477    /// Query for an AAAA record (IPv6)
478    ///
479    /// # Errors
480    ///
481    /// Returns a `DnsError` if the query fails or the hostname is invalid.
482    pub fn query_aaaa(&self, hostname: &str) -> Result<Option<Ipv6Addr>, DnsError> {
483        let name = Name::from_str(hostname)
484            .map_err(|e| DnsError::InvalidName(format!("{hostname}: {e}")))?;
485
486        let conn = UdpClientConnection::new(self.server_addr)
487            .map_err(|e| DnsError::Client(e.to_string()))?;
488
489        let client = SyncClient::new(conn);
490
491        let response = client
492            .query(&name, DNSClass::IN, RecordType::AAAA)
493            .map_err(|e| DnsError::Client(e.to_string()))?;
494
495        // Extract the AAAA record from the response
496        for answer in response.answers() {
497            if let Some(RData::AAAA(aaaa_record)) = answer.data() {
498                return Ok(Some((*aaaa_record).into()));
499            }
500        }
501
502        Ok(None)
503    }
504
505    /// Query for any address record (A or AAAA), returning the first match
506    ///
507    /// Tries A first, then AAAA. Returns the first successful result.
508    ///
509    /// # Errors
510    ///
511    /// Returns a `DnsError` if both queries fail or the hostname is invalid.
512    pub fn query_addr(&self, hostname: &str) -> Result<Option<IpAddr>, DnsError> {
513        // Try A record first
514        if let Ok(Some(v4)) = self.query_a(hostname) {
515            return Ok(Some(IpAddr::V4(v4)));
516        }
517
518        // Then try AAAA
519        if let Ok(Some(v6)) = self.query_aaaa(hostname) {
520            return Ok(Some(IpAddr::V6(v6)));
521        }
522
523        Ok(None)
524    }
525}
526
527/// Service discovery with DNS
528pub struct ServiceDiscovery {
529    dns_server: SocketAddr,
530    records: RwLock<HashMap<String, IpAddr>>,
531}
532
533impl ServiceDiscovery {
534    /// Create a new service discovery instance
535    #[must_use]
536    pub fn new(dns_server_addr: SocketAddr) -> Self {
537        Self {
538            dns_server: dns_server_addr,
539            records: RwLock::new(HashMap::new()),
540        }
541    }
542
543    /// Register a service (stores locally, does not update DNS server)
544    pub async fn register(&self, name: &str, ip: IpAddr) {
545        let mut records = self.records.write().await;
546        records.insert(name.to_string(), ip);
547    }
548
549    /// Resolve a service to an IP address
550    ///
551    /// Checks the local cache first, then queries the DNS server for both
552    /// A (IPv4) and AAAA (IPv6) records.
553    pub async fn resolve(&self, name: &str) -> Option<IpAddr> {
554        // First check local cache
555        {
556            let records = self.records.read().await;
557            if let Some(ip) = records.get(name) {
558                return Some(*ip);
559            }
560        }
561
562        // Query DNS server for both A and AAAA records
563        let client = DnsClient::new(self.dns_server);
564        if let Ok(Some(addr)) = client.query_addr(name) {
565            return Some(addr);
566        }
567
568        None
569    }
570
571    /// Unregister a service
572    pub async fn unregister(&self, name: &str) {
573        let mut records = self.records.write().await;
574        records.remove(name);
575    }
576
577    /// List all registered services
578    pub async fn list_services(&self) -> Vec<String> {
579        let records = self.records.read().await;
580        records.keys().cloned().collect()
581    }
582
583    /// Get the DNS server address
584    pub fn dns_server(&self) -> SocketAddr {
585        self.dns_server
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592
593    #[test]
594    fn test_peer_hostname_v4() {
595        // Test various IPv4 addresses
596        assert_eq!(
597            peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1))),
598            "node-0-1"
599        );
600        assert_eq!(
601            peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5))),
602            "node-0-5"
603        );
604        assert_eq!(
605            peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 200, 1, 100))),
606            "node-1-100"
607        );
608        assert_eq!(
609            peer_hostname(IpAddr::V4(Ipv4Addr::new(192, 168, 255, 254))),
610            "node-255-254"
611        );
612    }
613
614    #[test]
615    fn test_peer_hostname_v6() {
616        // Test various IPv6 addresses
617        assert_eq!(
618            peer_hostname(IpAddr::V6("fd00::1".parse().unwrap())),
619            "node-0001"
620        );
621        assert_eq!(
622            peer_hostname(IpAddr::V6("fd00::abcd".parse().unwrap())),
623            "node-abcd"
624        );
625        assert_eq!(
626            peer_hostname(IpAddr::V6("fd00:200::ffff".parse().unwrap())),
627            "node-ffff"
628        );
629        // Zero last segment
630        assert_eq!(
631            peer_hostname(IpAddr::V6("fd00::1:0".parse().unwrap())),
632            "node-0000"
633        );
634    }
635
636    #[test]
637    fn test_dns_config() {
638        let config = DnsConfig::new("overlay.local.", IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)));
639        assert_eq!(config.zone, "overlay.local.");
640        assert_eq!(config.port, DEFAULT_DNS_PORT);
641        assert_eq!(config.bind_addr, IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)));
642
643        // Test with_port
644        let config = config.with_port(5353);
645        assert_eq!(config.port, 5353);
646    }
647
648    #[test]
649    fn test_dns_config_serialization() {
650        let config = DnsConfig::new("overlay.local.", IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)))
651            .with_port(15353);
652
653        let json = serde_json::to_string(&config).unwrap();
654        let deserialized: DnsConfig = serde_json::from_str(&json).unwrap();
655
656        assert_eq!(deserialized.zone, config.zone);
657        assert_eq!(deserialized.port, config.port);
658        assert_eq!(deserialized.bind_addr, config.bind_addr);
659    }
660
661    #[tokio::test]
662    async fn test_service_discovery_local_cache() {
663        // Use a non-routable address since we're only testing local cache
664        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
665        let discovery = ServiceDiscovery::new(addr);
666
667        let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2));
668        discovery.register("test-service", ip).await;
669
670        let resolved = discovery.resolve("test-service").await;
671        assert_eq!(resolved, Some(ip));
672
673        // Test unregister
674        discovery.unregister("test-service").await;
675        let services = discovery.list_services().await;
676        assert!(services.is_empty());
677    }
678
679    #[test]
680    fn test_dns_server_creation() {
681        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
682        let server = DnsServer::new(addr, "overlay.local.");
683
684        assert!(server.is_ok());
685        let server = server.unwrap();
686        assert_eq!(server.listen_addr(), addr);
687        assert_eq!(server.zone_origin().to_string(), "overlay.local.");
688    }
689
690    #[test]
691    fn test_dns_server_from_config() {
692        let config =
693            DnsConfig::new("test.local.", IpAddr::V4(Ipv4Addr::LOCALHOST)).with_port(15353);
694        let server = DnsServer::from_config(&config);
695
696        assert!(server.is_ok());
697        let server = server.unwrap();
698        assert_eq!(server.listen_addr().port(), 15353);
699        assert_eq!(server.zone_origin().to_string(), "test.local.");
700    }
701
702    #[test]
703    fn test_dns_server_invalid_zone() {
704        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
705        // Empty zone name is technically valid in DNS, so use an obviously invalid one
706        let server = DnsServer::new(addr, "overlay.local.");
707        assert!(server.is_ok());
708    }
709
710    #[tokio::test]
711    async fn test_dns_server_add_record() {
712        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
713        let server = DnsServer::new(addr, "overlay.local.").unwrap();
714
715        let result = server
716            .add_record("myservice", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5)))
717            .await;
718        assert!(result.is_ok());
719    }
720
721    #[tokio::test]
722    async fn test_dns_handle_add_record() {
723        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
724        let server = DnsServer::new(addr, "overlay.local.").unwrap();
725
726        // Get handle and add records through it
727        let handle = server.handle();
728
729        let result = handle
730            .add_record("service1", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))
731            .await;
732        assert!(result.is_ok());
733
734        let result = handle
735            .add_record("service2", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)))
736            .await;
737        assert!(result.is_ok());
738
739        // Zone origin should be accessible
740        assert_eq!(handle.zone_origin().to_string(), "overlay.local.");
741    }
742
743    #[test]
744    fn test_dns_client_creation() {
745        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 53);
746        let client = DnsClient::new(addr);
747        assert_eq!(client.server_addr, addr);
748    }
749
750    #[tokio::test]
751    async fn test_dns_handle_add_aaaa_record() {
752        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
753        let server = DnsServer::new(addr, "overlay.local.").unwrap();
754        let handle = server.handle();
755
756        // Add an AAAA record via IPv6 address
757        let ipv6: IpAddr = "fd00::1".parse().unwrap();
758        let result = handle.add_record("service-v6", ipv6).await;
759        assert!(result.is_ok());
760
761        // Add a second AAAA record
762        let ipv6_2: IpAddr = "fd00::abcd".parse().unwrap();
763        let result = handle.add_record("service-v6-2", ipv6_2).await;
764        assert!(result.is_ok());
765    }
766
767    #[tokio::test]
768    async fn test_dns_server_add_aaaa_record() {
769        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
770        let server = DnsServer::new(addr, "overlay.local.").unwrap();
771
772        // Add AAAA record through the server directly
773        let ipv6: IpAddr = "fd00::42".parse().unwrap();
774        let result = server.add_record("myservice-v6", ipv6).await;
775        assert!(result.is_ok());
776    }
777
778    #[tokio::test]
779    async fn test_dns_handle_remove_record_covers_both_types() {
780        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
781        let server = DnsServer::new(addr, "overlay.local.").unwrap();
782        let handle = server.handle();
783
784        // Add an A record
785        let ipv4 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
786        handle.add_record("dual-service", ipv4).await.unwrap();
787
788        // Remove should succeed (tombstones both A and AAAA)
789        let removed = handle.remove_record("dual-service").await.unwrap();
790        assert!(removed);
791
792        // Add an AAAA record
793        let ipv6: IpAddr = "fd00::1".parse().unwrap();
794        handle.add_record("v6-service", ipv6).await.unwrap();
795
796        // Remove should also succeed for AAAA records
797        let removed = handle.remove_record("v6-service").await.unwrap();
798        assert!(removed);
799    }
800
801    #[tokio::test]
802    async fn test_service_discovery_local_cache_ipv6() {
803        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
804        let discovery = ServiceDiscovery::new(addr);
805
806        // Register an IPv6 service
807        let ipv6: IpAddr = "fd00::beef".parse().unwrap();
808        discovery.register("v6-service", ipv6).await;
809
810        // Should resolve from local cache
811        let resolved = discovery.resolve("v6-service").await;
812        assert_eq!(resolved, Some(ipv6));
813
814        // Unregister and verify
815        discovery.unregister("v6-service").await;
816        let services = discovery.list_services().await;
817        assert!(services.is_empty());
818    }
819
820    #[tokio::test]
821    async fn test_service_discovery_mixed_v4_v6_cache() {
822        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 15353);
823        let discovery = ServiceDiscovery::new(addr);
824
825        let ipv4 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
826        let ipv6: IpAddr = "fd00::1".parse().unwrap();
827
828        discovery.register("svc-v4", ipv4).await;
829        discovery.register("svc-v6", ipv6).await;
830
831        assert_eq!(discovery.resolve("svc-v4").await, Some(ipv4));
832        assert_eq!(discovery.resolve("svc-v6").await, Some(ipv6));
833
834        let mut services = discovery.list_services().await;
835        services.sort();
836        assert_eq!(services, vec!["svc-v4", "svc-v6"]);
837    }
838
839    #[test]
840    fn test_dns_config_with_ipv6_bind_addr() {
841        let ipv6_bind: IpAddr = "fd00::1".parse().unwrap();
842        let config = DnsConfig::new("overlay.local.", ipv6_bind);
843        assert_eq!(config.bind_addr, ipv6_bind);
844        assert_eq!(config.port, DEFAULT_DNS_PORT);
845
846        // Serialization round-trip
847        let json = serde_json::to_string(&config).unwrap();
848        let deserialized: DnsConfig = serde_json::from_str(&json).unwrap();
849        assert_eq!(deserialized.bind_addr, ipv6_bind);
850    }
851
852    #[test]
853    fn test_dns_server_creation_ipv6_bind() {
854        let ipv6_addr: IpAddr = "::1".parse().unwrap();
855        let addr = SocketAddr::new(ipv6_addr, 15353);
856        let server = DnsServer::new(addr, "overlay.local.");
857
858        assert!(server.is_ok());
859        let server = server.unwrap();
860        assert_eq!(server.listen_addr(), addr);
861    }
862
863    /// Smoke test for the Windows-fallback port-53 listener: binding to
864    /// 127.0.0.2:53 should fail fast on hosts where that port is privileged
865    /// or already in use, but we only care that the method surfaces a clean
866    /// `DnsError` (not a panic) when the bind is contested. When the bind
867    /// succeeds on a permissive CI host, we verify the returned handle shares
868    /// the authority with the primary listener.
869    #[tokio::test]
870    async fn test_bind_windows_fallback_errors_or_shares_authority() {
871        let primary = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
872        let server = DnsServer::new(primary, "overlay.local.").unwrap();
873        let bind_ip: IpAddr = "127.0.0.2".parse().unwrap();
874
875        match server.bind_windows_fallback(bind_ip).await {
876            Ok(handle) => {
877                // Best-effort: the handle must expose the same zone as the
878                // primary server so record mutations on either propagate to
879                // both listeners.
880                assert_eq!(handle.zone_origin().to_string(), "overlay.local.");
881                handle
882                    .add_record("dual", IpAddr::V4(Ipv4Addr::new(10, 0, 0, 9)))
883                    .await
884                    .expect("add_record via fallback handle");
885            }
886            Err(DnsError::Io(_)) => {
887                // Expected on hosts that reserve port 53 or where the
888                // loopback alias is already bound. Counts as a clean error
889                // rather than a panic.
890            }
891            Err(other) => panic!("unexpected error from bind_windows_fallback: {other}"),
892        }
893    }
894
895    #[test]
896    fn test_peer_hostname_uniqueness() {
897        // Different IPs should produce different hostnames
898        let v4_a = peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
899        let v4_b = peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)));
900        assert_ne!(v4_a, v4_b);
901
902        let v6_a = peer_hostname(IpAddr::V6("fd00::1".parse().unwrap()));
903        let v6_b = peer_hostname(IpAddr::V6("fd00::2".parse().unwrap()));
904        assert_ne!(v6_a, v6_b);
905
906        // IPv4 and IPv6 hostname formats are distinct
907        let v4 = peer_hostname(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
908        let v6 = peer_hostname(IpAddr::V6("fd00::1".parse().unwrap()));
909        assert_ne!(v4, v6);
910    }
911}