1use crate::allocator::IpAllocator;
7use crate::config::PeerInfo;
8use crate::dns::{peer_hostname, DnsConfig, DnsHandle, DnsServer, DEFAULT_DNS_PORT};
9use crate::error::{OverlayError, Result};
10use crate::transport::OverlayTransport;
11use serde::{Deserialize, Serialize};
12use std::net::{IpAddr, Ipv4Addr, SocketAddr};
13use std::path::{Path, PathBuf};
14use std::time::Duration;
15use tracing::{debug, info, warn};
16
17pub const DEFAULT_INTERFACE_NAME: &str = "zl-overlay0";
19
20pub const DEFAULT_WG_PORT: u16 = 51820;
22
23pub const DEFAULT_OVERLAY_CIDR: &str = "10.200.0.0/16";
25
26pub const DEFAULT_KEEPALIVE_SECS: u16 = 25;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct BootstrapConfig {
35 pub cidr: String,
37
38 pub node_ip: Ipv4Addr,
40
41 pub interface: String,
43
44 pub port: u16,
46
47 pub private_key: String,
49
50 pub public_key: String,
52
53 pub is_leader: bool,
55
56 pub created_at: u64,
58}
59
60impl BootstrapConfig {
61 #[must_use]
63 pub fn allowed_ip(&self) -> String {
64 format!("{}/32", self.node_ip)
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct PeerConfig {
71 pub node_id: String,
73
74 pub public_key: String,
76
77 pub endpoint: String,
79
80 pub overlay_ip: Ipv4Addr,
82
83 #[serde(default)]
85 pub keepalive: Option<u16>,
86
87 #[serde(default)]
91 pub hostname: Option<String>,
92}
93
94impl PeerConfig {
95 #[must_use]
97 pub fn new(
98 node_id: String,
99 public_key: String,
100 endpoint: String,
101 overlay_ip: Ipv4Addr,
102 ) -> Self {
103 Self {
104 node_id,
105 public_key,
106 endpoint,
107 overlay_ip,
108 keepalive: Some(DEFAULT_KEEPALIVE_SECS),
109 hostname: None,
110 }
111 }
112
113 #[must_use]
115 pub fn with_hostname(mut self, hostname: impl Into<String>) -> Self {
116 self.hostname = Some(hostname.into());
117 self
118 }
119
120 pub fn to_peer_info(&self) -> std::result::Result<PeerInfo, Box<dyn std::error::Error>> {
126 let endpoint: SocketAddr = self.endpoint.parse()?;
127 let keepalive =
128 Duration::from_secs(u64::from(self.keepalive.unwrap_or(DEFAULT_KEEPALIVE_SECS)));
129
130 Ok(PeerInfo::new(
131 self.public_key.clone(),
132 endpoint,
133 &format!("{}/32", self.overlay_ip),
134 keepalive,
135 ))
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct BootstrapState {
142 pub config: BootstrapConfig,
144
145 pub peers: Vec<PeerConfig>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub allocator_state: Option<crate::allocator::IpAllocatorState>,
151}
152
153pub struct OverlayBootstrap {
158 config: BootstrapConfig,
160
161 peers: Vec<PeerConfig>,
163
164 data_dir: PathBuf,
166
167 allocator: Option<IpAllocator>,
169
170 dns_config: Option<DnsConfig>,
172
173 dns_handle: Option<DnsHandle>,
175
176 transport: Option<OverlayTransport>,
181}
182
183impl OverlayBootstrap {
184 pub async fn init_leader(cidr: &str, port: u16, data_dir: &Path) -> Result<Self> {
207 let config_path = data_dir.join("overlay_bootstrap.json");
209 if config_path.exists() {
210 return Err(OverlayError::AlreadyInitialized(
211 config_path.display().to_string(),
212 ));
213 }
214
215 tokio::fs::create_dir_all(data_dir).await?;
217
218 info!("Generating overlay keypair for leader");
220 let (private_key, public_key) = OverlayTransport::generate_keys()
221 .await
222 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
223
224 let mut allocator = IpAllocator::new(cidr)?;
226 let node_ip = allocator.allocate_first()?;
227
228 info!(node_ip = %node_ip, cidr = cidr, "Allocated leader IP");
229
230 let config = BootstrapConfig {
232 cidr: cidr.to_string(),
233 node_ip,
234 interface: DEFAULT_INTERFACE_NAME.to_string(),
235 port,
236 private_key,
237 public_key,
238 is_leader: true,
239 created_at: current_timestamp(),
240 };
241
242 let bootstrap = Self {
243 config,
244 peers: Vec::new(),
245 data_dir: data_dir.to_path_buf(),
246 allocator: Some(allocator),
247 dns_config: None,
248 dns_handle: None,
249 transport: None,
250 };
251
252 bootstrap.save().await?;
254
255 Ok(bootstrap)
256 }
257
258 pub async fn join(
276 leader_cidr: &str,
277 leader_endpoint: &str,
278 leader_public_key: &str,
279 leader_overlay_ip: Ipv4Addr,
280 allocated_ip: Ipv4Addr,
281 port: u16,
282 data_dir: &Path,
283 ) -> Result<Self> {
284 let config_path = data_dir.join("overlay_bootstrap.json");
286 if config_path.exists() {
287 return Err(OverlayError::AlreadyInitialized(
288 config_path.display().to_string(),
289 ));
290 }
291
292 tokio::fs::create_dir_all(data_dir).await?;
294
295 info!("Generating overlay keypair for joining node");
297 let (private_key, public_key) = OverlayTransport::generate_keys()
298 .await
299 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
300
301 let config = BootstrapConfig {
303 cidr: leader_cidr.to_string(),
304 node_ip: allocated_ip,
305 interface: DEFAULT_INTERFACE_NAME.to_string(),
306 port,
307 private_key,
308 public_key,
309 is_leader: false,
310 created_at: current_timestamp(),
311 };
312
313 let leader_peer = PeerConfig {
315 node_id: "leader".to_string(),
316 public_key: leader_public_key.to_string(),
317 endpoint: leader_endpoint.to_string(),
318 overlay_ip: leader_overlay_ip,
319 keepalive: Some(DEFAULT_KEEPALIVE_SECS),
320 hostname: None, };
322
323 info!(
324 leader_endpoint = leader_endpoint,
325 overlay_ip = %allocated_ip,
326 "Configured leader as peer"
327 );
328
329 let bootstrap = Self {
330 config,
331 peers: vec![leader_peer],
332 data_dir: data_dir.to_path_buf(),
333 allocator: None, dns_config: None,
335 dns_handle: None,
336 transport: None,
337 };
338
339 bootstrap.save().await?;
341
342 Ok(bootstrap)
343 }
344
345 pub async fn load(data_dir: &Path) -> Result<Self> {
351 let config_path = data_dir.join("overlay_bootstrap.json");
352
353 if !config_path.exists() {
354 return Err(OverlayError::NotInitialized);
355 }
356
357 let contents = tokio::fs::read_to_string(&config_path).await?;
358 let state: BootstrapState = serde_json::from_str(&contents)?;
359
360 let allocator = if let Some(alloc_state) = state.allocator_state {
361 Some(IpAllocator::from_state(alloc_state)?)
362 } else {
363 None
364 };
365
366 Ok(Self {
367 config: state.config,
368 peers: state.peers,
369 data_dir: data_dir.to_path_buf(),
370 allocator,
371 dns_config: None, dns_handle: None,
373 transport: None,
374 })
375 }
376
377 pub async fn save(&self) -> Result<()> {
383 let config_path = self.data_dir.join("overlay_bootstrap.json");
384
385 let state = BootstrapState {
386 config: self.config.clone(),
387 peers: self.peers.clone(),
388 allocator_state: self
389 .allocator
390 .as_ref()
391 .map(super::allocator::IpAllocator::to_state),
392 };
393
394 let contents = serde_json::to_string_pretty(&state)?;
395 tokio::fs::write(&config_path, contents).await?;
396
397 debug!(path = %config_path.display(), "Saved bootstrap state");
398 Ok(())
399 }
400
401 pub fn with_dns(mut self, zone: &str, port: u16) -> Result<Self> {
425 self.dns_config = Some(DnsConfig {
426 zone: zone.to_string(),
427 port,
428 bind_addr: IpAddr::V4(self.config.node_ip),
429 });
430 Ok(self)
431 }
432
433 pub fn with_dns_default(self, zone: &str) -> Result<Self> {
439 self.with_dns(zone, DEFAULT_DNS_PORT)
440 }
441
442 #[must_use]
446 pub fn dns_handle(&self) -> Option<&DnsHandle> {
447 self.dns_handle.as_ref()
448 }
449
450 #[must_use]
452 pub fn dns_enabled(&self) -> bool {
453 self.dns_config.is_some()
454 }
455
456 pub async fn start(&mut self) -> Result<()> {
465 info!(
466 interface = %self.config.interface,
467 overlay_ip = %self.config.node_ip,
468 port = self.config.port,
469 dns_enabled = self.dns_config.is_some(),
470 "Starting overlay network"
471 );
472
473 let overlay_config = crate::config::OverlayConfig {
475 local_endpoint: SocketAddr::new(
476 std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
477 self.config.port,
478 ),
479 private_key: self.config.private_key.clone(),
480 public_key: self.config.public_key.clone(),
481 overlay_cidr: self.config.allowed_ip(),
482 peer_discovery_interval: Duration::from_secs(30),
483 };
484
485 let mut transport = OverlayTransport::new(overlay_config, self.config.interface.clone());
487
488 transport
490 .create_interface()
491 .await
492 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
493
494 let peer_infos: Vec<PeerInfo> = self
496 .peers
497 .iter()
498 .filter_map(|p| match p.to_peer_info() {
499 Ok(info) => Some(info),
500 Err(e) => {
501 warn!(peer = %p.node_id, error = %e, "Failed to parse peer info");
502 None
503 }
504 })
505 .collect();
506
507 transport
509 .configure(&peer_infos)
510 .await
511 .map_err(|e| OverlayError::TransportCommand(e.to_string()))?;
512
513 self.transport = Some(transport);
516
517 if let Some(dns_config) = &self.dns_config {
519 info!(
520 zone = %dns_config.zone,
521 port = dns_config.port,
522 "Starting DNS server for overlay"
523 );
524
525 let dns_server =
526 DnsServer::from_config(dns_config).map_err(|e| OverlayError::Dns(e.to_string()))?;
527
528 let self_hostname = peer_hostname(self.config.node_ip);
530 dns_server
531 .add_record(&self_hostname, self.config.node_ip)
532 .await
533 .map_err(|e| OverlayError::Dns(e.to_string()))?;
534
535 if self.config.is_leader {
537 dns_server
538 .add_record("leader", self.config.node_ip)
539 .await
540 .map_err(|e| OverlayError::Dns(e.to_string()))?;
541 debug!(ip = %self.config.node_ip, "Registered leader.{}", dns_config.zone);
542 }
543
544 for peer in &self.peers {
546 let hostname = peer_hostname(peer.overlay_ip);
548 dns_server
549 .add_record(&hostname, peer.overlay_ip)
550 .await
551 .map_err(|e| OverlayError::Dns(e.to_string()))?;
552
553 if let Some(custom) = &peer.hostname {
555 dns_server
556 .add_record(custom, peer.overlay_ip)
557 .await
558 .map_err(|e| OverlayError::Dns(e.to_string()))?;
559 debug!(
560 hostname = custom,
561 ip = %peer.overlay_ip,
562 "Registered custom hostname"
563 );
564 }
565 }
566
567 let handle = dns_server
569 .start()
570 .await
571 .map_err(|e| OverlayError::Dns(e.to_string()))?;
572 self.dns_handle = Some(handle);
573
574 info!("DNS server started successfully");
575 }
576
577 info!("Overlay network started successfully");
578 Ok(())
579 }
580
581 #[allow(clippy::unused_async)]
587 pub async fn stop(&mut self) -> Result<()> {
588 info!(interface = %self.config.interface, "Stopping overlay network");
589
590 if let Some(mut transport) = self.transport.take() {
591 transport.shutdown();
592 }
593
594 Ok(())
595 }
596
597 pub async fn add_peer(&mut self, mut peer: PeerConfig) -> Result<Ipv4Addr> {
605 let overlay_ip = if let Some(ref mut allocator) = self.allocator {
607 let ip = allocator.allocate().ok_or(OverlayError::NoAvailableIps)?;
608 peer.overlay_ip = ip;
609 ip
610 } else {
611 peer.overlay_ip
612 };
613
614 if let Ok(peer_info) = peer.to_peer_info() {
616 let transport_ref: Option<&OverlayTransport> = self.transport.as_ref();
619
620 let result = if let Some(t) = transport_ref {
621 t.add_peer(&peer_info).await
622 } else {
623 let overlay_config = crate::config::OverlayConfig {
624 local_endpoint: SocketAddr::new(
625 std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
626 self.config.port,
627 ),
628 private_key: self.config.private_key.clone(),
629 public_key: self.config.public_key.clone(),
630 overlay_cidr: self.config.allowed_ip(),
631 peer_discovery_interval: Duration::from_secs(30),
632 };
633 let tmp = OverlayTransport::new(overlay_config, self.config.interface.clone());
634 tmp.add_peer(&peer_info).await
635 };
636
637 match result {
638 Ok(()) => debug!(peer = %peer.node_id, "Added peer to overlay"),
639 Err(e) => {
640 warn!(peer = %peer.node_id, error = %e, "Failed to add peer to overlay (interface may not be up)");
641 }
642 }
643 }
644
645 if let Some(ref dns_handle) = self.dns_handle {
647 let hostname = peer_hostname(overlay_ip);
649 dns_handle
650 .add_record(&hostname, overlay_ip)
651 .await
652 .map_err(|e| OverlayError::Dns(e.to_string()))?;
653 debug!(hostname = %hostname, ip = %overlay_ip, "Registered peer in DNS");
654
655 if let Some(ref custom) = peer.hostname {
657 dns_handle
658 .add_record(custom, overlay_ip)
659 .await
660 .map_err(|e| OverlayError::Dns(e.to_string()))?;
661 debug!(hostname = %custom, ip = %overlay_ip, "Registered custom hostname in DNS");
662 }
663 }
664
665 self.peers.push(peer);
667
668 self.save().await?;
670
671 info!(peer_ip = %overlay_ip, "Added peer to overlay");
672 Ok(overlay_ip)
673 }
674
675 pub async fn remove_peer(&mut self, public_key: &str) -> Result<()> {
681 let peer_idx = self
683 .peers
684 .iter()
685 .position(|p| p.public_key == public_key)
686 .ok_or_else(|| OverlayError::PeerNotFound(public_key.to_string()))?;
687
688 let peer = &self.peers[peer_idx];
689
690 let peer_overlay_ip = peer.overlay_ip;
692 let peer_custom_hostname = peer.hostname.clone();
693
694 if let Some(ref mut allocator) = self.allocator {
696 allocator.release(peer_overlay_ip);
697 }
698
699 if let Some(ref dns_handle) = self.dns_handle {
701 let hostname = peer_hostname(peer_overlay_ip);
703 dns_handle
704 .remove_record(&hostname)
705 .await
706 .map_err(|e| OverlayError::Dns(e.to_string()))?;
707 debug!(hostname = %hostname, "Removed peer from DNS");
708
709 if let Some(ref custom) = peer_custom_hostname {
711 dns_handle
712 .remove_record(custom)
713 .await
714 .map_err(|e| OverlayError::Dns(e.to_string()))?;
715 debug!(hostname = %custom, "Removed custom hostname from DNS");
716 }
717 }
718
719 let transport_ref: Option<&OverlayTransport> = self.transport.as_ref();
721
722 let result = if let Some(t) = transport_ref {
723 t.remove_peer(public_key).await
724 } else {
725 let overlay_config = crate::config::OverlayConfig {
726 local_endpoint: SocketAddr::new(
727 std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED),
728 self.config.port,
729 ),
730 private_key: self.config.private_key.clone(),
731 public_key: self.config.public_key.clone(),
732 overlay_cidr: self.config.allowed_ip(),
733 peer_discovery_interval: Duration::from_secs(30),
734 };
735 let tmp = OverlayTransport::new(overlay_config, self.config.interface.clone());
736 tmp.remove_peer(public_key).await
737 };
738
739 match result {
740 Ok(()) => debug!(public_key = public_key, "Removed peer from overlay"),
741 Err(e) => {
742 warn!(public_key = public_key, error = %e, "Failed to remove peer from overlay");
743 }
744 }
745
746 self.peers.remove(peer_idx);
748
749 self.save().await?;
751
752 info!(public_key = public_key, "Removed peer from overlay");
753 Ok(())
754 }
755
756 #[must_use]
758 pub fn public_key(&self) -> &str {
759 &self.config.public_key
760 }
761
762 #[must_use]
764 pub fn node_ip(&self) -> Ipv4Addr {
765 self.config.node_ip
766 }
767
768 #[must_use]
770 pub fn cidr(&self) -> &str {
771 &self.config.cidr
772 }
773
774 #[must_use]
776 pub fn interface(&self) -> &str {
777 &self.config.interface
778 }
779
780 #[must_use]
782 pub fn port(&self) -> u16 {
783 self.config.port
784 }
785
786 #[must_use]
788 pub fn is_leader(&self) -> bool {
789 self.config.is_leader
790 }
791
792 #[must_use]
794 pub fn peers(&self) -> &[PeerConfig] {
795 &self.peers
796 }
797
798 #[must_use]
800 pub fn config(&self) -> &BootstrapConfig {
801 &self.config
802 }
803
804 pub fn allocate_peer_ip(&mut self) -> Result<Ipv4Addr> {
812 let allocator = self
813 .allocator
814 .as_mut()
815 .ok_or(OverlayError::Config("Not a leader node".to_string()))?;
816
817 allocator.allocate().ok_or(OverlayError::NoAvailableIps)
818 }
819
820 #[must_use]
822 #[allow(clippy::cast_possible_truncation)]
823 pub fn allocation_stats(&self) -> Option<(u32, u32)> {
824 self.allocator
825 .as_ref()
826 .map(|a| (a.allocated_count() as u32, a.total_hosts()))
827 }
828}
829
830fn current_timestamp() -> u64 {
832 std::time::SystemTime::now()
833 .duration_since(std::time::UNIX_EPOCH)
834 .unwrap_or_default()
835 .as_secs()
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841
842 #[test]
843 fn test_bootstrap_config_allowed_ip() {
844 let config = BootstrapConfig {
845 cidr: "10.200.0.0/16".to_string(),
846 node_ip: "10.200.0.1".parse().unwrap(),
847 interface: DEFAULT_INTERFACE_NAME.to_string(),
848 port: DEFAULT_WG_PORT,
849 private_key: "test_private".to_string(),
850 public_key: "test_public".to_string(),
851 is_leader: true,
852 created_at: 0,
853 };
854
855 assert_eq!(config.allowed_ip(), "10.200.0.1/32");
856 }
857
858 #[test]
859 fn test_peer_config_new() {
860 let peer = PeerConfig::new(
861 "node-1".to_string(),
862 "pubkey123".to_string(),
863 "192.168.1.100:51820".to_string(),
864 "10.200.0.5".parse().unwrap(),
865 );
866
867 assert_eq!(peer.node_id, "node-1");
868 assert_eq!(peer.keepalive, Some(DEFAULT_KEEPALIVE_SECS));
869 assert_eq!(peer.hostname, None);
870 }
871
872 #[test]
873 fn test_peer_config_with_hostname() {
874 let peer = PeerConfig::new(
875 "node-1".to_string(),
876 "pubkey123".to_string(),
877 "192.168.1.100:51820".to_string(),
878 "10.200.0.5".parse().unwrap(),
879 )
880 .with_hostname("web-server");
881
882 assert_eq!(peer.hostname, Some("web-server".to_string()));
883 }
884
885 #[test]
886 fn test_peer_config_to_peer_info() {
887 let peer = PeerConfig::new(
888 "node-1".to_string(),
889 "pubkey123".to_string(),
890 "192.168.1.100:51820".to_string(),
891 "10.200.0.5".parse().unwrap(),
892 );
893
894 let peer_info = peer.to_peer_info().unwrap();
895 assert_eq!(peer_info.public_key, "pubkey123");
896 assert_eq!(peer_info.allowed_ips, "10.200.0.5/32");
897 }
898
899 #[test]
900 fn test_bootstrap_state_serialization() {
901 let config = BootstrapConfig {
902 cidr: "10.200.0.0/16".to_string(),
903 node_ip: "10.200.0.1".parse().unwrap(),
904 interface: DEFAULT_INTERFACE_NAME.to_string(),
905 port: DEFAULT_WG_PORT,
906 private_key: "private".to_string(),
907 public_key: "public".to_string(),
908 is_leader: true,
909 created_at: 1234567890,
910 };
911
912 let state = BootstrapState {
913 config,
914 peers: vec![],
915 allocator_state: None,
916 };
917
918 let json = serde_json::to_string_pretty(&state).unwrap();
919 let deserialized: BootstrapState = serde_json::from_str(&json).unwrap();
920
921 assert_eq!(deserialized.config.cidr, "10.200.0.0/16");
922 assert_eq!(deserialized.config.node_ip.to_string(), "10.200.0.1");
923 }
924}