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        };
705
706        #[cfg(feature = "nat")]
707        let nat_config = overlay_config.nat.clone();
708
709        // Create overlay transport
710        let mut transport = OverlayTransport::new(overlay_config, self.config.interface.clone());
711
712        // Create the interface
713        transport
714            .create_interface()
715            .await
716            .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
717
718        // On macOS, the kernel assigns a utunN name that may differ from
719        // the requested name. Update our config to reflect the actual name.
720        let actual_name = transport.interface_name().to_string();
721        if actual_name != self.config.interface {
722            info!(
723                requested = %self.config.interface,
724                actual = %actual_name,
725                "Interface name resolved by kernel"
726            );
727            self.config.interface = actual_name;
728        }
729
730        // Convert peers to PeerInfo
731        let peer_infos: Vec<PeerInfo> = self
732            .peers
733            .iter()
734            .filter_map(|p| match p.to_peer_info() {
735                Ok(info) => Some(info),
736                Err(e) => {
737                    warn!(peer = %p.node_id, error = %e, "Failed to parse peer info");
738                    None
739                }
740            })
741            .collect();
742
743        // Configure transport with peers
744        transport
745            .configure(&peer_infos)
746            .await
747            .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
748
749        // Store the transport so the TUN device stays alive for the overlay
750        // lifetime. Dropping the OverlayTransport destroys the boringtun device.
751        self.transport = Some(transport);
752
753        // NAT traversal: gather candidates and connect to peers
754        #[cfg(feature = "nat")]
755        self.start_nat_traversal(nat_config).await;
756
757        // Start DNS server if configured
758        self.start_dns().await?;
759
760        info!("Overlay network started successfully");
761        Ok(())
762    }
763
764    /// Start the DNS server and register all known peers.
765    async fn start_dns(&mut self) -> Result<()> {
766        let Some(dns_config) = &self.dns_config else {
767            return Ok(());
768        };
769
770        info!(
771            zone = %dns_config.zone,
772            port = dns_config.port,
773            "Starting DNS server for overlay"
774        );
775
776        let dns_server =
777            DnsServer::from_config(dns_config).map_err(|e| OverlayError::Dns(e.to_string()))?;
778
779        // Register self with IP-based hostname
780        let self_hostname = peer_hostname(self.config.node_ip);
781        dns_server
782            .add_record(&self_hostname, self.config.node_ip)
783            .await
784            .map_err(|e| OverlayError::Dns(e.to_string()))?;
785
786        // If leader, also register "leader" alias
787        if self.config.is_leader {
788            dns_server
789                .add_record("leader", self.config.node_ip)
790                .await
791                .map_err(|e| OverlayError::Dns(e.to_string()))?;
792            debug!(ip = %self.config.node_ip, "Registered leader.{}", dns_config.zone);
793        }
794
795        // Register existing peers
796        for peer in &self.peers {
797            // Always register IP-based hostname
798            let hostname = peer_hostname(peer.overlay_ip);
799            dns_server
800                .add_record(&hostname, peer.overlay_ip)
801                .await
802                .map_err(|e| OverlayError::Dns(e.to_string()))?;
803
804            // Also register custom hostname if provided
805            if let Some(custom) = &peer.hostname {
806                dns_server
807                    .add_record(custom, peer.overlay_ip)
808                    .await
809                    .map_err(|e| OverlayError::Dns(e.to_string()))?;
810                debug!(
811                    hostname = custom,
812                    ip = %peer.overlay_ip,
813                    "Registered custom hostname"
814                );
815            }
816        }
817
818        // Start the DNS server and store the handle
819        let handle = dns_server
820            .start()
821            .await
822            .map_err(|e| OverlayError::Dns(e.to_string()))?;
823        self.dns_handle = Some(handle);
824
825        info!("DNS server started successfully");
826        Ok(())
827    }
828
829    /// Initialize NAT traversal, gather candidates, and connect to known peers.
830    #[cfg(feature = "nat")]
831    async fn start_nat_traversal(&mut self, nat_config: crate::nat::NatConfig) {
832        if !nat_config.enabled {
833            return;
834        }
835
836        // Optionally start built-in relay server
837        if let Some(ref relay_config) = nat_config.relay_server {
838            let relay = RelayServer::new(relay_config, &self.config.private_key);
839            match relay.start().await {
840                Ok(()) => {
841                    info!("Built-in relay server started");
842                    self.relay_server = Some(relay);
843                }
844                Err(e) => {
845                    warn!(error = %e, "Failed to start relay server");
846                }
847            }
848        }
849
850        let mut nat = NatTraversal::new(nat_config, self.config.port);
851        match nat.gather_candidates().await {
852            Ok(candidates) => {
853                info!(count = candidates.len(), "Gathered NAT candidates");
854                if let Some(ref transport) = self.transport {
855                    for peer in &mut self.peers {
856                        if !peer.candidates.is_empty() {
857                            match nat
858                                .connect_to_peer(transport, &peer.public_key, &peer.candidates)
859                                .await
860                            {
861                                Ok(ct) => {
862                                    peer.connection_type = ct;
863                                    info!(
864                                        peer = %peer.node_id,
865                                        connection = %ct,
866                                        "NAT traversal succeeded"
867                                    );
868                                }
869                                Err(e) => warn!(
870                                    peer = %peer.node_id,
871                                    error = %e,
872                                    "NAT traversal failed"
873                                ),
874                            }
875                        }
876                    }
877                }
878                self.nat_traversal = Some(nat);
879            }
880            Err(e) => warn!(error = %e, "NAT candidate gathering failed"),
881        }
882    }
883
884    /// Stop the overlay network (shut down the boringtun transport)
885    ///
886    /// # Errors
887    ///
888    /// This method currently always succeeds but returns `Result` for API consistency.
889    #[allow(clippy::unused_async)]
890    pub async fn stop(&mut self) -> Result<()> {
891        info!(interface = %self.config.interface, "Stopping overlay network");
892
893        if let Some(mut transport) = self.transport.take() {
894            transport.shutdown();
895        }
896
897        Ok(())
898    }
899
900    /// Replay assigned slices from persisted `PeerConfig.slice_cidr` values
901    /// back into the slice allocator. Call on leader startup after `load()`
902    /// so the in-memory allocator matches what's on disk.
903    ///
904    /// No-op for workers (who have no allocator).
905    ///
906    /// # Errors
907    ///
908    /// Returns an error if the snapshot rebuild fails.
909    pub fn reconcile_existing_peers(&mut self) -> Result<()> {
910        let Some(ref mut allocator) = self.slice_allocator else {
911            return Ok(());
912        };
913        // Collect into a snapshot so we can restore cleanly.
914        let mut assigned: Vec<(String, String)> = Vec::new();
915        for peer in &self.peers {
916            if let Some(slice) = peer.slice_cidr {
917                assigned.push((peer.node_id.clone(), slice.to_string()));
918            }
919        }
920        if assigned.is_empty() {
921            return Ok(());
922        }
923        let snapshot = NodeSliceAllocatorSnapshot {
924            cluster_cidr: allocator.cluster_cidr().to_string(),
925            slice_prefix: allocator.slice_prefix(),
926            assigned,
927        };
928        *allocator = NodeSliceAllocator::restore(snapshot)?;
929        Ok(())
930    }
931
932    /// Add a new peer to the overlay network
933    ///
934    /// For leader nodes, this also allocates an IP address for the peer.
935    ///
936    /// # Errors
937    ///
938    /// Returns an error if no IPs are available, DNS registration fails, or state cannot be saved.
939    pub async fn add_peer(&mut self, mut peer: PeerConfig) -> Result<IpAddr> {
940        // If we're the leader, allocate an IP for this peer
941        let overlay_ip = if let Some(ref mut allocator) = self.allocator {
942            let ip = allocator.allocate().ok_or(OverlayError::NoAvailableIps)?;
943            peer.overlay_ip = ip;
944            ip
945        } else {
946            peer.overlay_ip
947        };
948
949        // If we're the leader and have a slice allocator, assign a /28 slice
950        // to this peer. Peers track their slice so `to_peer_info()` can emit the
951        // correct `allowed_ips`.
952        if let Some(ref mut slice_allocator) = self.slice_allocator {
953            let slice = slice_allocator.assign(&peer.node_id)?;
954            peer.slice_cidr = Some(slice);
955            // The peer's overlay_ip (reallocated above from the flat allocator)
956            // is left as-is for compatibility with callers that still read it;
957            // the authoritative routing info is `slice_cidr`.
958        }
959
960        // Add peer to overlay transport via UAPI
961        if let Ok(peer_info) = peer.to_peer_info() {
962            // Prefer the stored transport; fall back to a temporary instance
963            // (UAPI calls work via the Unix socket regardless of DeviceHandle)
964            let transport_ref: Option<&OverlayTransport> = self.transport.as_ref();
965
966            let result = if let Some(t) = transport_ref {
967                t.add_peer(&peer_info).await
968            } else {
969                let overlay_config = crate::config::OverlayConfig {
970                    local_endpoint: SocketAddr::new(
971                        std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
972                        self.config.port,
973                    ),
974                    private_key: self.config.private_key.clone(),
975                    public_key: self.config.public_key.clone(),
976                    overlay_cidr: self.config.allowed_ip(),
977                    cluster_cidr: Some(self.config.cidr.clone()),
978                    peer_discovery_interval: Duration::from_secs(30),
979                    #[cfg(feature = "nat")]
980                    nat: crate::nat::NatConfig::default(),
981                };
982                let tmp = OverlayTransport::new(overlay_config, self.config.interface.clone());
983                tmp.add_peer(&peer_info).await
984            };
985
986            match result {
987                Ok(()) => debug!(peer = %peer.node_id, "Added peer to overlay"),
988                Err(e) => {
989                    warn!(peer = %peer.node_id, error = %e, "Failed to add peer to overlay (interface may not be up)");
990                }
991            }
992        }
993
994        // Register peer in DNS if enabled
995        if let Some(ref dns_handle) = self.dns_handle {
996            // IP-based hostname
997            let hostname = peer_hostname(overlay_ip);
998            dns_handle
999                .add_record(&hostname, overlay_ip)
1000                .await
1001                .map_err(|e| OverlayError::Dns(e.to_string()))?;
1002            debug!(hostname = %hostname, ip = %overlay_ip, "Registered peer in DNS");
1003
1004            // Custom hostname alias if provided
1005            if let Some(ref custom) = peer.hostname {
1006                dns_handle
1007                    .add_record(custom, overlay_ip)
1008                    .await
1009                    .map_err(|e| OverlayError::Dns(e.to_string()))?;
1010                debug!(hostname = %custom, ip = %overlay_ip, "Registered custom hostname in DNS");
1011            }
1012        }
1013
1014        // NAT traversal for new peer
1015        #[cfg(feature = "nat")]
1016        {
1017            if let (Some(ref nat), Some(ref transport)) = (&self.nat_traversal, &self.transport) {
1018                if !peer.candidates.is_empty() {
1019                    match nat
1020                        .connect_to_peer(transport, &peer.public_key, &peer.candidates)
1021                        .await
1022                    {
1023                        Ok(ct) => {
1024                            peer.connection_type = ct;
1025                            info!(
1026                                peer = %peer.node_id,
1027                                connection = %ct,
1028                                "NAT traversal for new peer"
1029                            );
1030                        }
1031                        Err(e) => warn!(
1032                            peer = %peer.node_id,
1033                            error = %e,
1034                            "NAT failed for new peer"
1035                        ),
1036                    }
1037                }
1038            }
1039        }
1040
1041        // Add to peer list
1042        self.peers.push(peer);
1043
1044        // Persist state
1045        self.save().await?;
1046
1047        info!(peer_ip = %overlay_ip, "Added peer to overlay");
1048        Ok(overlay_ip)
1049    }
1050
1051    /// Remove a peer from the overlay network
1052    ///
1053    /// # Errors
1054    ///
1055    /// Returns an error if the peer is not found, DNS removal fails, or state cannot be saved.
1056    pub async fn remove_peer(&mut self, public_key: &str) -> Result<()> {
1057        // Find the peer
1058        let peer_idx = self
1059            .peers
1060            .iter()
1061            .position(|p| p.public_key == public_key)
1062            .ok_or_else(|| OverlayError::PeerNotFound(public_key.to_string()))?;
1063
1064        let peer = &self.peers[peer_idx];
1065
1066        // Capture peer info for DNS removal before we lose the reference
1067        let peer_overlay_ip = peer.overlay_ip;
1068        let peer_custom_hostname = peer.hostname.clone();
1069
1070        // Release IP if we're managing allocation
1071        if let Some(ref mut allocator) = self.allocator {
1072            allocator.release(peer_overlay_ip);
1073        }
1074
1075        // Remove from DNS if enabled
1076        if let Some(ref dns_handle) = self.dns_handle {
1077            // Remove IP-based hostname
1078            let hostname = peer_hostname(peer_overlay_ip);
1079            dns_handle
1080                .remove_record(&hostname)
1081                .await
1082                .map_err(|e| OverlayError::Dns(e.to_string()))?;
1083            debug!(hostname = %hostname, "Removed peer from DNS");
1084
1085            // Remove custom hostname if it was set
1086            if let Some(ref custom) = peer_custom_hostname {
1087                dns_handle
1088                    .remove_record(custom)
1089                    .await
1090                    .map_err(|e| OverlayError::Dns(e.to_string()))?;
1091                debug!(hostname = %custom, "Removed custom hostname from DNS");
1092            }
1093        }
1094
1095        // Remove peer from overlay transport via UAPI
1096        let transport_ref: Option<&OverlayTransport> = self.transport.as_ref();
1097
1098        let result = if let Some(t) = transport_ref {
1099            t.remove_peer(public_key).await
1100        } else {
1101            let overlay_config = crate::config::OverlayConfig {
1102                local_endpoint: SocketAddr::new(
1103                    std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
1104                    self.config.port,
1105                ),
1106                private_key: self.config.private_key.clone(),
1107                public_key: self.config.public_key.clone(),
1108                overlay_cidr: self.config.allowed_ip(),
1109                cluster_cidr: Some(self.config.cidr.clone()),
1110                peer_discovery_interval: Duration::from_secs(30),
1111                #[cfg(feature = "nat")]
1112                nat: crate::nat::NatConfig::default(),
1113            };
1114            let tmp = OverlayTransport::new(overlay_config, self.config.interface.clone());
1115            tmp.remove_peer(public_key).await
1116        };
1117
1118        match result {
1119            Ok(()) => debug!(public_key = public_key, "Removed peer from overlay"),
1120            Err(e) => {
1121                warn!(public_key = public_key, error = %e, "Failed to remove peer from overlay");
1122            }
1123        }
1124
1125        // Remove from peer list
1126        self.peers.remove(peer_idx);
1127
1128        // Persist state
1129        self.save().await?;
1130
1131        info!(public_key = public_key, "Removed peer from overlay");
1132        Ok(())
1133    }
1134
1135    /// Get this node's public key
1136    #[must_use]
1137    pub fn public_key(&self) -> &str {
1138        &self.config.public_key
1139    }
1140
1141    /// Get this node's overlay IP (IPv4 or IPv6)
1142    #[must_use]
1143    pub fn node_ip(&self) -> IpAddr {
1144        self.config.node_ip
1145    }
1146
1147    /// Get the overlay CIDR
1148    #[must_use]
1149    pub fn cidr(&self) -> &str {
1150        &self.config.cidr
1151    }
1152
1153    /// Get the overlay interface name
1154    #[must_use]
1155    pub fn interface(&self) -> &str {
1156        &self.config.interface
1157    }
1158
1159    /// Get the overlay listen port
1160    #[must_use]
1161    pub fn port(&self) -> u16 {
1162        self.config.port
1163    }
1164
1165    /// Check if this node is the leader
1166    #[must_use]
1167    pub fn is_leader(&self) -> bool {
1168        self.config.is_leader
1169    }
1170
1171    /// Get configured peers
1172    #[must_use]
1173    pub fn peers(&self) -> &[PeerConfig] {
1174        &self.peers
1175    }
1176
1177    /// Get the bootstrap config
1178    #[must_use]
1179    pub fn config(&self) -> &BootstrapConfig {
1180        &self.config
1181    }
1182
1183    /// Allocate an IP for a new peer (leader only)
1184    ///
1185    /// This is used by the control plane when processing join requests.
1186    ///
1187    /// # Errors
1188    ///
1189    /// Returns an error if this node is not a leader or no IPs are available.
1190    pub fn allocate_peer_ip(&mut self) -> Result<IpAddr> {
1191        let allocator = self
1192            .allocator
1193            .as_mut()
1194            .ok_or(OverlayError::Config("Not a leader node".to_string()))?;
1195
1196        allocator.allocate().ok_or(OverlayError::NoAvailableIps)
1197    }
1198
1199    /// Get IP allocation statistics (leader only)
1200    #[must_use]
1201    #[allow(clippy::cast_possible_truncation)]
1202    pub fn allocation_stats(&self) -> Option<(u32, u32)> {
1203        self.allocator
1204            .as_ref()
1205            .map(|a| (a.allocated_count() as u32, a.total_hosts()))
1206    }
1207
1208    /// Perform NAT maintenance: refresh STUN, attempt relay upgrades.
1209    ///
1210    /// Call this periodically from the runtime's main loop. Re-probes
1211    /// STUN servers to detect reflexive address changes and attempts
1212    /// to upgrade relayed connections to direct or hole-punched.
1213    ///
1214    /// # Errors
1215    ///
1216    /// Returns an error if STUN refresh fails.
1217    #[cfg(feature = "nat")]
1218    pub async fn nat_maintenance_tick(&mut self) -> Result<()> {
1219        let (Some(nat), Some(transport)) = (&mut self.nat_traversal, &self.transport) else {
1220            return Ok(());
1221        };
1222
1223        if nat.refresh().await? {
1224            info!("Reflexive address changed");
1225        }
1226
1227        for peer in &mut self.peers {
1228            if peer.connection_type == ConnectionType::Relayed && !peer.candidates.is_empty() {
1229                if let Ok(Some(upgraded)) = nat
1230                    .attempt_upgrade(transport, &peer.public_key, &peer.candidates)
1231                    .await
1232                {
1233                    peer.connection_type = upgraded;
1234                    info!(
1235                        peer = %peer.node_id,
1236                        connection = %upgraded,
1237                        "Upgraded relayed connection"
1238                    );
1239                }
1240            }
1241        }
1242
1243        Ok(())
1244    }
1245
1246    /// Get this node's NAT candidates for sharing with peers.
1247    ///
1248    /// Returns an empty vec if NAT traversal has not been initialized
1249    /// or no candidates were gathered.
1250    #[cfg(feature = "nat")]
1251    #[must_use]
1252    pub fn nat_candidates(&self) -> Vec<Candidate> {
1253        self.nat_traversal
1254            .as_ref()
1255            .map(|n| n.local_candidates().to_vec())
1256            .unwrap_or_default()
1257    }
1258}
1259
1260/// Get current Unix timestamp
1261fn current_timestamp() -> u64 {
1262    std::time::SystemTime::now()
1263        .duration_since(std::time::UNIX_EPOCH)
1264        .unwrap_or_default()
1265        .as_secs()
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270    use super::*;
1271    use std::net::Ipv4Addr;
1272
1273    #[test]
1274    fn test_bootstrap_config_allowed_ip_v4() {
1275        let config = BootstrapConfig {
1276            cidr: "10.200.0.0/16".to_string(),
1277            node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)),
1278            interface: DEFAULT_INTERFACE_NAME.to_string(),
1279            port: DEFAULT_WG_PORT,
1280            private_key: "test_private".to_string(),
1281            public_key: "test_public".to_string(),
1282            is_leader: true,
1283            created_at: 0,
1284            slice_cidr: None,
1285        };
1286
1287        assert_eq!(config.allowed_ip(), "10.200.0.1/32");
1288    }
1289
1290    #[test]
1291    fn test_bootstrap_config_allowed_ip_v6() {
1292        let config = BootstrapConfig {
1293            cidr: "fd00:200::/48".to_string(),
1294            node_ip: "fd00:200::1".parse::<IpAddr>().unwrap(),
1295            interface: DEFAULT_INTERFACE_NAME.to_string(),
1296            port: DEFAULT_WG_PORT,
1297            private_key: "test_private".to_string(),
1298            public_key: "test_public".to_string(),
1299            is_leader: true,
1300            created_at: 0,
1301            slice_cidr: None,
1302        };
1303
1304        assert_eq!(config.allowed_ip(), "fd00:200::1/128");
1305    }
1306
1307    #[test]
1308    fn test_peer_config_new_v4() {
1309        let peer = PeerConfig::new(
1310            "node-1".to_string(),
1311            "pubkey123".to_string(),
1312            "192.168.1.100:51820".to_string(),
1313            IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5)),
1314        );
1315
1316        assert_eq!(peer.node_id, "node-1");
1317        assert_eq!(peer.keepalive, Some(DEFAULT_KEEPALIVE_SECS));
1318        assert_eq!(peer.hostname, None);
1319    }
1320
1321    #[test]
1322    fn test_peer_config_new_v6() {
1323        let peer = PeerConfig::new(
1324            "node-1".to_string(),
1325            "pubkey123".to_string(),
1326            "[::1]:51820".to_string(),
1327            "fd00:200::5".parse::<IpAddr>().unwrap(),
1328        );
1329
1330        assert_eq!(peer.node_id, "node-1");
1331        assert_eq!(peer.keepalive, Some(DEFAULT_KEEPALIVE_SECS));
1332        assert_eq!(peer.hostname, None);
1333    }
1334
1335    #[test]
1336    fn test_peer_config_with_hostname() {
1337        let peer = PeerConfig::new(
1338            "node-1".to_string(),
1339            "pubkey123".to_string(),
1340            "192.168.1.100:51820".to_string(),
1341            IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5)),
1342        )
1343        .with_hostname("web-server");
1344
1345        assert_eq!(peer.hostname, Some("web-server".to_string()));
1346    }
1347
1348    #[test]
1349    fn test_peer_config_to_peer_info_v4() {
1350        let peer = PeerConfig::new(
1351            "node-1".to_string(),
1352            "pubkey123".to_string(),
1353            "192.168.1.100:51820".to_string(),
1354            IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5)),
1355        );
1356
1357        let peer_info = peer.to_peer_info().unwrap();
1358        assert_eq!(peer_info.public_key, "pubkey123");
1359        assert_eq!(peer_info.allowed_ips, "10.200.0.5/32");
1360    }
1361
1362    #[test]
1363    fn test_peer_config_to_peer_info_v6() {
1364        let peer = PeerConfig::new(
1365            "node-1".to_string(),
1366            "pubkey123".to_string(),
1367            "[::1]:51820".to_string(),
1368            "fd00:200::5".parse::<IpAddr>().unwrap(),
1369        );
1370
1371        let peer_info = peer.to_peer_info().unwrap();
1372        assert_eq!(peer_info.public_key, "pubkey123");
1373        assert_eq!(peer_info.allowed_ips, "fd00:200::5/128");
1374    }
1375
1376    #[test]
1377    fn test_bootstrap_state_serialization_v4() {
1378        let config = BootstrapConfig {
1379            cidr: "10.200.0.0/16".to_string(),
1380            node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)),
1381            interface: DEFAULT_INTERFACE_NAME.to_string(),
1382            port: DEFAULT_WG_PORT,
1383            private_key: "private".to_string(),
1384            public_key: "public".to_string(),
1385            is_leader: true,
1386            created_at: 1_234_567_890,
1387            slice_cidr: None,
1388        };
1389
1390        let state = BootstrapState {
1391            config,
1392            peers: vec![],
1393            allocator_state: None,
1394            slice_allocator_state: None,
1395        };
1396
1397        let json = serde_json::to_string_pretty(&state).unwrap();
1398        let deserialized: BootstrapState = serde_json::from_str(&json).unwrap();
1399
1400        assert_eq!(deserialized.config.cidr, "10.200.0.0/16");
1401        assert_eq!(deserialized.config.node_ip.to_string(), "10.200.0.1");
1402    }
1403
1404    #[test]
1405    fn test_bootstrap_state_serialization_v6() {
1406        let config = BootstrapConfig {
1407            cidr: "fd00:200::/48".to_string(),
1408            node_ip: "fd00:200::1".parse::<IpAddr>().unwrap(),
1409            interface: DEFAULT_INTERFACE_NAME.to_string(),
1410            port: DEFAULT_WG_PORT,
1411            private_key: "private".to_string(),
1412            public_key: "public".to_string(),
1413            is_leader: true,
1414            created_at: 1_234_567_890,
1415            slice_cidr: None,
1416        };
1417
1418        let state = BootstrapState {
1419            config,
1420            peers: vec![],
1421            allocator_state: None,
1422            slice_allocator_state: None,
1423        };
1424
1425        let json = serde_json::to_string_pretty(&state).unwrap();
1426        let deserialized: BootstrapState = serde_json::from_str(&json).unwrap();
1427
1428        assert_eq!(deserialized.config.cidr, "fd00:200::/48");
1429        assert_eq!(deserialized.config.node_ip.to_string(), "fd00:200::1");
1430    }
1431
1432    #[test]
1433    fn test_default_overlay_cidr_v6_constant() {
1434        // Verify the IPv6 CIDR constant is valid
1435        let net: ipnet::IpNet = DEFAULT_OVERLAY_CIDR_V6.parse().unwrap();
1436        assert!(matches!(net, ipnet::IpNet::V6(_)));
1437        assert_eq!(net.prefix_len(), 48);
1438    }
1439
1440    #[test]
1441    fn test_to_peer_info_uses_slice_when_set() {
1442        let peer = PeerConfig::new(
1443            "node-42".to_string(),
1444            "pubkey-xyz".to_string(),
1445            "192.168.1.100:51820".to_string(),
1446            IpAddr::V4(Ipv4Addr::new(10, 200, 42, 1)),
1447        )
1448        .with_slice_cidr("10.200.42.0/28".parse().unwrap());
1449
1450        let peer_info = peer.to_peer_info().unwrap();
1451        assert_eq!(peer_info.allowed_ips, "10.200.42.0/28");
1452    }
1453
1454    #[test]
1455    fn test_to_peer_info_falls_back_to_node_ip_when_no_slice() {
1456        let peer = PeerConfig::new(
1457            "node-5".to_string(),
1458            "pubkey-abc".to_string(),
1459            "192.168.1.100:51820".to_string(),
1460            "10.200.0.5".parse().unwrap(),
1461        );
1462
1463        let peer_info = peer.to_peer_info().unwrap();
1464        assert_eq!(peer_info.allowed_ips, "10.200.0.5/32");
1465    }
1466
1467    #[test]
1468    fn test_bootstrap_config_allowed_ip_prefers_slice() {
1469        let config = BootstrapConfig {
1470            cidr: "10.200.0.0/16".to_string(),
1471            node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 7, 1)),
1472            interface: DEFAULT_INTERFACE_NAME.to_string(),
1473            port: DEFAULT_WG_PORT,
1474            private_key: "private".to_string(),
1475            public_key: "public".to_string(),
1476            is_leader: false,
1477            created_at: 0,
1478            slice_cidr: Some("10.200.7.0/28".parse().unwrap()),
1479        };
1480
1481        assert_eq!(config.allowed_ip(), "10.200.7.0/28");
1482    }
1483
1484    #[test]
1485    fn test_bootstrap_config_allowed_ip_falls_back_to_node_ip() {
1486        let config = BootstrapConfig {
1487            cidr: "10.200.0.0/16".to_string(),
1488            node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 0, 9)),
1489            interface: DEFAULT_INTERFACE_NAME.to_string(),
1490            port: DEFAULT_WG_PORT,
1491            private_key: "private".to_string(),
1492            public_key: "public".to_string(),
1493            is_leader: false,
1494            created_at: 0,
1495            slice_cidr: None,
1496        };
1497
1498        assert_eq!(config.allowed_ip(), "10.200.0.9/32");
1499    }
1500}