Skip to main content

zlayer_overlay/
bootstrap.rs

1//! Overlay network bootstrap functionality
2//!
3//! Provides initialization and joining capabilities for overlay networks,
4//! including keypair generation, interface creation, and peer management.
5
6use crate::allocator::IpAllocator;
7use crate::allocator::{NodeSliceAllocator, NodeSliceAllocatorSnapshot};
8use crate::config::PeerInfo;
9use crate::dns::{peer_hostname, DnsConfig, DnsHandle, DnsServer, DEFAULT_DNS_PORT};
10use crate::error::{OverlayError, Result};
11#[cfg(feature = "nat")]
12use crate::nat::{Candidate, ConnectionType, NatTraversal, RelayServer};
13use crate::transport::OverlayTransport;
14use ipnet::IpNet;
15use serde::{Deserialize, Serialize};
16use std::net::{IpAddr, SocketAddr};
17use std::path::{Path, PathBuf};
18use std::time::Duration;
19use tracing::{debug, info, warn};
20
21/// Default overlay interface name for `ZLayer`
22///
23/// On macOS, this is `"utun"` which tells boringtun to let the kernel
24/// auto-assign a `utunN` device. On Linux, a custom name is used.
25#[cfg(target_os = "macos")]
26pub const DEFAULT_INTERFACE_NAME: &str = "utun";
27#[cfg(not(target_os = "macos"))]
28pub const DEFAULT_INTERFACE_NAME: &str = "zl-overlay0";
29
30/// Default overlay listen port (re-exported from `zlayer-core`).
31pub use zlayer_core::DEFAULT_WG_PORT;
32
33/// Default overlay network CIDR (IPv4)
34pub const DEFAULT_OVERLAY_CIDR: &str = "10.200.0.0/16";
35
36/// Default overlay network CIDR (IPv6)
37///
38/// Uses a ULA (Unique Local Address) prefix in the `fd00::/8` range.
39/// The `fd00:200::/48` prefix mirrors the IPv4 `10.200.0.0/16` convention.
40pub const DEFAULT_OVERLAY_CIDR_V6: &str = "fd00:200::/48";
41
42/// Default persistent keepalive interval (seconds)
43pub const DEFAULT_KEEPALIVE_SECS: u16 = 25;
44
45/// Default per-node slice prefix length for carving the cluster CIDR into
46/// per-node allocation slices. A `/28` gives each node 16 addresses
47/// (14 usable hosts) from the cluster `/16`, supporting up to 4096 nodes.
48pub const DEFAULT_SLICE_PREFIX: u8 = 28;
49
50/// Serde helper for `Option<IpNet>`: serialize as a CIDR string (e.g.
51/// `"10.200.7.0/28"`), so we don't depend on `ipnet`'s optional `serde`
52/// feature flag. Keeps persisted state compact and human-readable.
53mod option_ipnet_str {
54    use ipnet::IpNet;
55    use serde::{Deserialize, Deserializer, Serialize, Serializer};
56
57    #[allow(clippy::ref_option)]
58    pub fn serialize<S>(value: &Option<IpNet>, serializer: S) -> Result<S::Ok, S::Error>
59    where
60        S: Serializer,
61    {
62        value.map(|v| v.to_string()).serialize(serializer)
63    }
64
65    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<IpNet>, D::Error>
66    where
67        D: Deserializer<'de>,
68    {
69        let opt = Option::<String>::deserialize(deserializer)?;
70        match opt {
71            None => Ok(None),
72            Some(s) => s
73                .parse::<IpNet>()
74                .map(Some)
75                .map_err(serde::de::Error::custom),
76        }
77    }
78}
79
80/// Overlay network bootstrap configuration
81///
82/// Contains all configuration needed to initialize and manage
83/// an overlay network on a node.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct BootstrapConfig {
86    /// Network CIDR (e.g., "10.200.0.0/16")
87    pub cidr: String,
88
89    /// This node's overlay IP address (IPv4 or IPv6)
90    pub node_ip: IpAddr,
91
92    /// Overlay interface name
93    pub interface: String,
94
95    /// Overlay listen port
96    pub port: u16,
97
98    /// This node's overlay private key
99    pub private_key: String,
100
101    /// This node's overlay public key
102    pub public_key: String,
103
104    /// Whether this node is the cluster leader
105    pub is_leader: bool,
106
107    /// Creation timestamp (Unix epoch seconds)
108    pub created_at: u64,
109
110    /// Per-node slice of the cluster CIDR that this node draws container
111    /// IPs from. `None` for pre-slice-aware configs (back-compat). When set,
112    /// this replaces `node_ip/32` in `allowed_ip()`.
113    #[serde(default, with = "option_ipnet_str")]
114    pub slice_cidr: Option<IpNet>,
115}
116
117impl BootstrapConfig {
118    /// Get the overlay IP with host prefix for allowed IPs
119    ///
120    /// When `slice_cidr` is set, returns the slice CIDR string unchanged so
121    /// the overlay accepts the full per-node slice of container IPs. Otherwise
122    /// falls back to the legacy `/32` (IPv4) or `/128` (IPv6) single-host
123    /// representation of `node_ip` for back-compat with pre-slice configs.
124    #[must_use]
125    pub fn allowed_ip(&self) -> String {
126        if let Some(slice) = self.slice_cidr {
127            return slice.to_string();
128        }
129        let prefix = match self.node_ip {
130            IpAddr::V4(_) => 32,
131            IpAddr::V6(_) => 128,
132        };
133        format!("{}/{}", self.node_ip, prefix)
134    }
135}
136
137/// Peer configuration for overlay network
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct PeerConfig {
140    /// Peer's node ID (for identification)
141    pub node_id: String,
142
143    /// Peer's overlay public key
144    pub public_key: String,
145
146    /// Peer's public endpoint (host:port)
147    pub endpoint: String,
148
149    /// Peer's overlay IP address (IPv4 or IPv6)
150    pub overlay_ip: IpAddr,
151
152    /// Optional persistent keepalive interval in seconds
153    #[serde(default)]
154    pub keepalive: Option<u16>,
155
156    /// Optional custom DNS hostname for this peer (without zone suffix)
157    /// If provided, the peer will be registered with this name in addition
158    /// to the auto-generated IP-based hostname.
159    #[serde(default)]
160    pub hostname: Option<String>,
161
162    /// NAT traversal candidates for this peer
163    #[serde(default)]
164    #[cfg(feature = "nat")]
165    pub candidates: Vec<Candidate>,
166
167    /// How this peer is currently connected
168    #[serde(default)]
169    #[cfg(feature = "nat")]
170    pub connection_type: ConnectionType,
171
172    /// Peer's per-node slice CIDR. `None` on old configs; when set, used as
173    /// the peer's `allowed_ips` instead of `overlay_ip/32`.
174    #[serde(default, with = "option_ipnet_str")]
175    pub slice_cidr: Option<IpNet>,
176}
177
178impl PeerConfig {
179    /// Create a new peer configuration
180    #[must_use]
181    pub fn new(node_id: String, public_key: String, endpoint: String, overlay_ip: IpAddr) -> Self {
182        Self {
183            node_id,
184            public_key,
185            endpoint,
186            overlay_ip,
187            keepalive: Some(DEFAULT_KEEPALIVE_SECS),
188            hostname: None,
189            #[cfg(feature = "nat")]
190            candidates: Vec::new(),
191            #[cfg(feature = "nat")]
192            connection_type: ConnectionType::default(),
193            slice_cidr: None,
194        }
195    }
196
197    /// Set a custom DNS hostname for this peer
198    #[must_use]
199    pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
200        self.hostname = Some(hostname.into());
201        self
202    }
203
204    /// Set the per-node slice CIDR for this peer.
205    ///
206    /// When set, `to_peer_info()` uses the slice CIDR as the peer's
207    /// `allowed_ips` (so the overlay accepts the peer's full slice of
208    /// container IPs) instead of the legacy single-host `overlay_ip/32`.
209    #[must_use]
210    pub fn with_slice_cidr(mut self, cidr: IpNet) -> Self {
211        self.slice_cidr = Some(cidr);
212        self
213    }
214
215    /// Convert to `PeerInfo` for overlay transport configuration
216    ///
217    /// When `slice_cidr` is set, the peer's `allowed_ips` is the slice CIDR
218    /// string. Otherwise, falls back to the legacy `overlay_ip/{32,128}`
219    /// host-prefix representation for back-compat.
220    ///
221    /// # Errors
222    ///
223    /// Returns an error if the endpoint address cannot be parsed.
224    pub fn to_peer_info(&self) -> std::result::Result<PeerInfo, Box<dyn std::error::Error>> {
225        let endpoint: SocketAddr = self.endpoint.parse()?;
226        let keepalive =
227            Duration::from_secs(u64::from(self.keepalive.unwrap_or(DEFAULT_KEEPALIVE_SECS)));
228
229        let allowed_ips = if let Some(slice) = self.slice_cidr {
230            slice.to_string()
231        } else {
232            let prefix = match self.overlay_ip {
233                IpAddr::V4(_) => 32,
234                IpAddr::V6(_) => 128,
235            };
236            format!("{}/{}", self.overlay_ip, prefix)
237        };
238
239        Ok(PeerInfo::new(
240            self.public_key.clone(),
241            endpoint,
242            &allowed_ips,
243            keepalive,
244        ))
245    }
246}
247
248/// Persistent state for the overlay bootstrap
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct BootstrapState {
251    /// Bootstrap configuration
252    pub config: BootstrapConfig,
253
254    /// List of configured peers
255    pub peers: Vec<PeerConfig>,
256
257    /// IP allocator state (only for leader)
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub allocator_state: Option<crate::allocator::IpAllocatorState>,
260
261    /// Per-node slice allocator state (only for leader).
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub slice_allocator_state: Option<NodeSliceAllocatorSnapshot>,
264}
265
266/// Bootstrap manager for overlay network
267///
268/// Handles overlay network initialization, peer management,
269/// and overlay transport interface configuration.
270pub struct OverlayBootstrap {
271    /// Bootstrap configuration
272    config: BootstrapConfig,
273
274    /// Configured peers
275    peers: Vec<PeerConfig>,
276
277    /// Data directory for persistent state
278    data_dir: PathBuf,
279
280    /// IP allocator (only for leader nodes)
281    allocator: Option<IpAllocator>,
282
283    /// Per-node slice allocator (only for leader nodes) — carves the cluster
284    /// CIDR into `/28` slices assigned to each joining node. Added to fix the
285    /// latent IP-collision bug where every agent independently allocated
286    /// container IPs from the full cluster `/16`.
287    slice_allocator: Option<NodeSliceAllocator>,
288
289    /// DNS configuration (opt-in)
290    dns_config: Option<DnsConfig>,
291
292    /// DNS handle for managing records (available after `start()` if DNS enabled)
293    dns_handle: Option<DnsHandle>,
294
295    /// Overlay transport (boringtun device handle).
296    ///
297    /// Must be kept alive for the overlay network lifetime; dropping the
298    /// transport destroys the TUN device.
299    transport: Option<OverlayTransport>,
300
301    /// NAT traversal orchestrator (available after `start()` if NAT is enabled)
302    #[cfg(feature = "nat")]
303    nat_traversal: Option<NatTraversal>,
304
305    /// Built-in relay server (available after `start()` if relay is configured)
306    #[cfg(feature = "nat")]
307    relay_server: Option<RelayServer>,
308
309    /// NAT traversal configuration. When `None`, [`NatConfig::default()`] is
310    /// used at `start()` time so callers that never set a value still pick up
311    /// the defaults (NAT enabled, public STUN servers).
312    #[cfg(feature = "nat")]
313    nat_config: Option<crate::nat::NatConfig>,
314}
315
316impl OverlayBootstrap {
317    /// Initialize as cluster leader (first node in the overlay)
318    ///
319    /// This generates a new overlay keypair, allocates the first IP
320    /// in the CIDR range, and prepares the node as the overlay leader.
321    ///
322    /// # Arguments
323    /// * `cidr` - Overlay network CIDR (e.g., "10.200.0.0/16")
324    /// * `port` - Overlay listen port
325    /// * `data_dir` - Directory for persistent state
326    ///
327    /// # Example
328    /// ```ignore
329    /// let bootstrap = OverlayBootstrap::init_leader(
330    ///     "10.200.0.0/16",
331    ///     51820,
332    ///     Path::new("/var/lib/zlayer"),
333    /// ).await?;
334    /// ```
335    ///
336    /// # Errors
337    ///
338    /// Returns an error if already initialized, key generation fails, or state cannot be saved.
339    pub async fn init_leader(cidr: &str, port: u16, data_dir: &Path) -> Result<Self> {
340        // Check if already initialized
341        let config_path = data_dir.join("overlay_bootstrap.json");
342        if config_path.exists() {
343            return Err(OverlayError::AlreadyInitialized(
344                config_path.display().to_string(),
345            ));
346        }
347
348        // Ensure data directory exists
349        tokio::fs::create_dir_all(data_dir).await?;
350
351        // Generate overlay keypair
352        info!("Generating overlay keypair for leader");
353        let (private_key, public_key) = OverlayTransport::generate_keys()
354            .await
355            .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
356
357        // Initialize IP allocator (legacy flat allocator, kept in sync for
358        // back-compat with callers that still read it) and allocate its
359        // first IP so its state tracks at least the leader.
360        let mut allocator = IpAllocator::new(cidr)?;
361        let _legacy_first = allocator.allocate_first()?;
362
363        // Initialize the per-node slice allocator and carve the leader's
364        // own slice. The leader's `node_ip` is taken as the first usable
365        // host of its slice so it always lives inside the slice it owns.
366        let cluster_cidr: IpNet = cidr
367            .parse()
368            .map_err(|e: ipnet::AddrParseError| OverlayError::InvalidCidr(e.to_string()))?;
369        let mut slice_allocator = NodeSliceAllocator::new(cluster_cidr, DEFAULT_SLICE_PREFIX)?;
370        let leader_slice = slice_allocator.assign("leader")?;
371        let node_ip = leader_slice.hosts().next().unwrap_or_else(|| {
372            // Fall back to network() + 1 for very small slices where
373            // `hosts()` can return empty. We still need a valid address
374            // to assign to the interface.
375            match leader_slice.network() {
376                IpAddr::V4(v4) => {
377                    let bits = u32::from(v4).saturating_add(1);
378                    IpAddr::V4(std::net::Ipv4Addr::from(bits))
379                }
380                IpAddr::V6(v6) => {
381                    let bits = u128::from(v6).saturating_add(1);
382                    IpAddr::V6(std::net::Ipv6Addr::from(bits))
383                }
384            }
385        });
386
387        info!(
388            node_ip = %node_ip,
389            cidr = cidr,
390            slice = %leader_slice,
391            "Allocated leader IP from slice"
392        );
393
394        // Create config
395        let config = BootstrapConfig {
396            cidr: cidr.to_string(),
397            node_ip,
398            interface: DEFAULT_INTERFACE_NAME.to_string(),
399            port,
400            private_key,
401            public_key,
402            is_leader: true,
403            created_at: current_timestamp(),
404            slice_cidr: Some(leader_slice),
405        };
406
407        let bootstrap = Self {
408            config,
409            peers: Vec::new(),
410            data_dir: data_dir.to_path_buf(),
411            allocator: Some(allocator),
412            slice_allocator: Some(slice_allocator),
413            dns_config: None,
414            dns_handle: None,
415            transport: None,
416            #[cfg(feature = "nat")]
417            nat_traversal: None,
418            #[cfg(feature = "nat")]
419            relay_server: None,
420            #[cfg(feature = "nat")]
421            nat_config: None,
422        };
423
424        // Persist state
425        bootstrap.save().await?;
426
427        Ok(bootstrap)
428    }
429
430    /// Join an existing overlay network
431    ///
432    /// Generates a new overlay keypair and configures this node
433    /// to connect to an existing overlay network.
434    ///
435    /// # Arguments
436    /// * `leader_cidr` - Leader's overlay network CIDR
437    /// * `leader_endpoint` - Leader's public endpoint (host:port)
438    /// * `leader_public_key` - Leader's overlay public key
439    /// * `leader_overlay_ip` - Leader's overlay IP address
440    /// * `allocated_ip` - IP address allocated for this node by the leader
441    /// * `port` - Overlay listen port for this node
442    /// * `slice_cidr` - Per-node slice assigned by the leader (if any). When
443    ///   set, the worker uses this slice as its `allowed_ip`; `None` preserves
444    ///   the legacy `allocated_ip/32` behavior for pre-slice-aware clusters.
445    /// * `data_dir` - Directory for persistent state
446    ///
447    /// # Errors
448    ///
449    /// Returns an error if already initialized, key generation fails, or state cannot be saved.
450    #[allow(clippy::too_many_arguments)]
451    pub async fn join(
452        leader_cidr: &str,
453        leader_endpoint: &str,
454        leader_public_key: &str,
455        leader_overlay_ip: IpAddr,
456        allocated_ip: IpAddr,
457        port: u16,
458        slice_cidr: Option<IpNet>,
459        data_dir: &Path,
460    ) -> Result<Self> {
461        // Check if already initialized
462        let config_path = data_dir.join("overlay_bootstrap.json");
463        if config_path.exists() {
464            return Err(OverlayError::AlreadyInitialized(
465                config_path.display().to_string(),
466            ));
467        }
468
469        // Ensure data directory exists
470        tokio::fs::create_dir_all(data_dir).await?;
471
472        // Generate overlay keypair for this node
473        info!("Generating overlay keypair for joining node");
474        let (private_key, public_key) = OverlayTransport::generate_keys()
475            .await
476            .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
477
478        // Create config
479        let config = BootstrapConfig {
480            cidr: leader_cidr.to_string(),
481            node_ip: allocated_ip,
482            interface: DEFAULT_INTERFACE_NAME.to_string(),
483            port,
484            private_key,
485            public_key,
486            is_leader: false,
487            created_at: current_timestamp(),
488            slice_cidr,
489        };
490
491        // Add leader as the first peer
492        let leader_peer = PeerConfig {
493            node_id: "leader".to_string(),
494            public_key: leader_public_key.to_string(),
495            endpoint: leader_endpoint.to_string(),
496            overlay_ip: leader_overlay_ip,
497            keepalive: Some(DEFAULT_KEEPALIVE_SECS),
498            hostname: None, // Leader gets its own DNS alias "leader.zone"
499            #[cfg(feature = "nat")]
500            candidates: Vec::new(),
501            #[cfg(feature = "nat")]
502            connection_type: ConnectionType::default(),
503            slice_cidr: None,
504        };
505
506        info!(
507            leader_endpoint = leader_endpoint,
508            overlay_ip = %allocated_ip,
509            "Configured leader as peer"
510        );
511
512        let bootstrap = Self {
513            config,
514            peers: vec![leader_peer],
515            data_dir: data_dir.to_path_buf(),
516            allocator: None, // Workers don't manage IP allocation
517            slice_allocator: None,
518            dns_config: None,
519            dns_handle: None,
520            transport: None,
521            #[cfg(feature = "nat")]
522            nat_traversal: None,
523            #[cfg(feature = "nat")]
524            relay_server: None,
525            #[cfg(feature = "nat")]
526            nat_config: None,
527        };
528
529        // Persist state
530        bootstrap.save().await?;
531
532        Ok(bootstrap)
533    }
534
535    /// Load existing bootstrap state from disk
536    ///
537    /// # Errors
538    ///
539    /// Returns an error if the state file is missing, unreadable, or invalid.
540    pub async fn load(data_dir: &Path) -> Result<Self> {
541        let config_path = data_dir.join("overlay_bootstrap.json");
542
543        if !config_path.exists() {
544            return Err(OverlayError::NotInitialized);
545        }
546
547        let contents = tokio::fs::read_to_string(&config_path).await?;
548        let state: BootstrapState = serde_json::from_str(&contents)?;
549
550        let allocator = if let Some(alloc_state) = state.allocator_state {
551            Some(IpAllocator::from_state(alloc_state)?)
552        } else {
553            None
554        };
555
556        let slice_allocator = if let Some(snapshot) = state.slice_allocator_state {
557            Some(NodeSliceAllocator::restore(snapshot)?)
558        } else {
559            None
560        };
561
562        Ok(Self {
563            config: state.config,
564            peers: state.peers,
565            data_dir: data_dir.to_path_buf(),
566            allocator,
567            slice_allocator,
568            dns_config: None, // DNS config must be re-enabled after load
569            dns_handle: None,
570            transport: None,
571            #[cfg(feature = "nat")]
572            nat_traversal: None,
573            #[cfg(feature = "nat")]
574            relay_server: None,
575            #[cfg(feature = "nat")]
576            nat_config: None,
577        })
578    }
579
580    /// Save bootstrap state to disk
581    ///
582    /// # Errors
583    ///
584    /// Returns an error if serialization or file writing fails.
585    pub async fn save(&self) -> Result<()> {
586        let config_path = self.data_dir.join("overlay_bootstrap.json");
587
588        let state = BootstrapState {
589            config: self.config.clone(),
590            peers: self.peers.clone(),
591            allocator_state: self
592                .allocator
593                .as_ref()
594                .map(super::allocator::IpAllocator::to_state),
595            slice_allocator_state: self
596                .slice_allocator
597                .as_ref()
598                .map(NodeSliceAllocator::snapshot),
599        };
600
601        let contents = serde_json::to_string_pretty(&state)?;
602        tokio::fs::write(&config_path, contents).await?;
603
604        debug!(path = %config_path.display(), "Saved bootstrap state");
605        Ok(())
606    }
607
608    /// Enable DNS service discovery for the overlay network
609    ///
610    /// When DNS is enabled, peers are automatically registered with both:
611    /// - An IP-based hostname: `node-X-Y.zone` (e.g., `node-0-5.overlay.local`)
612    /// - A custom hostname if provided in `PeerConfig`
613    ///
614    /// The leader node additionally gets a `leader.zone` alias.
615    ///
616    /// # Arguments
617    /// * `zone` - DNS zone (e.g., "overlay.local.")
618    /// * `port` - DNS server port (default: 15353 to avoid conflicts)
619    ///
620    /// # Example
621    /// ```ignore
622    /// let bootstrap = OverlayBootstrap::init_leader(cidr, port, data_dir)
623    ///     .await?
624    ///     .with_dns("overlay.local.", 15353)?;
625    /// bootstrap.start().await?;
626    /// ```
627    ///
628    /// # Errors
629    ///
630    /// This method currently always succeeds but returns `Result` for API consistency.
631    pub fn with_dns(mut self, zone: &str, port: u16) -> Result<Self> {
632        self.dns_config = Some(DnsConfig {
633            zone: zone.to_string(),
634            port,
635            bind_addr: self.config.node_ip,
636        });
637        Ok(self)
638    }
639
640    /// Enable DNS with default port (15353)
641    ///
642    /// # Errors
643    ///
644    /// This method currently always succeeds but returns `Result` for API consistency.
645    pub fn with_dns_default(self, zone: &str) -> Result<Self> {
646        self.with_dns(zone, DEFAULT_DNS_PORT)
647    }
648
649    /// Override the NAT traversal configuration used by `start()`.
650    ///
651    /// When unset, `start()` falls back to [`crate::nat::NatConfig::default()`]
652    /// which enables NAT traversal with public STUN servers.
653    #[cfg(feature = "nat")]
654    #[must_use]
655    pub fn with_nat_config(mut self, nat: crate::nat::NatConfig) -> Self {
656        self.nat_config = Some(nat);
657        self
658    }
659
660    /// Get the DNS handle for managing records
661    ///
662    /// Returns None if DNS is not enabled or `start()` hasn't been called yet.
663    #[must_use]
664    pub fn dns_handle(&self) -> Option<&DnsHandle> {
665        self.dns_handle.as_ref()
666    }
667
668    /// Check if DNS is enabled
669    #[must_use]
670    pub fn dns_enabled(&self) -> bool {
671        self.dns_config.is_some()
672    }
673
674    /// Start the overlay network (create and configure overlay transport)
675    ///
676    /// This creates the boringtun TUN interface, assigns the overlay IP,
677    /// configures all known peers, and starts the DNS server if enabled.
678    ///
679    /// # Errors
680    ///
681    /// Returns an error if interface creation, peer configuration, or DNS startup fails.
682    pub async fn start(&mut self) -> Result<()> {
683        info!(
684            interface = %self.config.interface,
685            overlay_ip = %self.config.node_ip,
686            port = self.config.port,
687            dns_enabled = self.dns_config.is_some(),
688            "Starting overlay network"
689        );
690
691        // Convert our config to OverlayConfig
692        let overlay_config = crate::config::OverlayConfig {
693            local_endpoint: SocketAddr::new(
694                std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
695                self.config.port,
696            ),
697            private_key: self.config.private_key.clone(),
698            public_key: self.config.public_key.clone(),
699            overlay_cidr: self.config.allowed_ip(),
700            cluster_cidr: Some(self.config.cidr.clone()),
701            peer_discovery_interval: Duration::from_secs(30),
702            #[cfg(feature = "nat")]
703            nat: self.nat_config.clone().unwrap_or_default(),
704            ..crate::config::OverlayConfig::default()
705        };
706
707        #[cfg(feature = "nat")]
708        let nat_config = overlay_config.nat.clone();
709
710        // Create overlay transport
711        let mut transport = OverlayTransport::new(overlay_config, self.config.interface.clone());
712
713        // Create the interface
714        transport
715            .create_interface()
716            .await
717            .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
718
719        // On macOS, the kernel assigns a utunN name that may differ from
720        // the requested name. Update our config to reflect the actual name.
721        let actual_name = transport.interface_name().to_string();
722        if actual_name != self.config.interface {
723            info!(
724                requested = %self.config.interface,
725                actual = %actual_name,
726                "Interface name resolved by kernel"
727            );
728            self.config.interface = actual_name;
729        }
730
731        // Convert peers to PeerInfo
732        let peer_infos: Vec<PeerInfo> = self
733            .peers
734            .iter()
735            .filter_map(|p| match p.to_peer_info() {
736                Ok(info) => Some(info),
737                Err(e) => {
738                    warn!(peer = %p.node_id, error = %e, "Failed to parse peer info");
739                    None
740                }
741            })
742            .collect();
743
744        // Configure transport with peers
745        transport
746            .configure(&peer_infos)
747            .await
748            .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
749
750        // Store the transport so the TUN device stays alive for the overlay
751        // lifetime. Dropping the OverlayTransport destroys the boringtun device.
752        self.transport = Some(transport);
753
754        // NAT traversal: gather candidates and connect to peers
755        #[cfg(feature = "nat")]
756        self.start_nat_traversal(nat_config).await;
757
758        // Start DNS server if configured
759        self.start_dns().await?;
760
761        info!("Overlay network started successfully");
762        Ok(())
763    }
764
765    /// Start the DNS server and register all known peers.
766    async fn start_dns(&mut self) -> Result<()> {
767        let Some(dns_config) = &self.dns_config else {
768            return Ok(());
769        };
770
771        info!(
772            zone = %dns_config.zone,
773            port = dns_config.port,
774            "Starting DNS server for overlay"
775        );
776
777        let dns_server =
778            DnsServer::from_config(dns_config).map_err(|e| OverlayError::Dns(e.to_string()))?;
779
780        // Register self with IP-based hostname
781        let self_hostname = peer_hostname(self.config.node_ip);
782        dns_server
783            .add_record(&self_hostname, self.config.node_ip)
784            .await
785            .map_err(|e| OverlayError::Dns(e.to_string()))?;
786
787        // If leader, also register "leader" alias
788        if self.config.is_leader {
789            dns_server
790                .add_record("leader", self.config.node_ip)
791                .await
792                .map_err(|e| OverlayError::Dns(e.to_string()))?;
793            debug!(ip = %self.config.node_ip, "Registered leader.{}", dns_config.zone);
794        }
795
796        // Register existing peers
797        for peer in &self.peers {
798            // Always register IP-based hostname
799            let hostname = peer_hostname(peer.overlay_ip);
800            dns_server
801                .add_record(&hostname, peer.overlay_ip)
802                .await
803                .map_err(|e| OverlayError::Dns(e.to_string()))?;
804
805            // Also register custom hostname if provided
806            if let Some(custom) = &peer.hostname {
807                dns_server
808                    .add_record(custom, peer.overlay_ip)
809                    .await
810                    .map_err(|e| OverlayError::Dns(e.to_string()))?;
811                debug!(
812                    hostname = custom,
813                    ip = %peer.overlay_ip,
814                    "Registered custom hostname"
815                );
816            }
817        }
818
819        // Start the DNS server and store the handle
820        let handle = dns_server
821            .start()
822            .await
823            .map_err(|e| OverlayError::Dns(e.to_string()))?;
824        self.dns_handle = Some(handle);
825
826        info!("DNS server started successfully");
827        Ok(())
828    }
829
830    /// Initialize NAT traversal, gather candidates, and connect to known peers.
831    #[cfg(feature = "nat")]
832    async fn start_nat_traversal(&mut self, nat_config: crate::nat::NatConfig) {
833        if !nat_config.enabled {
834            return;
835        }
836
837        // Optionally start built-in relay server
838        if let Some(ref relay_config) = nat_config.relay_server {
839            let relay = RelayServer::new(relay_config, &self.config.private_key);
840            match relay.start().await {
841                Ok(()) => {
842                    info!("Built-in relay server started");
843                    self.relay_server = Some(relay);
844                }
845                Err(e) => {
846                    warn!(error = %e, "Failed to start relay server");
847                }
848            }
849        }
850
851        let mut nat = NatTraversal::new(nat_config, self.config.port);
852        match nat.gather_candidates().await {
853            Ok(candidates) => {
854                info!(count = candidates.len(), "Gathered NAT candidates");
855                if let Some(ref transport) = self.transport {
856                    for peer in &mut self.peers {
857                        if !peer.candidates.is_empty() {
858                            match nat
859                                .connect_to_peer(transport, &peer.public_key, &peer.candidates)
860                                .await
861                            {
862                                Ok(ct) => {
863                                    peer.connection_type = ct;
864                                    info!(
865                                        peer = %peer.node_id,
866                                        connection = %ct,
867                                        "NAT traversal succeeded"
868                                    );
869                                }
870                                Err(e) => warn!(
871                                    peer = %peer.node_id,
872                                    error = %e,
873                                    "NAT traversal failed"
874                                ),
875                            }
876                        }
877                    }
878                }
879                self.nat_traversal = Some(nat);
880            }
881            Err(e) => warn!(error = %e, "NAT candidate gathering failed"),
882        }
883    }
884
885    /// Stop the overlay network (shut down the boringtun transport)
886    ///
887    /// # Errors
888    ///
889    /// This method currently always succeeds but returns `Result` for API consistency.
890    #[allow(clippy::unused_async)]
891    pub async fn stop(&mut self) -> Result<()> {
892        info!(interface = %self.config.interface, "Stopping overlay network");
893
894        if let Some(mut transport) = self.transport.take() {
895            transport.shutdown();
896        }
897
898        Ok(())
899    }
900
901    /// Replay assigned slices from persisted `PeerConfig.slice_cidr` values
902    /// back into the slice allocator. Call on leader startup after `load()`
903    /// so the in-memory allocator matches what's on disk.
904    ///
905    /// No-op for workers (who have no allocator).
906    ///
907    /// # Errors
908    ///
909    /// Returns an error if the snapshot rebuild fails.
910    pub fn reconcile_existing_peers(&mut self) -> Result<()> {
911        let Some(ref mut allocator) = self.slice_allocator else {
912            return Ok(());
913        };
914        // Collect into a snapshot so we can restore cleanly.
915        let mut assigned: Vec<(String, String)> = Vec::new();
916        for peer in &self.peers {
917            if let Some(slice) = peer.slice_cidr {
918                assigned.push((peer.node_id.clone(), slice.to_string()));
919            }
920        }
921        if assigned.is_empty() {
922            return Ok(());
923        }
924        let snapshot = NodeSliceAllocatorSnapshot {
925            cluster_cidr: allocator.cluster_cidr().to_string(),
926            slice_prefix: allocator.slice_prefix(),
927            assigned,
928        };
929        *allocator = NodeSliceAllocator::restore(snapshot)?;
930        Ok(())
931    }
932
933    /// Add a new peer to the overlay network
934    ///
935    /// For leader nodes, this also allocates an IP address for the peer.
936    ///
937    /// # Errors
938    ///
939    /// Returns an error if no IPs are available, DNS registration fails, or state cannot be saved.
940    pub async fn add_peer(&mut self, mut peer: PeerConfig) -> Result<IpAddr> {
941        // If we're the leader, allocate an IP for this peer
942        let overlay_ip = if let Some(ref mut allocator) = self.allocator {
943            let ip = allocator.allocate().ok_or(OverlayError::NoAvailableIps)?;
944            peer.overlay_ip = ip;
945            ip
946        } else {
947            peer.overlay_ip
948        };
949
950        // If we're the leader and have a slice allocator, assign a /28 slice
951        // to this peer. Peers track their slice so `to_peer_info()` can emit the
952        // correct `allowed_ips`.
953        if let Some(ref mut slice_allocator) = self.slice_allocator {
954            let slice = slice_allocator.assign(&peer.node_id)?;
955            peer.slice_cidr = Some(slice);
956            // The peer's overlay_ip (reallocated above from the flat allocator)
957            // is left as-is for compatibility with callers that still read it;
958            // the authoritative routing info is `slice_cidr`.
959        }
960
961        // Add peer to overlay transport via UAPI
962        if let Ok(peer_info) = peer.to_peer_info() {
963            // Prefer the stored transport; fall back to a temporary instance
964            // (UAPI calls work via the Unix socket regardless of DeviceHandle)
965            let transport_ref: Option<&OverlayTransport> = self.transport.as_ref();
966
967            let result = if let Some(t) = transport_ref {
968                t.add_peer(&peer_info).await
969            } else {
970                let overlay_config = crate::config::OverlayConfig {
971                    local_endpoint: SocketAddr::new(
972                        std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
973                        self.config.port,
974                    ),
975                    private_key: self.config.private_key.clone(),
976                    public_key: self.config.public_key.clone(),
977                    overlay_cidr: self.config.allowed_ip(),
978                    cluster_cidr: Some(self.config.cidr.clone()),
979                    peer_discovery_interval: Duration::from_secs(30),
980                    #[cfg(feature = "nat")]
981                    nat: crate::nat::NatConfig::default(),
982                    ..crate::config::OverlayConfig::default()
983                };
984                let tmp = OverlayTransport::new(overlay_config, self.config.interface.clone());
985                tmp.add_peer(&peer_info).await
986            };
987
988            match result {
989                Ok(()) => debug!(peer = %peer.node_id, "Added peer to overlay"),
990                Err(e) => {
991                    warn!(peer = %peer.node_id, error = %e, "Failed to add peer to overlay (interface may not be up)");
992                }
993            }
994        }
995
996        // Register peer in DNS if enabled
997        if let Some(ref dns_handle) = self.dns_handle {
998            // IP-based hostname
999            let hostname = peer_hostname(overlay_ip);
1000            dns_handle
1001                .add_record(&hostname, overlay_ip)
1002                .await
1003                .map_err(|e| OverlayError::Dns(e.to_string()))?;
1004            debug!(hostname = %hostname, ip = %overlay_ip, "Registered peer in DNS");
1005
1006            // Custom hostname alias if provided
1007            if let Some(ref custom) = peer.hostname {
1008                dns_handle
1009                    .add_record(custom, overlay_ip)
1010                    .await
1011                    .map_err(|e| OverlayError::Dns(e.to_string()))?;
1012                debug!(hostname = %custom, ip = %overlay_ip, "Registered custom hostname in DNS");
1013            }
1014        }
1015
1016        // NAT traversal for new peer
1017        #[cfg(feature = "nat")]
1018        {
1019            if let (Some(ref nat), Some(ref transport)) = (&self.nat_traversal, &self.transport) {
1020                if !peer.candidates.is_empty() {
1021                    match nat
1022                        .connect_to_peer(transport, &peer.public_key, &peer.candidates)
1023                        .await
1024                    {
1025                        Ok(ct) => {
1026                            peer.connection_type = ct;
1027                            info!(
1028                                peer = %peer.node_id,
1029                                connection = %ct,
1030                                "NAT traversal for new peer"
1031                            );
1032                        }
1033                        Err(e) => warn!(
1034                            peer = %peer.node_id,
1035                            error = %e,
1036                            "NAT failed for new peer"
1037                        ),
1038                    }
1039                }
1040            }
1041        }
1042
1043        // Add to peer list
1044        self.peers.push(peer);
1045
1046        // Persist state
1047        self.save().await?;
1048
1049        info!(peer_ip = %overlay_ip, "Added peer to overlay");
1050        Ok(overlay_ip)
1051    }
1052
1053    /// Remove a peer from the overlay network
1054    ///
1055    /// # Errors
1056    ///
1057    /// Returns an error if the peer is not found, DNS removal fails, or state cannot be saved.
1058    pub async fn remove_peer(&mut self, public_key: &str) -> Result<()> {
1059        // Find the peer
1060        let peer_idx = self
1061            .peers
1062            .iter()
1063            .position(|p| p.public_key == public_key)
1064            .ok_or_else(|| OverlayError::PeerNotFound(public_key.to_string()))?;
1065
1066        let peer = &self.peers[peer_idx];
1067
1068        // Capture peer info for DNS removal before we lose the reference
1069        let peer_overlay_ip = peer.overlay_ip;
1070        let peer_custom_hostname = peer.hostname.clone();
1071
1072        // Release IP if we're managing allocation
1073        if let Some(ref mut allocator) = self.allocator {
1074            allocator.release(peer_overlay_ip);
1075        }
1076
1077        // Remove from DNS if enabled
1078        if let Some(ref dns_handle) = self.dns_handle {
1079            // Remove IP-based hostname
1080            let hostname = peer_hostname(peer_overlay_ip);
1081            dns_handle
1082                .remove_record(&hostname)
1083                .await
1084                .map_err(|e| OverlayError::Dns(e.to_string()))?;
1085            debug!(hostname = %hostname, "Removed peer from DNS");
1086
1087            // Remove custom hostname if it was set
1088            if let Some(ref custom) = peer_custom_hostname {
1089                dns_handle
1090                    .remove_record(custom)
1091                    .await
1092                    .map_err(|e| OverlayError::Dns(e.to_string()))?;
1093                debug!(hostname = %custom, "Removed custom hostname from DNS");
1094            }
1095        }
1096
1097        // Remove peer from overlay transport via UAPI
1098        let transport_ref: Option<&OverlayTransport> = self.transport.as_ref();
1099
1100        let result = if let Some(t) = transport_ref {
1101            t.remove_peer(public_key).await
1102        } else {
1103            let overlay_config = crate::config::OverlayConfig {
1104                local_endpoint: SocketAddr::new(
1105                    std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
1106                    self.config.port,
1107                ),
1108                private_key: self.config.private_key.clone(),
1109                public_key: self.config.public_key.clone(),
1110                overlay_cidr: self.config.allowed_ip(),
1111                cluster_cidr: Some(self.config.cidr.clone()),
1112                peer_discovery_interval: Duration::from_secs(30),
1113                #[cfg(feature = "nat")]
1114                nat: crate::nat::NatConfig::default(),
1115                ..crate::config::OverlayConfig::default()
1116            };
1117            let tmp = OverlayTransport::new(overlay_config, self.config.interface.clone());
1118            tmp.remove_peer(public_key).await
1119        };
1120
1121        match result {
1122            Ok(()) => debug!(public_key = public_key, "Removed peer from overlay"),
1123            Err(e) => {
1124                warn!(public_key = public_key, error = %e, "Failed to remove peer from overlay");
1125            }
1126        }
1127
1128        // Remove from peer list
1129        self.peers.remove(peer_idx);
1130
1131        // Persist state
1132        self.save().await?;
1133
1134        info!(public_key = public_key, "Removed peer from overlay");
1135        Ok(())
1136    }
1137
1138    /// Get this node's public key
1139    #[must_use]
1140    pub fn public_key(&self) -> &str {
1141        &self.config.public_key
1142    }
1143
1144    /// Get this node's overlay IP (IPv4 or IPv6)
1145    #[must_use]
1146    pub fn node_ip(&self) -> IpAddr {
1147        self.config.node_ip
1148    }
1149
1150    /// Get the overlay CIDR
1151    #[must_use]
1152    pub fn cidr(&self) -> &str {
1153        &self.config.cidr
1154    }
1155
1156    /// Get the overlay interface name
1157    #[must_use]
1158    pub fn interface(&self) -> &str {
1159        &self.config.interface
1160    }
1161
1162    /// Get the overlay listen port
1163    #[must_use]
1164    pub fn port(&self) -> u16 {
1165        self.config.port
1166    }
1167
1168    /// Check if this node is the leader
1169    #[must_use]
1170    pub fn is_leader(&self) -> bool {
1171        self.config.is_leader
1172    }
1173
1174    /// Get configured peers
1175    #[must_use]
1176    pub fn peers(&self) -> &[PeerConfig] {
1177        &self.peers
1178    }
1179
1180    /// Get the bootstrap config
1181    #[must_use]
1182    pub fn config(&self) -> &BootstrapConfig {
1183        &self.config
1184    }
1185
1186    /// Allocate an IP for a new peer (leader only)
1187    ///
1188    /// This is used by the control plane when processing join requests.
1189    ///
1190    /// # Errors
1191    ///
1192    /// Returns an error if this node is not a leader or no IPs are available.
1193    pub fn allocate_peer_ip(&mut self) -> Result<IpAddr> {
1194        let allocator = self
1195            .allocator
1196            .as_mut()
1197            .ok_or(OverlayError::Config("Not a leader node".to_string()))?;
1198
1199        allocator.allocate().ok_or(OverlayError::NoAvailableIps)
1200    }
1201
1202    /// Get IP allocation statistics (leader only)
1203    #[must_use]
1204    #[allow(clippy::cast_possible_truncation)]
1205    pub fn allocation_stats(&self) -> Option<(u32, u32)> {
1206        self.allocator
1207            .as_ref()
1208            .map(|a| (a.allocated_count() as u32, a.total_hosts()))
1209    }
1210
1211    /// Perform NAT maintenance: refresh STUN, attempt relay upgrades.
1212    ///
1213    /// Call this periodically from the runtime's main loop. Re-probes
1214    /// STUN servers to detect reflexive address changes and attempts
1215    /// to upgrade relayed connections to direct or hole-punched.
1216    ///
1217    /// # Errors
1218    ///
1219    /// Returns an error if STUN refresh fails.
1220    #[cfg(feature = "nat")]
1221    pub async fn nat_maintenance_tick(&mut self) -> Result<()> {
1222        let (Some(nat), Some(transport)) = (&mut self.nat_traversal, &self.transport) else {
1223            return Ok(());
1224        };
1225
1226        if nat.refresh().await? {
1227            info!("Reflexive address changed");
1228        }
1229
1230        for peer in &mut self.peers {
1231            if peer.connection_type == ConnectionType::Relayed && !peer.candidates.is_empty() {
1232                if let Ok(Some(upgraded)) = nat
1233                    .attempt_upgrade(transport, &peer.public_key, &peer.candidates)
1234                    .await
1235                {
1236                    peer.connection_type = upgraded;
1237                    info!(
1238                        peer = %peer.node_id,
1239                        connection = %upgraded,
1240                        "Upgraded relayed connection"
1241                    );
1242                }
1243            }
1244        }
1245
1246        Ok(())
1247    }
1248
1249    /// Get this node's NAT candidates for sharing with peers.
1250    ///
1251    /// Returns an empty vec if NAT traversal has not been initialized
1252    /// or no candidates were gathered.
1253    #[cfg(feature = "nat")]
1254    #[must_use]
1255    pub fn nat_candidates(&self) -> Vec<Candidate> {
1256        self.nat_traversal
1257            .as_ref()
1258            .map(|n| n.local_candidates().to_vec())
1259            .unwrap_or_default()
1260    }
1261}
1262
1263/// Get current Unix timestamp
1264fn current_timestamp() -> u64 {
1265    std::time::SystemTime::now()
1266        .duration_since(std::time::UNIX_EPOCH)
1267        .unwrap_or_default()
1268        .as_secs()
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273    use super::*;
1274    use std::net::Ipv4Addr;
1275
1276    #[test]
1277    fn test_bootstrap_config_allowed_ip_v4() {
1278        let config = BootstrapConfig {
1279            cidr: "10.200.0.0/16".to_string(),
1280            node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)),
1281            interface: DEFAULT_INTERFACE_NAME.to_string(),
1282            port: DEFAULT_WG_PORT,
1283            private_key: "test_private".to_string(),
1284            public_key: "test_public".to_string(),
1285            is_leader: true,
1286            created_at: 0,
1287            slice_cidr: None,
1288        };
1289
1290        assert_eq!(config.allowed_ip(), "10.200.0.1/32");
1291    }
1292
1293    #[test]
1294    fn test_bootstrap_config_allowed_ip_v6() {
1295        let config = BootstrapConfig {
1296            cidr: "fd00:200::/48".to_string(),
1297            node_ip: "fd00:200::1".parse::<IpAddr>().unwrap(),
1298            interface: DEFAULT_INTERFACE_NAME.to_string(),
1299            port: DEFAULT_WG_PORT,
1300            private_key: "test_private".to_string(),
1301            public_key: "test_public".to_string(),
1302            is_leader: true,
1303            created_at: 0,
1304            slice_cidr: None,
1305        };
1306
1307        assert_eq!(config.allowed_ip(), "fd00:200::1/128");
1308    }
1309
1310    #[test]
1311    fn test_peer_config_new_v4() {
1312        let peer = PeerConfig::new(
1313            "node-1".to_string(),
1314            "pubkey123".to_string(),
1315            "192.168.1.100:51820".to_string(),
1316            IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5)),
1317        );
1318
1319        assert_eq!(peer.node_id, "node-1");
1320        assert_eq!(peer.keepalive, Some(DEFAULT_KEEPALIVE_SECS));
1321        assert_eq!(peer.hostname, None);
1322    }
1323
1324    #[test]
1325    fn test_peer_config_new_v6() {
1326        let peer = PeerConfig::new(
1327            "node-1".to_string(),
1328            "pubkey123".to_string(),
1329            "[::1]:51820".to_string(),
1330            "fd00:200::5".parse::<IpAddr>().unwrap(),
1331        );
1332
1333        assert_eq!(peer.node_id, "node-1");
1334        assert_eq!(peer.keepalive, Some(DEFAULT_KEEPALIVE_SECS));
1335        assert_eq!(peer.hostname, None);
1336    }
1337
1338    #[test]
1339    fn test_peer_config_with_hostname() {
1340        let peer = PeerConfig::new(
1341            "node-1".to_string(),
1342            "pubkey123".to_string(),
1343            "192.168.1.100:51820".to_string(),
1344            IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5)),
1345        )
1346        .with_hostname("web-server");
1347
1348        assert_eq!(peer.hostname, Some("web-server".to_string()));
1349    }
1350
1351    #[test]
1352    fn test_peer_config_to_peer_info_v4() {
1353        let peer = PeerConfig::new(
1354            "node-1".to_string(),
1355            "pubkey123".to_string(),
1356            "192.168.1.100:51820".to_string(),
1357            IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5)),
1358        );
1359
1360        let peer_info = peer.to_peer_info().unwrap();
1361        assert_eq!(peer_info.public_key, "pubkey123");
1362        assert_eq!(peer_info.allowed_ips, "10.200.0.5/32");
1363    }
1364
1365    #[test]
1366    fn test_peer_config_to_peer_info_v6() {
1367        let peer = PeerConfig::new(
1368            "node-1".to_string(),
1369            "pubkey123".to_string(),
1370            "[::1]:51820".to_string(),
1371            "fd00:200::5".parse::<IpAddr>().unwrap(),
1372        );
1373
1374        let peer_info = peer.to_peer_info().unwrap();
1375        assert_eq!(peer_info.public_key, "pubkey123");
1376        assert_eq!(peer_info.allowed_ips, "fd00:200::5/128");
1377    }
1378
1379    #[test]
1380    fn test_bootstrap_state_serialization_v4() {
1381        let config = BootstrapConfig {
1382            cidr: "10.200.0.0/16".to_string(),
1383            node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)),
1384            interface: DEFAULT_INTERFACE_NAME.to_string(),
1385            port: DEFAULT_WG_PORT,
1386            private_key: "private".to_string(),
1387            public_key: "public".to_string(),
1388            is_leader: true,
1389            created_at: 1_234_567_890,
1390            slice_cidr: None,
1391        };
1392
1393        let state = BootstrapState {
1394            config,
1395            peers: vec![],
1396            allocator_state: None,
1397            slice_allocator_state: None,
1398        };
1399
1400        let json = serde_json::to_string_pretty(&state).unwrap();
1401        let deserialized: BootstrapState = serde_json::from_str(&json).unwrap();
1402
1403        assert_eq!(deserialized.config.cidr, "10.200.0.0/16");
1404        assert_eq!(deserialized.config.node_ip.to_string(), "10.200.0.1");
1405    }
1406
1407    #[test]
1408    fn test_bootstrap_state_serialization_v6() {
1409        let config = BootstrapConfig {
1410            cidr: "fd00:200::/48".to_string(),
1411            node_ip: "fd00:200::1".parse::<IpAddr>().unwrap(),
1412            interface: DEFAULT_INTERFACE_NAME.to_string(),
1413            port: DEFAULT_WG_PORT,
1414            private_key: "private".to_string(),
1415            public_key: "public".to_string(),
1416            is_leader: true,
1417            created_at: 1_234_567_890,
1418            slice_cidr: None,
1419        };
1420
1421        let state = BootstrapState {
1422            config,
1423            peers: vec![],
1424            allocator_state: None,
1425            slice_allocator_state: None,
1426        };
1427
1428        let json = serde_json::to_string_pretty(&state).unwrap();
1429        let deserialized: BootstrapState = serde_json::from_str(&json).unwrap();
1430
1431        assert_eq!(deserialized.config.cidr, "fd00:200::/48");
1432        assert_eq!(deserialized.config.node_ip.to_string(), "fd00:200::1");
1433    }
1434
1435    #[test]
1436    fn test_default_overlay_cidr_v6_constant() {
1437        // Verify the IPv6 CIDR constant is valid
1438        let net: ipnet::IpNet = DEFAULT_OVERLAY_CIDR_V6.parse().unwrap();
1439        assert!(matches!(net, ipnet::IpNet::V6(_)));
1440        assert_eq!(net.prefix_len(), 48);
1441    }
1442
1443    #[test]
1444    fn test_to_peer_info_uses_slice_when_set() {
1445        let peer = PeerConfig::new(
1446            "node-42".to_string(),
1447            "pubkey-xyz".to_string(),
1448            "192.168.1.100:51820".to_string(),
1449            IpAddr::V4(Ipv4Addr::new(10, 200, 42, 1)),
1450        )
1451        .with_slice_cidr("10.200.42.0/28".parse().unwrap());
1452
1453        let peer_info = peer.to_peer_info().unwrap();
1454        assert_eq!(peer_info.allowed_ips, "10.200.42.0/28");
1455    }
1456
1457    #[test]
1458    fn test_to_peer_info_falls_back_to_node_ip_when_no_slice() {
1459        let peer = PeerConfig::new(
1460            "node-5".to_string(),
1461            "pubkey-abc".to_string(),
1462            "192.168.1.100:51820".to_string(),
1463            "10.200.0.5".parse().unwrap(),
1464        );
1465
1466        let peer_info = peer.to_peer_info().unwrap();
1467        assert_eq!(peer_info.allowed_ips, "10.200.0.5/32");
1468    }
1469
1470    #[test]
1471    fn test_bootstrap_config_allowed_ip_prefers_slice() {
1472        let config = BootstrapConfig {
1473            cidr: "10.200.0.0/16".to_string(),
1474            node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 7, 1)),
1475            interface: DEFAULT_INTERFACE_NAME.to_string(),
1476            port: DEFAULT_WG_PORT,
1477            private_key: "private".to_string(),
1478            public_key: "public".to_string(),
1479            is_leader: false,
1480            created_at: 0,
1481            slice_cidr: Some("10.200.7.0/28".parse().unwrap()),
1482        };
1483
1484        assert_eq!(config.allowed_ip(), "10.200.7.0/28");
1485    }
1486
1487    #[test]
1488    fn test_bootstrap_config_allowed_ip_falls_back_to_node_ip() {
1489        let config = BootstrapConfig {
1490            cidr: "10.200.0.0/16".to_string(),
1491            node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 0, 9)),
1492            interface: DEFAULT_INTERFACE_NAME.to_string(),
1493            port: DEFAULT_WG_PORT,
1494            private_key: "private".to_string(),
1495            public_key: "public".to_string(),
1496            is_leader: false,
1497            created_at: 0,
1498            slice_cidr: None,
1499        };
1500
1501        assert_eq!(config.allowed_ip(), "10.200.0.9/32");
1502    }
1503}