1use 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#[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
30pub use zlayer_core::DEFAULT_WG_PORT;
32
33pub const DEFAULT_OVERLAY_CIDR: &str = "10.200.0.0/16";
35
36pub const DEFAULT_OVERLAY_CIDR_V6: &str = "fd00:200::/48";
41
42pub const DEFAULT_KEEPALIVE_SECS: u16 = 25;
44
45pub const DEFAULT_SLICE_PREFIX: u8 = 28;
49
50mod 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#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct BootstrapConfig {
86 pub cidr: String,
88
89 pub node_ip: IpAddr,
91
92 pub interface: String,
94
95 pub port: u16,
97
98 pub private_key: String,
100
101 pub public_key: String,
103
104 pub is_leader: bool,
106
107 pub created_at: u64,
109
110 #[serde(default, with = "option_ipnet_str")]
114 pub slice_cidr: Option<IpNet>,
115}
116
117impl BootstrapConfig {
118 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct PeerConfig {
140 pub node_id: String,
142
143 pub public_key: String,
145
146 pub endpoint: String,
148
149 pub overlay_ip: IpAddr,
151
152 #[serde(default)]
154 pub keepalive: Option<u16>,
155
156 #[serde(default)]
160 pub hostname: Option<String>,
161
162 #[serde(default)]
164 #[cfg(feature = "nat")]
165 pub candidates: Vec<Candidate>,
166
167 #[serde(default)]
169 #[cfg(feature = "nat")]
170 pub connection_type: ConnectionType,
171
172 #[serde(default, with = "option_ipnet_str")]
175 pub slice_cidr: Option<IpNet>,
176}
177
178impl PeerConfig {
179 #[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 #[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 #[must_use]
210 pub fn with_slice_cidr(mut self, cidr: IpNet) -> Self {
211 self.slice_cidr = Some(cidr);
212 self
213 }
214
215 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#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct BootstrapState {
251 pub config: BootstrapConfig,
253
254 pub peers: Vec<PeerConfig>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub allocator_state: Option<crate::allocator::IpAllocatorState>,
260
261 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub slice_allocator_state: Option<NodeSliceAllocatorSnapshot>,
264}
265
266pub struct OverlayBootstrap {
271 config: BootstrapConfig,
273
274 peers: Vec<PeerConfig>,
276
277 data_dir: PathBuf,
279
280 allocator: Option<IpAllocator>,
282
283 slice_allocator: Option<NodeSliceAllocator>,
288
289 dns_config: Option<DnsConfig>,
291
292 dns_handle: Option<DnsHandle>,
294
295 transport: Option<OverlayTransport>,
300
301 #[cfg(feature = "nat")]
303 nat_traversal: Option<NatTraversal>,
304
305 #[cfg(feature = "nat")]
307 relay_server: Option<RelayServer>,
308
309 #[cfg(feature = "nat")]
313 nat_config: Option<crate::nat::NatConfig>,
314}
315
316impl OverlayBootstrap {
317 pub async fn init_leader(cidr: &str, port: u16, data_dir: &Path) -> Result<Self> {
340 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 tokio::fs::create_dir_all(data_dir).await?;
350
351 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 let mut allocator = IpAllocator::new(cidr)?;
361 let _legacy_first = allocator.allocate_first()?;
362
363 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 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 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 bootstrap.save().await?;
426
427 Ok(bootstrap)
428 }
429
430 #[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 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 tokio::fs::create_dir_all(data_dir).await?;
471
472 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 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 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, #[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, 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 bootstrap.save().await?;
531
532 Ok(bootstrap)
533 }
534
535 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_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 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 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 pub fn with_dns_default(self, zone: &str) -> Result<Self> {
646 self.with_dns(zone, DEFAULT_DNS_PORT)
647 }
648
649 #[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 #[must_use]
664 pub fn dns_handle(&self) -> Option<&DnsHandle> {
665 self.dns_handle.as_ref()
666 }
667
668 #[must_use]
670 pub fn dns_enabled(&self) -> bool {
671 self.dns_config.is_some()
672 }
673
674 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 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 let mut transport = OverlayTransport::new(overlay_config, self.config.interface.clone());
712
713 transport
715 .create_interface()
716 .await
717 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
718
719 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 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 transport
746 .configure(&peer_infos)
747 .await
748 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
749
750 self.transport = Some(transport);
753
754 #[cfg(feature = "nat")]
756 self.start_nat_traversal(nat_config).await;
757
758 self.start_dns().await?;
760
761 info!("Overlay network started successfully");
762 Ok(())
763 }
764
765 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 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 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 for peer in &self.peers {
798 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 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 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 #[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 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 #[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 pub fn reconcile_existing_peers(&mut self) -> Result<()> {
911 let Some(ref mut allocator) = self.slice_allocator else {
912 return Ok(());
913 };
914 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 pub async fn add_peer(&mut self, mut peer: PeerConfig) -> Result<IpAddr> {
941 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 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 }
960
961 if let Ok(peer_info) = peer.to_peer_info() {
963 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 if let Some(ref dns_handle) = self.dns_handle {
998 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 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 #[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 self.peers.push(peer);
1045
1046 self.save().await?;
1048
1049 info!(peer_ip = %overlay_ip, "Added peer to overlay");
1050 Ok(overlay_ip)
1051 }
1052
1053 pub async fn remove_peer(&mut self, public_key: &str) -> Result<()> {
1059 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 let peer_overlay_ip = peer.overlay_ip;
1070 let peer_custom_hostname = peer.hostname.clone();
1071
1072 if let Some(ref mut allocator) = self.allocator {
1074 allocator.release(peer_overlay_ip);
1075 }
1076
1077 if let Some(ref dns_handle) = self.dns_handle {
1079 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 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 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 self.peers.remove(peer_idx);
1130
1131 self.save().await?;
1133
1134 info!(public_key = public_key, "Removed peer from overlay");
1135 Ok(())
1136 }
1137
1138 #[must_use]
1140 pub fn public_key(&self) -> &str {
1141 &self.config.public_key
1142 }
1143
1144 #[must_use]
1146 pub fn node_ip(&self) -> IpAddr {
1147 self.config.node_ip
1148 }
1149
1150 #[must_use]
1152 pub fn cidr(&self) -> &str {
1153 &self.config.cidr
1154 }
1155
1156 #[must_use]
1158 pub fn interface(&self) -> &str {
1159 &self.config.interface
1160 }
1161
1162 #[must_use]
1164 pub fn port(&self) -> u16 {
1165 self.config.port
1166 }
1167
1168 #[must_use]
1170 pub fn is_leader(&self) -> bool {
1171 self.config.is_leader
1172 }
1173
1174 #[must_use]
1176 pub fn peers(&self) -> &[PeerConfig] {
1177 &self.peers
1178 }
1179
1180 #[must_use]
1182 pub fn config(&self) -> &BootstrapConfig {
1183 &self.config
1184 }
1185
1186 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 #[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 #[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 #[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
1263fn 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 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}