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
310impl OverlayBootstrap {
311 pub async fn init_leader(cidr: &str, port: u16, data_dir: &Path) -> Result<Self> {
334 let config_path = data_dir.join("overlay_bootstrap.json");
336 if config_path.exists() {
337 return Err(OverlayError::AlreadyInitialized(
338 config_path.display().to_string(),
339 ));
340 }
341
342 tokio::fs::create_dir_all(data_dir).await?;
344
345 info!("Generating overlay keypair for leader");
347 let (private_key, public_key) = OverlayTransport::generate_keys()
348 .await
349 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
350
351 let mut allocator = IpAllocator::new(cidr)?;
355 let _legacy_first = allocator.allocate_first()?;
356
357 let cluster_cidr: IpNet = cidr
361 .parse()
362 .map_err(|e: ipnet::AddrParseError| OverlayError::InvalidCidr(e.to_string()))?;
363 let mut slice_allocator = NodeSliceAllocator::new(cluster_cidr, DEFAULT_SLICE_PREFIX)?;
364 let leader_slice = slice_allocator.assign("leader")?;
365 let node_ip = leader_slice.hosts().next().unwrap_or_else(|| {
366 match leader_slice.network() {
370 IpAddr::V4(v4) => {
371 let bits = u32::from(v4).saturating_add(1);
372 IpAddr::V4(std::net::Ipv4Addr::from(bits))
373 }
374 IpAddr::V6(v6) => {
375 let bits = u128::from(v6).saturating_add(1);
376 IpAddr::V6(std::net::Ipv6Addr::from(bits))
377 }
378 }
379 });
380
381 info!(
382 node_ip = %node_ip,
383 cidr = cidr,
384 slice = %leader_slice,
385 "Allocated leader IP from slice"
386 );
387
388 let config = BootstrapConfig {
390 cidr: cidr.to_string(),
391 node_ip,
392 interface: DEFAULT_INTERFACE_NAME.to_string(),
393 port,
394 private_key,
395 public_key,
396 is_leader: true,
397 created_at: current_timestamp(),
398 slice_cidr: Some(leader_slice),
399 };
400
401 let bootstrap = Self {
402 config,
403 peers: Vec::new(),
404 data_dir: data_dir.to_path_buf(),
405 allocator: Some(allocator),
406 slice_allocator: Some(slice_allocator),
407 dns_config: None,
408 dns_handle: None,
409 transport: None,
410 #[cfg(feature = "nat")]
411 nat_traversal: None,
412 #[cfg(feature = "nat")]
413 relay_server: None,
414 };
415
416 bootstrap.save().await?;
418
419 Ok(bootstrap)
420 }
421
422 #[allow(clippy::too_many_arguments)]
443 pub async fn join(
444 leader_cidr: &str,
445 leader_endpoint: &str,
446 leader_public_key: &str,
447 leader_overlay_ip: IpAddr,
448 allocated_ip: IpAddr,
449 port: u16,
450 slice_cidr: Option<IpNet>,
451 data_dir: &Path,
452 ) -> Result<Self> {
453 let config_path = data_dir.join("overlay_bootstrap.json");
455 if config_path.exists() {
456 return Err(OverlayError::AlreadyInitialized(
457 config_path.display().to_string(),
458 ));
459 }
460
461 tokio::fs::create_dir_all(data_dir).await?;
463
464 info!("Generating overlay keypair for joining node");
466 let (private_key, public_key) = OverlayTransport::generate_keys()
467 .await
468 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
469
470 let config = BootstrapConfig {
472 cidr: leader_cidr.to_string(),
473 node_ip: allocated_ip,
474 interface: DEFAULT_INTERFACE_NAME.to_string(),
475 port,
476 private_key,
477 public_key,
478 is_leader: false,
479 created_at: current_timestamp(),
480 slice_cidr,
481 };
482
483 let leader_peer = PeerConfig {
485 node_id: "leader".to_string(),
486 public_key: leader_public_key.to_string(),
487 endpoint: leader_endpoint.to_string(),
488 overlay_ip: leader_overlay_ip,
489 keepalive: Some(DEFAULT_KEEPALIVE_SECS),
490 hostname: None, #[cfg(feature = "nat")]
492 candidates: Vec::new(),
493 #[cfg(feature = "nat")]
494 connection_type: ConnectionType::default(),
495 slice_cidr: None,
496 };
497
498 info!(
499 leader_endpoint = leader_endpoint,
500 overlay_ip = %allocated_ip,
501 "Configured leader as peer"
502 );
503
504 let bootstrap = Self {
505 config,
506 peers: vec![leader_peer],
507 data_dir: data_dir.to_path_buf(),
508 allocator: None, slice_allocator: None,
510 dns_config: None,
511 dns_handle: None,
512 transport: None,
513 #[cfg(feature = "nat")]
514 nat_traversal: None,
515 #[cfg(feature = "nat")]
516 relay_server: None,
517 };
518
519 bootstrap.save().await?;
521
522 Ok(bootstrap)
523 }
524
525 pub async fn load(data_dir: &Path) -> Result<Self> {
531 let config_path = data_dir.join("overlay_bootstrap.json");
532
533 if !config_path.exists() {
534 return Err(OverlayError::NotInitialized);
535 }
536
537 let contents = tokio::fs::read_to_string(&config_path).await?;
538 let state: BootstrapState = serde_json::from_str(&contents)?;
539
540 let allocator = if let Some(alloc_state) = state.allocator_state {
541 Some(IpAllocator::from_state(alloc_state)?)
542 } else {
543 None
544 };
545
546 let slice_allocator = if let Some(snapshot) = state.slice_allocator_state {
547 Some(NodeSliceAllocator::restore(snapshot)?)
548 } else {
549 None
550 };
551
552 Ok(Self {
553 config: state.config,
554 peers: state.peers,
555 data_dir: data_dir.to_path_buf(),
556 allocator,
557 slice_allocator,
558 dns_config: None, dns_handle: None,
560 transport: None,
561 #[cfg(feature = "nat")]
562 nat_traversal: None,
563 #[cfg(feature = "nat")]
564 relay_server: None,
565 })
566 }
567
568 pub async fn save(&self) -> Result<()> {
574 let config_path = self.data_dir.join("overlay_bootstrap.json");
575
576 let state = BootstrapState {
577 config: self.config.clone(),
578 peers: self.peers.clone(),
579 allocator_state: self
580 .allocator
581 .as_ref()
582 .map(super::allocator::IpAllocator::to_state),
583 slice_allocator_state: self
584 .slice_allocator
585 .as_ref()
586 .map(NodeSliceAllocator::snapshot),
587 };
588
589 let contents = serde_json::to_string_pretty(&state)?;
590 tokio::fs::write(&config_path, contents).await?;
591
592 debug!(path = %config_path.display(), "Saved bootstrap state");
593 Ok(())
594 }
595
596 pub fn with_dns(mut self, zone: &str, port: u16) -> Result<Self> {
620 self.dns_config = Some(DnsConfig {
621 zone: zone.to_string(),
622 port,
623 bind_addr: self.config.node_ip,
624 });
625 Ok(self)
626 }
627
628 pub fn with_dns_default(self, zone: &str) -> Result<Self> {
634 self.with_dns(zone, DEFAULT_DNS_PORT)
635 }
636
637 #[must_use]
641 pub fn dns_handle(&self) -> Option<&DnsHandle> {
642 self.dns_handle.as_ref()
643 }
644
645 #[must_use]
647 pub fn dns_enabled(&self) -> bool {
648 self.dns_config.is_some()
649 }
650
651 pub async fn start(&mut self) -> Result<()> {
660 info!(
661 interface = %self.config.interface,
662 overlay_ip = %self.config.node_ip,
663 port = self.config.port,
664 dns_enabled = self.dns_config.is_some(),
665 "Starting overlay network"
666 );
667
668 let overlay_config = crate::config::OverlayConfig {
670 local_endpoint: SocketAddr::new(
671 std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
672 self.config.port,
673 ),
674 private_key: self.config.private_key.clone(),
675 public_key: self.config.public_key.clone(),
676 overlay_cidr: self.config.allowed_ip(),
677 cluster_cidr: Some(self.config.cidr.clone()),
678 peer_discovery_interval: Duration::from_secs(30),
679 #[cfg(feature = "nat")]
680 nat: crate::nat::NatConfig::default(),
681 };
682
683 #[cfg(feature = "nat")]
684 let nat_config = overlay_config.nat.clone();
685
686 let mut transport = OverlayTransport::new(overlay_config, self.config.interface.clone());
688
689 transport
691 .create_interface()
692 .await
693 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
694
695 let actual_name = transport.interface_name().to_string();
698 if actual_name != self.config.interface {
699 info!(
700 requested = %self.config.interface,
701 actual = %actual_name,
702 "Interface name resolved by kernel"
703 );
704 self.config.interface = actual_name;
705 }
706
707 let peer_infos: Vec<PeerInfo> = self
709 .peers
710 .iter()
711 .filter_map(|p| match p.to_peer_info() {
712 Ok(info) => Some(info),
713 Err(e) => {
714 warn!(peer = %p.node_id, error = %e, "Failed to parse peer info");
715 None
716 }
717 })
718 .collect();
719
720 transport
722 .configure(&peer_infos)
723 .await
724 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
725
726 self.transport = Some(transport);
729
730 #[cfg(feature = "nat")]
732 self.start_nat_traversal(nat_config).await;
733
734 self.start_dns().await?;
736
737 info!("Overlay network started successfully");
738 Ok(())
739 }
740
741 async fn start_dns(&mut self) -> Result<()> {
743 let Some(dns_config) = &self.dns_config else {
744 return Ok(());
745 };
746
747 info!(
748 zone = %dns_config.zone,
749 port = dns_config.port,
750 "Starting DNS server for overlay"
751 );
752
753 let dns_server =
754 DnsServer::from_config(dns_config).map_err(|e| OverlayError::Dns(e.to_string()))?;
755
756 let self_hostname = peer_hostname(self.config.node_ip);
758 dns_server
759 .add_record(&self_hostname, self.config.node_ip)
760 .await
761 .map_err(|e| OverlayError::Dns(e.to_string()))?;
762
763 if self.config.is_leader {
765 dns_server
766 .add_record("leader", self.config.node_ip)
767 .await
768 .map_err(|e| OverlayError::Dns(e.to_string()))?;
769 debug!(ip = %self.config.node_ip, "Registered leader.{}", dns_config.zone);
770 }
771
772 for peer in &self.peers {
774 let hostname = peer_hostname(peer.overlay_ip);
776 dns_server
777 .add_record(&hostname, peer.overlay_ip)
778 .await
779 .map_err(|e| OverlayError::Dns(e.to_string()))?;
780
781 if let Some(custom) = &peer.hostname {
783 dns_server
784 .add_record(custom, peer.overlay_ip)
785 .await
786 .map_err(|e| OverlayError::Dns(e.to_string()))?;
787 debug!(
788 hostname = custom,
789 ip = %peer.overlay_ip,
790 "Registered custom hostname"
791 );
792 }
793 }
794
795 let handle = dns_server
797 .start()
798 .await
799 .map_err(|e| OverlayError::Dns(e.to_string()))?;
800 self.dns_handle = Some(handle);
801
802 info!("DNS server started successfully");
803 Ok(())
804 }
805
806 #[cfg(feature = "nat")]
808 async fn start_nat_traversal(&mut self, nat_config: crate::nat::NatConfig) {
809 if !nat_config.enabled {
810 return;
811 }
812
813 if let Some(ref relay_config) = nat_config.relay_server {
815 let relay = RelayServer::new(relay_config, &self.config.private_key);
816 match relay.start().await {
817 Ok(()) => {
818 info!("Built-in relay server started");
819 self.relay_server = Some(relay);
820 }
821 Err(e) => {
822 warn!(error = %e, "Failed to start relay server");
823 }
824 }
825 }
826
827 let mut nat = NatTraversal::new(nat_config, self.config.port);
828 match nat.gather_candidates().await {
829 Ok(candidates) => {
830 info!(count = candidates.len(), "Gathered NAT candidates");
831 if let Some(ref transport) = self.transport {
832 for peer in &mut self.peers {
833 if !peer.candidates.is_empty() {
834 match nat
835 .connect_to_peer(transport, &peer.public_key, &peer.candidates)
836 .await
837 {
838 Ok(ct) => {
839 peer.connection_type = ct;
840 info!(
841 peer = %peer.node_id,
842 connection = %ct,
843 "NAT traversal succeeded"
844 );
845 }
846 Err(e) => warn!(
847 peer = %peer.node_id,
848 error = %e,
849 "NAT traversal failed"
850 ),
851 }
852 }
853 }
854 }
855 self.nat_traversal = Some(nat);
856 }
857 Err(e) => warn!(error = %e, "NAT candidate gathering failed"),
858 }
859 }
860
861 #[allow(clippy::unused_async)]
867 pub async fn stop(&mut self) -> Result<()> {
868 info!(interface = %self.config.interface, "Stopping overlay network");
869
870 if let Some(mut transport) = self.transport.take() {
871 transport.shutdown();
872 }
873
874 Ok(())
875 }
876
877 pub fn reconcile_existing_peers(&mut self) -> Result<()> {
887 let Some(ref mut allocator) = self.slice_allocator else {
888 return Ok(());
889 };
890 let mut assigned: Vec<(String, String)> = Vec::new();
892 for peer in &self.peers {
893 if let Some(slice) = peer.slice_cidr {
894 assigned.push((peer.node_id.clone(), slice.to_string()));
895 }
896 }
897 if assigned.is_empty() {
898 return Ok(());
899 }
900 let snapshot = NodeSliceAllocatorSnapshot {
901 cluster_cidr: allocator.cluster_cidr().to_string(),
902 slice_prefix: allocator.slice_prefix(),
903 assigned,
904 };
905 *allocator = NodeSliceAllocator::restore(snapshot)?;
906 Ok(())
907 }
908
909 pub async fn add_peer(&mut self, mut peer: PeerConfig) -> Result<IpAddr> {
917 let overlay_ip = if let Some(ref mut allocator) = self.allocator {
919 let ip = allocator.allocate().ok_or(OverlayError::NoAvailableIps)?;
920 peer.overlay_ip = ip;
921 ip
922 } else {
923 peer.overlay_ip
924 };
925
926 if let Some(ref mut slice_allocator) = self.slice_allocator {
930 let slice = slice_allocator.assign(&peer.node_id)?;
931 peer.slice_cidr = Some(slice);
932 }
936
937 if let Ok(peer_info) = peer.to_peer_info() {
939 let transport_ref: Option<&OverlayTransport> = self.transport.as_ref();
942
943 let result = if let Some(t) = transport_ref {
944 t.add_peer(&peer_info).await
945 } else {
946 let overlay_config = crate::config::OverlayConfig {
947 local_endpoint: SocketAddr::new(
948 std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
949 self.config.port,
950 ),
951 private_key: self.config.private_key.clone(),
952 public_key: self.config.public_key.clone(),
953 overlay_cidr: self.config.allowed_ip(),
954 cluster_cidr: Some(self.config.cidr.clone()),
955 peer_discovery_interval: Duration::from_secs(30),
956 #[cfg(feature = "nat")]
957 nat: crate::nat::NatConfig::default(),
958 };
959 let tmp = OverlayTransport::new(overlay_config, self.config.interface.clone());
960 tmp.add_peer(&peer_info).await
961 };
962
963 match result {
964 Ok(()) => debug!(peer = %peer.node_id, "Added peer to overlay"),
965 Err(e) => {
966 warn!(peer = %peer.node_id, error = %e, "Failed to add peer to overlay (interface may not be up)");
967 }
968 }
969 }
970
971 if let Some(ref dns_handle) = self.dns_handle {
973 let hostname = peer_hostname(overlay_ip);
975 dns_handle
976 .add_record(&hostname, overlay_ip)
977 .await
978 .map_err(|e| OverlayError::Dns(e.to_string()))?;
979 debug!(hostname = %hostname, ip = %overlay_ip, "Registered peer in DNS");
980
981 if let Some(ref custom) = peer.hostname {
983 dns_handle
984 .add_record(custom, overlay_ip)
985 .await
986 .map_err(|e| OverlayError::Dns(e.to_string()))?;
987 debug!(hostname = %custom, ip = %overlay_ip, "Registered custom hostname in DNS");
988 }
989 }
990
991 #[cfg(feature = "nat")]
993 {
994 if let (Some(ref nat), Some(ref transport)) = (&self.nat_traversal, &self.transport) {
995 if !peer.candidates.is_empty() {
996 match nat
997 .connect_to_peer(transport, &peer.public_key, &peer.candidates)
998 .await
999 {
1000 Ok(ct) => {
1001 peer.connection_type = ct;
1002 info!(
1003 peer = %peer.node_id,
1004 connection = %ct,
1005 "NAT traversal for new peer"
1006 );
1007 }
1008 Err(e) => warn!(
1009 peer = %peer.node_id,
1010 error = %e,
1011 "NAT failed for new peer"
1012 ),
1013 }
1014 }
1015 }
1016 }
1017
1018 self.peers.push(peer);
1020
1021 self.save().await?;
1023
1024 info!(peer_ip = %overlay_ip, "Added peer to overlay");
1025 Ok(overlay_ip)
1026 }
1027
1028 pub async fn remove_peer(&mut self, public_key: &str) -> Result<()> {
1034 let peer_idx = self
1036 .peers
1037 .iter()
1038 .position(|p| p.public_key == public_key)
1039 .ok_or_else(|| OverlayError::PeerNotFound(public_key.to_string()))?;
1040
1041 let peer = &self.peers[peer_idx];
1042
1043 let peer_overlay_ip = peer.overlay_ip;
1045 let peer_custom_hostname = peer.hostname.clone();
1046
1047 if let Some(ref mut allocator) = self.allocator {
1049 allocator.release(peer_overlay_ip);
1050 }
1051
1052 if let Some(ref dns_handle) = self.dns_handle {
1054 let hostname = peer_hostname(peer_overlay_ip);
1056 dns_handle
1057 .remove_record(&hostname)
1058 .await
1059 .map_err(|e| OverlayError::Dns(e.to_string()))?;
1060 debug!(hostname = %hostname, "Removed peer from DNS");
1061
1062 if let Some(ref custom) = peer_custom_hostname {
1064 dns_handle
1065 .remove_record(custom)
1066 .await
1067 .map_err(|e| OverlayError::Dns(e.to_string()))?;
1068 debug!(hostname = %custom, "Removed custom hostname from DNS");
1069 }
1070 }
1071
1072 let transport_ref: Option<&OverlayTransport> = self.transport.as_ref();
1074
1075 let result = if let Some(t) = transport_ref {
1076 t.remove_peer(public_key).await
1077 } else {
1078 let overlay_config = crate::config::OverlayConfig {
1079 local_endpoint: SocketAddr::new(
1080 std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
1081 self.config.port,
1082 ),
1083 private_key: self.config.private_key.clone(),
1084 public_key: self.config.public_key.clone(),
1085 overlay_cidr: self.config.allowed_ip(),
1086 cluster_cidr: Some(self.config.cidr.clone()),
1087 peer_discovery_interval: Duration::from_secs(30),
1088 #[cfg(feature = "nat")]
1089 nat: crate::nat::NatConfig::default(),
1090 };
1091 let tmp = OverlayTransport::new(overlay_config, self.config.interface.clone());
1092 tmp.remove_peer(public_key).await
1093 };
1094
1095 match result {
1096 Ok(()) => debug!(public_key = public_key, "Removed peer from overlay"),
1097 Err(e) => {
1098 warn!(public_key = public_key, error = %e, "Failed to remove peer from overlay");
1099 }
1100 }
1101
1102 self.peers.remove(peer_idx);
1104
1105 self.save().await?;
1107
1108 info!(public_key = public_key, "Removed peer from overlay");
1109 Ok(())
1110 }
1111
1112 #[must_use]
1114 pub fn public_key(&self) -> &str {
1115 &self.config.public_key
1116 }
1117
1118 #[must_use]
1120 pub fn node_ip(&self) -> IpAddr {
1121 self.config.node_ip
1122 }
1123
1124 #[must_use]
1126 pub fn cidr(&self) -> &str {
1127 &self.config.cidr
1128 }
1129
1130 #[must_use]
1132 pub fn interface(&self) -> &str {
1133 &self.config.interface
1134 }
1135
1136 #[must_use]
1138 pub fn port(&self) -> u16 {
1139 self.config.port
1140 }
1141
1142 #[must_use]
1144 pub fn is_leader(&self) -> bool {
1145 self.config.is_leader
1146 }
1147
1148 #[must_use]
1150 pub fn peers(&self) -> &[PeerConfig] {
1151 &self.peers
1152 }
1153
1154 #[must_use]
1156 pub fn config(&self) -> &BootstrapConfig {
1157 &self.config
1158 }
1159
1160 pub fn allocate_peer_ip(&mut self) -> Result<IpAddr> {
1168 let allocator = self
1169 .allocator
1170 .as_mut()
1171 .ok_or(OverlayError::Config("Not a leader node".to_string()))?;
1172
1173 allocator.allocate().ok_or(OverlayError::NoAvailableIps)
1174 }
1175
1176 #[must_use]
1178 #[allow(clippy::cast_possible_truncation)]
1179 pub fn allocation_stats(&self) -> Option<(u32, u32)> {
1180 self.allocator
1181 .as_ref()
1182 .map(|a| (a.allocated_count() as u32, a.total_hosts()))
1183 }
1184
1185 #[cfg(feature = "nat")]
1195 pub async fn nat_maintenance_tick(&mut self) -> Result<()> {
1196 let (Some(nat), Some(transport)) = (&mut self.nat_traversal, &self.transport) else {
1197 return Ok(());
1198 };
1199
1200 if nat.refresh().await? {
1201 info!("Reflexive address changed");
1202 }
1203
1204 for peer in &mut self.peers {
1205 if peer.connection_type == ConnectionType::Relayed && !peer.candidates.is_empty() {
1206 if let Ok(Some(upgraded)) = nat
1207 .attempt_upgrade(transport, &peer.public_key, &peer.candidates)
1208 .await
1209 {
1210 peer.connection_type = upgraded;
1211 info!(
1212 peer = %peer.node_id,
1213 connection = %upgraded,
1214 "Upgraded relayed connection"
1215 );
1216 }
1217 }
1218 }
1219
1220 Ok(())
1221 }
1222
1223 #[cfg(feature = "nat")]
1228 #[must_use]
1229 pub fn nat_candidates(&self) -> Vec<Candidate> {
1230 self.nat_traversal
1231 .as_ref()
1232 .map(|n| n.local_candidates().to_vec())
1233 .unwrap_or_default()
1234 }
1235}
1236
1237fn current_timestamp() -> u64 {
1239 std::time::SystemTime::now()
1240 .duration_since(std::time::UNIX_EPOCH)
1241 .unwrap_or_default()
1242 .as_secs()
1243}
1244
1245#[cfg(test)]
1246mod tests {
1247 use super::*;
1248 use std::net::Ipv4Addr;
1249
1250 #[test]
1251 fn test_bootstrap_config_allowed_ip_v4() {
1252 let config = BootstrapConfig {
1253 cidr: "10.200.0.0/16".to_string(),
1254 node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)),
1255 interface: DEFAULT_INTERFACE_NAME.to_string(),
1256 port: DEFAULT_WG_PORT,
1257 private_key: "test_private".to_string(),
1258 public_key: "test_public".to_string(),
1259 is_leader: true,
1260 created_at: 0,
1261 slice_cidr: None,
1262 };
1263
1264 assert_eq!(config.allowed_ip(), "10.200.0.1/32");
1265 }
1266
1267 #[test]
1268 fn test_bootstrap_config_allowed_ip_v6() {
1269 let config = BootstrapConfig {
1270 cidr: "fd00:200::/48".to_string(),
1271 node_ip: "fd00:200::1".parse::<IpAddr>().unwrap(),
1272 interface: DEFAULT_INTERFACE_NAME.to_string(),
1273 port: DEFAULT_WG_PORT,
1274 private_key: "test_private".to_string(),
1275 public_key: "test_public".to_string(),
1276 is_leader: true,
1277 created_at: 0,
1278 slice_cidr: None,
1279 };
1280
1281 assert_eq!(config.allowed_ip(), "fd00:200::1/128");
1282 }
1283
1284 #[test]
1285 fn test_peer_config_new_v4() {
1286 let peer = PeerConfig::new(
1287 "node-1".to_string(),
1288 "pubkey123".to_string(),
1289 "192.168.1.100:51820".to_string(),
1290 IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5)),
1291 );
1292
1293 assert_eq!(peer.node_id, "node-1");
1294 assert_eq!(peer.keepalive, Some(DEFAULT_KEEPALIVE_SECS));
1295 assert_eq!(peer.hostname, None);
1296 }
1297
1298 #[test]
1299 fn test_peer_config_new_v6() {
1300 let peer = PeerConfig::new(
1301 "node-1".to_string(),
1302 "pubkey123".to_string(),
1303 "[::1]:51820".to_string(),
1304 "fd00:200::5".parse::<IpAddr>().unwrap(),
1305 );
1306
1307 assert_eq!(peer.node_id, "node-1");
1308 assert_eq!(peer.keepalive, Some(DEFAULT_KEEPALIVE_SECS));
1309 assert_eq!(peer.hostname, None);
1310 }
1311
1312 #[test]
1313 fn test_peer_config_with_hostname() {
1314 let peer = PeerConfig::new(
1315 "node-1".to_string(),
1316 "pubkey123".to_string(),
1317 "192.168.1.100:51820".to_string(),
1318 IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5)),
1319 )
1320 .with_hostname("web-server");
1321
1322 assert_eq!(peer.hostname, Some("web-server".to_string()));
1323 }
1324
1325 #[test]
1326 fn test_peer_config_to_peer_info_v4() {
1327 let peer = PeerConfig::new(
1328 "node-1".to_string(),
1329 "pubkey123".to_string(),
1330 "192.168.1.100:51820".to_string(),
1331 IpAddr::V4(Ipv4Addr::new(10, 200, 0, 5)),
1332 );
1333
1334 let peer_info = peer.to_peer_info().unwrap();
1335 assert_eq!(peer_info.public_key, "pubkey123");
1336 assert_eq!(peer_info.allowed_ips, "10.200.0.5/32");
1337 }
1338
1339 #[test]
1340 fn test_peer_config_to_peer_info_v6() {
1341 let peer = PeerConfig::new(
1342 "node-1".to_string(),
1343 "pubkey123".to_string(),
1344 "[::1]:51820".to_string(),
1345 "fd00:200::5".parse::<IpAddr>().unwrap(),
1346 );
1347
1348 let peer_info = peer.to_peer_info().unwrap();
1349 assert_eq!(peer_info.public_key, "pubkey123");
1350 assert_eq!(peer_info.allowed_ips, "fd00:200::5/128");
1351 }
1352
1353 #[test]
1354 fn test_bootstrap_state_serialization_v4() {
1355 let config = BootstrapConfig {
1356 cidr: "10.200.0.0/16".to_string(),
1357 node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 0, 1)),
1358 interface: DEFAULT_INTERFACE_NAME.to_string(),
1359 port: DEFAULT_WG_PORT,
1360 private_key: "private".to_string(),
1361 public_key: "public".to_string(),
1362 is_leader: true,
1363 created_at: 1_234_567_890,
1364 slice_cidr: None,
1365 };
1366
1367 let state = BootstrapState {
1368 config,
1369 peers: vec![],
1370 allocator_state: None,
1371 slice_allocator_state: None,
1372 };
1373
1374 let json = serde_json::to_string_pretty(&state).unwrap();
1375 let deserialized: BootstrapState = serde_json::from_str(&json).unwrap();
1376
1377 assert_eq!(deserialized.config.cidr, "10.200.0.0/16");
1378 assert_eq!(deserialized.config.node_ip.to_string(), "10.200.0.1");
1379 }
1380
1381 #[test]
1382 fn test_bootstrap_state_serialization_v6() {
1383 let config = BootstrapConfig {
1384 cidr: "fd00:200::/48".to_string(),
1385 node_ip: "fd00:200::1".parse::<IpAddr>().unwrap(),
1386 interface: DEFAULT_INTERFACE_NAME.to_string(),
1387 port: DEFAULT_WG_PORT,
1388 private_key: "private".to_string(),
1389 public_key: "public".to_string(),
1390 is_leader: true,
1391 created_at: 1_234_567_890,
1392 slice_cidr: None,
1393 };
1394
1395 let state = BootstrapState {
1396 config,
1397 peers: vec![],
1398 allocator_state: None,
1399 slice_allocator_state: None,
1400 };
1401
1402 let json = serde_json::to_string_pretty(&state).unwrap();
1403 let deserialized: BootstrapState = serde_json::from_str(&json).unwrap();
1404
1405 assert_eq!(deserialized.config.cidr, "fd00:200::/48");
1406 assert_eq!(deserialized.config.node_ip.to_string(), "fd00:200::1");
1407 }
1408
1409 #[test]
1410 fn test_default_overlay_cidr_v6_constant() {
1411 let net: ipnet::IpNet = DEFAULT_OVERLAY_CIDR_V6.parse().unwrap();
1413 assert!(matches!(net, ipnet::IpNet::V6(_)));
1414 assert_eq!(net.prefix_len(), 48);
1415 }
1416
1417 #[test]
1418 fn test_to_peer_info_uses_slice_when_set() {
1419 let peer = PeerConfig::new(
1420 "node-42".to_string(),
1421 "pubkey-xyz".to_string(),
1422 "192.168.1.100:51820".to_string(),
1423 IpAddr::V4(Ipv4Addr::new(10, 200, 42, 1)),
1424 )
1425 .with_slice_cidr("10.200.42.0/28".parse().unwrap());
1426
1427 let peer_info = peer.to_peer_info().unwrap();
1428 assert_eq!(peer_info.allowed_ips, "10.200.42.0/28");
1429 }
1430
1431 #[test]
1432 fn test_to_peer_info_falls_back_to_node_ip_when_no_slice() {
1433 let peer = PeerConfig::new(
1434 "node-5".to_string(),
1435 "pubkey-abc".to_string(),
1436 "192.168.1.100:51820".to_string(),
1437 "10.200.0.5".parse().unwrap(),
1438 );
1439
1440 let peer_info = peer.to_peer_info().unwrap();
1441 assert_eq!(peer_info.allowed_ips, "10.200.0.5/32");
1442 }
1443
1444 #[test]
1445 fn test_bootstrap_config_allowed_ip_prefers_slice() {
1446 let config = BootstrapConfig {
1447 cidr: "10.200.0.0/16".to_string(),
1448 node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 7, 1)),
1449 interface: DEFAULT_INTERFACE_NAME.to_string(),
1450 port: DEFAULT_WG_PORT,
1451 private_key: "private".to_string(),
1452 public_key: "public".to_string(),
1453 is_leader: false,
1454 created_at: 0,
1455 slice_cidr: Some("10.200.7.0/28".parse().unwrap()),
1456 };
1457
1458 assert_eq!(config.allowed_ip(), "10.200.7.0/28");
1459 }
1460
1461 #[test]
1462 fn test_bootstrap_config_allowed_ip_falls_back_to_node_ip() {
1463 let config = BootstrapConfig {
1464 cidr: "10.200.0.0/16".to_string(),
1465 node_ip: IpAddr::V4(Ipv4Addr::new(10, 200, 0, 9)),
1466 interface: DEFAULT_INTERFACE_NAME.to_string(),
1467 port: DEFAULT_WG_PORT,
1468 private_key: "private".to_string(),
1469 public_key: "public".to_string(),
1470 is_leader: false,
1471 created_at: 0,
1472 slice_cidr: None,
1473 };
1474
1475 assert_eq!(config.allowed_ip(), "10.200.0.9/32");
1476 }
1477}