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 };
705
706 #[cfg(feature = "nat")]
707 let nat_config = overlay_config.nat.clone();
708
709 let mut transport = OverlayTransport::new(overlay_config, self.config.interface.clone());
711
712 transport
714 .create_interface()
715 .await
716 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
717
718 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 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 transport
745 .configure(&peer_infos)
746 .await
747 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
748
749 self.transport = Some(transport);
752
753 #[cfg(feature = "nat")]
755 self.start_nat_traversal(nat_config).await;
756
757 self.start_dns().await?;
759
760 info!("Overlay network started successfully");
761 Ok(())
762 }
763
764 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 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 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 for peer in &self.peers {
797 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 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 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 #[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 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 #[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 pub fn reconcile_existing_peers(&mut self) -> Result<()> {
910 let Some(ref mut allocator) = self.slice_allocator else {
911 return Ok(());
912 };
913 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 pub async fn add_peer(&mut self, mut peer: PeerConfig) -> Result<IpAddr> {
940 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 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 }
959
960 if let Ok(peer_info) = peer.to_peer_info() {
962 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 if let Some(ref dns_handle) = self.dns_handle {
996 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 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 #[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 self.peers.push(peer);
1043
1044 self.save().await?;
1046
1047 info!(peer_ip = %overlay_ip, "Added peer to overlay");
1048 Ok(overlay_ip)
1049 }
1050
1051 pub async fn remove_peer(&mut self, public_key: &str) -> Result<()> {
1057 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 let peer_overlay_ip = peer.overlay_ip;
1068 let peer_custom_hostname = peer.hostname.clone();
1069
1070 if let Some(ref mut allocator) = self.allocator {
1072 allocator.release(peer_overlay_ip);
1073 }
1074
1075 if let Some(ref dns_handle) = self.dns_handle {
1077 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 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 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 self.peers.remove(peer_idx);
1127
1128 self.save().await?;
1130
1131 info!(public_key = public_key, "Removed peer from overlay");
1132 Ok(())
1133 }
1134
1135 #[must_use]
1137 pub fn public_key(&self) -> &str {
1138 &self.config.public_key
1139 }
1140
1141 #[must_use]
1143 pub fn node_ip(&self) -> IpAddr {
1144 self.config.node_ip
1145 }
1146
1147 #[must_use]
1149 pub fn cidr(&self) -> &str {
1150 &self.config.cidr
1151 }
1152
1153 #[must_use]
1155 pub fn interface(&self) -> &str {
1156 &self.config.interface
1157 }
1158
1159 #[must_use]
1161 pub fn port(&self) -> u16 {
1162 self.config.port
1163 }
1164
1165 #[must_use]
1167 pub fn is_leader(&self) -> bool {
1168 self.config.is_leader
1169 }
1170
1171 #[must_use]
1173 pub fn peers(&self) -> &[PeerConfig] {
1174 &self.peers
1175 }
1176
1177 #[must_use]
1179 pub fn config(&self) -> &BootstrapConfig {
1180 &self.config
1181 }
1182
1183 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 #[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 #[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 #[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
1260fn 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 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}