1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt;
9use std::net::Ipv4Addr;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum NetworkMode {
15 #[default]
17 Tsi,
18
19 Bridge {
21 network: String,
23 },
24
25 None,
27}
28
29impl fmt::Display for NetworkMode {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 NetworkMode::Tsi => write!(f, "tsi"),
33 NetworkMode::Bridge { network } => write!(f, "bridge:{}", network),
34 NetworkMode::None => write!(f, "none"),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct NetworkConfig {
42 pub name: String,
44
45 pub subnet: String,
47
48 pub gateway: Ipv4Addr,
50
51 #[serde(default = "default_driver")]
53 pub driver: String,
54
55 #[serde(default)]
57 pub labels: HashMap<String, String>,
58
59 #[serde(default)]
61 pub endpoints: HashMap<String, NetworkEndpoint>,
62
63 pub created_at: String,
65
66 #[serde(default)]
68 pub policy: NetworkPolicy,
69}
70
71fn default_driver() -> String {
72 "bridge".to_string()
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct NetworkEndpoint {
78 pub box_id: String,
80
81 pub box_name: String,
83
84 #[serde(default)]
87 pub aliases: Vec<String>,
88
89 pub ip_address: Ipv4Addr,
91
92 pub mac_address: String,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, Default)]
100pub struct NetworkPolicy {
101 #[serde(default)]
103 pub isolation: IsolationMode,
104
105 #[serde(default)]
108 pub ingress: Vec<PolicyRule>,
109
110 #[serde(default)]
113 pub egress: Vec<PolicyRule>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
118#[serde(rename_all = "snake_case")]
119pub enum IsolationMode {
120 #[default]
122 None,
123 Strict,
125 Custom,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct PolicyRule {
132 #[serde(default = "wildcard")]
134 pub from: String,
135
136 #[serde(default = "wildcard")]
138 pub to: String,
139
140 #[serde(default)]
142 pub ports: Vec<u16>,
143
144 #[serde(default = "default_protocol")]
146 pub protocol: String,
147
148 #[serde(default)]
150 pub action: PolicyAction,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
155#[serde(rename_all = "snake_case")]
156pub enum PolicyAction {
157 #[default]
159 Allow,
160 Deny,
162}
163
164fn wildcard() -> String {
165 "*".to_string()
166}
167
168fn default_protocol() -> String {
169 "any".to_string()
170}
171
172impl NetworkPolicy {
173 pub fn validate(&self) -> Result<(), String> {
179 match self.isolation {
180 IsolationMode::None => Ok(()),
181 IsolationMode::Strict => Err(
182 "network policy isolation mode 'strict' is not yet enforced at runtime; \
183 packets will NOT be filtered. Remove the policy or use isolation=none"
184 .to_string(),
185 ),
186 IsolationMode::Custom => Err(
187 "network policy isolation mode 'custom' is not yet enforced at runtime; \
188 ingress/egress rules will NOT be applied. Remove the policy or use isolation=none"
189 .to_string(),
190 ),
191 }
192 }
193
194 pub fn is_peer_allowed(&self, box_name: &str, peer_name: &str) -> bool {
196 match self.isolation {
197 IsolationMode::None => true,
198 IsolationMode::Strict => false,
199 IsolationMode::Custom => {
200 self.evaluate_rules(&self.egress, box_name, peer_name)
202 }
203 }
204 }
205
206 fn evaluate_rules(&self, rules: &[PolicyRule], from: &str, to: &str) -> bool {
208 for rule in rules {
209 if matches_pattern(&rule.from, from) && matches_pattern(&rule.to, to) {
210 return rule.action == PolicyAction::Allow;
211 }
212 }
213 false
215 }
216
217 pub fn allowed_peers<'a>(
219 &self,
220 box_name: &str,
221 peers: &'a [(String, String)],
222 ) -> Vec<&'a (String, String)> {
223 peers
224 .iter()
225 .filter(|(_, peer_name)| self.is_peer_allowed(box_name, peer_name))
226 .collect()
227 }
228}
229
230fn matches_pattern(pattern: &str, name: &str) -> bool {
232 pattern == "*" || pattern == name
233}
234
235#[derive(Debug)]
237pub struct Ipam {
238 network: Ipv4Addr,
240 prefix_len: u8,
242 gateway: Ipv4Addr,
244}
245
246impl Ipam {
247 pub fn new(cidr: &str) -> Result<Self, String> {
249 let parts: Vec<&str> = cidr.split('/').collect();
250 if parts.len() != 2 {
251 return Err(format!("invalid CIDR notation: {}", cidr));
252 }
253
254 let network: Ipv4Addr = parts[0]
255 .parse()
256 .map_err(|e| format!("invalid network address '{}': {}", parts[0], e))?;
257 let prefix_len: u8 = parts[1]
258 .parse()
259 .map_err(|e| format!("invalid prefix length '{}': {}", parts[1], e))?;
260
261 if prefix_len == 0 || prefix_len > 30 {
262 return Err(format!(
263 "prefix length {} out of range (must be 1-30 for a usable subnet)",
264 prefix_len
265 ));
266 }
267
268 let net_u32 = u32::from(network);
271 let gateway =
272 Ipv4Addr::from(net_u32.checked_add(1).ok_or_else(|| {
273 format!("network address '{}' has no room for a gateway", network)
274 })?);
275
276 Ok(Self {
277 network,
278 prefix_len,
279 gateway,
280 })
281 }
282
283 pub fn gateway(&self) -> Ipv4Addr {
285 self.gateway
286 }
287
288 pub fn cidr(&self) -> String {
290 format!("{}/{}", self.network, self.prefix_len)
291 }
292
293 pub fn broadcast(&self) -> Ipv4Addr {
295 let net_u32 = u32::from(self.network);
296 let host_bits = 32 - self.prefix_len as u32;
297 let broadcast = net_u32 | ((1u32 << host_bits) - 1);
298 Ipv4Addr::from(broadcast)
299 }
300
301 pub fn capacity(&self) -> u32 {
303 let host_bits = 32 - self.prefix_len as u32;
304 let total = (1u32 << host_bits) - 1; total.saturating_sub(2) }
307
308 pub fn allocate(&self, used: &[Ipv4Addr]) -> Result<Ipv4Addr, String> {
310 let net_u32 = u32::from(self.network);
311 let broadcast_u32 = u32::from(self.broadcast());
312 let gateway_u32 = u32::from(self.gateway);
313
314 let mut candidate = match net_u32.checked_add(2) {
318 Some(c) => c,
319 None => return Err("no available IP addresses in subnet".to_string()),
320 };
321 while candidate < broadcast_u32 {
322 if candidate != gateway_u32 {
323 let ip = Ipv4Addr::from(candidate);
324 if !used.contains(&ip) {
325 return Ok(ip);
326 }
327 }
328 candidate = match candidate.checked_add(1) {
329 Some(c) => c,
330 None => break,
331 };
332 }
333
334 Err("no available IP addresses in subnet".to_string())
335 }
336
337 pub fn mac_from_ip(ip: &Ipv4Addr) -> String {
340 let octets = ip.octets();
341 format!(
342 "02:42:{:02x}:{:02x}:{:02x}:{:02x}",
343 octets[0], octets[1], octets[2], octets[3]
344 )
345 }
346}
347
348#[derive(Debug)]
353pub struct Ipam6 {
354 network: std::net::Ipv6Addr,
356 prefix_len: u8,
358 gateway: std::net::Ipv6Addr,
360}
361
362impl Ipam6 {
363 pub fn new(cidr: &str) -> Result<Self, String> {
365 let parts: Vec<&str> = cidr.split('/').collect();
366 if parts.len() != 2 {
367 return Err(format!("invalid IPv6 CIDR notation: {}", cidr));
368 }
369
370 let network: std::net::Ipv6Addr = parts[0]
371 .parse()
372 .map_err(|e| format!("invalid IPv6 network address '{}': {}", parts[0], e))?;
373 let prefix_len: u8 = parts[1]
374 .parse()
375 .map_err(|e| format!("invalid prefix length '{}': {}", parts[1], e))?;
376
377 if !(64..=120).contains(&prefix_len) {
378 return Err(format!(
379 "IPv6 prefix length {} out of range (64..=120)",
380 prefix_len
381 ));
382 }
383
384 let net_u128 = u128::from(network);
386 let gateway = std::net::Ipv6Addr::from(net_u128 + 1);
387
388 Ok(Self {
389 network,
390 prefix_len,
391 gateway,
392 })
393 }
394
395 pub fn gateway(&self) -> std::net::Ipv6Addr {
397 self.gateway
398 }
399
400 pub fn cidr(&self) -> String {
402 format!("{}/{}", self.network, self.prefix_len)
403 }
404
405 pub fn allocate(&self, used: &[std::net::Ipv6Addr]) -> Result<std::net::Ipv6Addr, String> {
407 let net_u128 = u128::from(self.network);
408 let host_bits = 128 - self.prefix_len as u32;
409 let max_host = (1u128 << host_bits) - 1; let gateway_u128 = u128::from(self.gateway);
411
412 let mut offset = 2u128;
414 while offset < max_host {
415 let candidate_u128 = net_u128 + offset;
416 if candidate_u128 != gateway_u128 {
417 let ip = std::net::Ipv6Addr::from(candidate_u128);
418 if !used.contains(&ip) {
419 return Ok(ip);
420 }
421 }
422 offset += 1;
423 }
424
425 Err("no available IPv6 addresses in subnet".to_string())
426 }
427}
428
429impl NetworkConfig {
430 pub fn new(name: &str, subnet: &str) -> Result<Self, String> {
432 let ipam = Ipam::new(subnet)?;
433
434 Ok(Self {
435 name: name.to_string(),
436 subnet: ipam.cidr(),
437 gateway: ipam.gateway(),
438 driver: "bridge".to_string(),
439 labels: HashMap::new(),
440 endpoints: HashMap::new(),
441 created_at: chrono::Utc::now().to_rfc3339(),
442 policy: NetworkPolicy::default(),
443 })
444 }
445
446 pub fn validate_runtime(&self) -> Result<(), String> {
448 if self.driver != "bridge" {
449 return Err(format!(
450 "Unsupported network driver '{}'. Only 'bridge' is currently supported",
451 self.driver
452 ));
453 }
454 self.policy
455 .validate()
456 .map_err(|error| format!("Unsupported network isolation mode: {error}"))
457 }
458
459 pub fn connect(&mut self, box_id: &str, box_name: &str) -> Result<NetworkEndpoint, String> {
461 self.connect_with_aliases(box_id, box_name, &[])
462 }
463
464 pub fn connect_with_aliases(
467 &mut self,
468 box_id: &str,
469 box_name: &str,
470 aliases: &[String],
471 ) -> Result<NetworkEndpoint, String> {
472 if self.endpoints.contains_key(box_id) {
473 return Err(format!(
474 "box '{}' is already connected to network '{}'",
475 box_id, self.name
476 ));
477 }
478
479 let ipam = Ipam::new(&self.subnet)?;
480 let used: Vec<Ipv4Addr> = self.endpoints.values().map(|e| e.ip_address).collect();
481 let ip = ipam.allocate(&used)?;
482 let mac = Ipam::mac_from_ip(&ip);
483
484 let endpoint = NetworkEndpoint {
485 box_id: box_id.to_string(),
486 box_name: box_name.to_string(),
487 aliases: aliases
488 .iter()
489 .filter(|a| !a.is_empty() && *a != box_name)
490 .cloned()
491 .collect(),
492 ip_address: ip,
493 mac_address: mac,
494 };
495
496 self.endpoints.insert(box_id.to_string(), endpoint.clone());
497 Ok(endpoint)
498 }
499
500 pub fn disconnect(&mut self, box_id: &str) -> Result<NetworkEndpoint, String> {
502 self.endpoints.remove(box_id).ok_or_else(|| {
503 format!(
504 "box '{}' is not connected to network '{}'",
505 box_id, self.name
506 )
507 })
508 }
509
510 pub fn set_policy(&mut self, policy: NetworkPolicy) -> Result<(), String> {
515 policy.validate()?;
516 self.policy = policy;
517 Ok(())
518 }
519
520 pub fn connected_boxes(&self) -> Vec<&NetworkEndpoint> {
522 self.endpoints.values().collect()
523 }
524
525 pub fn peer_endpoints(&self, exclude_box_id: &str) -> Vec<(String, String)> {
529 self.endpoints
530 .values()
531 .filter(|ep| ep.box_id != exclude_box_id)
532 .flat_map(|ep| {
533 let ip = ep.ip_address.to_string();
534 std::iter::once((ip.clone(), ep.box_name.clone()))
537 .chain(ep.aliases.iter().map(move |a| (ip.clone(), a.clone())))
538 })
539 .collect()
540 }
541
542 pub fn allowed_peer_endpoints(&self, exclude_box_id: &str) -> Vec<(String, String)> {
547 let box_name = self
548 .endpoints
549 .get(exclude_box_id)
550 .map(|ep| ep.box_name.as_str())
551 .unwrap_or("");
552
553 let all_peers = self.peer_endpoints(exclude_box_id);
554 self.policy
555 .allowed_peers(box_name, &all_peers)
556 .into_iter()
557 .cloned()
558 .collect()
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
569 fn test_network_mode_default_is_tsi() {
570 let mode = NetworkMode::default();
571 assert_eq!(mode, NetworkMode::Tsi);
572 }
573
574 #[test]
575 fn test_network_mode_display() {
576 assert_eq!(NetworkMode::Tsi.to_string(), "tsi");
577 assert_eq!(NetworkMode::None.to_string(), "none");
578 assert_eq!(
579 NetworkMode::Bridge {
580 network: "mynet".to_string()
581 }
582 .to_string(),
583 "bridge:mynet"
584 );
585 }
586
587 #[test]
588 fn test_network_mode_serialization() {
589 let mode = NetworkMode::Bridge {
590 network: "test-net".to_string(),
591 };
592 let json = serde_json::to_string(&mode).unwrap();
593 let parsed: NetworkMode = serde_json::from_str(&json).unwrap();
594 assert_eq!(parsed, mode);
595 }
596
597 #[test]
598 fn test_network_mode_tsi_serialization() {
599 let mode = NetworkMode::Tsi;
600 let json = serde_json::to_string(&mode).unwrap();
601 let parsed: NetworkMode = serde_json::from_str(&json).unwrap();
602 assert_eq!(parsed, NetworkMode::Tsi);
603 }
604
605 #[test]
608 fn test_ipam_new_valid() {
609 let ipam = Ipam::new("10.88.0.0/24").unwrap();
610 assert_eq!(ipam.gateway(), Ipv4Addr::new(10, 88, 0, 1));
611 assert_eq!(ipam.cidr(), "10.88.0.0/24");
612 }
613
614 #[test]
615 fn test_ipam_new_slash16() {
616 let ipam = Ipam::new("172.20.0.0/16").unwrap();
617 assert_eq!(ipam.gateway(), Ipv4Addr::new(172, 20, 0, 1));
618 }
619
620 #[test]
621 fn test_ipam_invalid_cidr() {
622 assert!(Ipam::new("10.88.0.0").is_err());
623 assert!(Ipam::new("not-an-ip/24").is_err());
624 assert!(Ipam::new("10.88.0.0/33").is_err());
625 assert!(Ipam::new("10.88.0.0/31").is_err());
626 }
627
628 #[test]
629 fn test_ipam_broadcast() {
630 let ipam = Ipam::new("10.88.0.0/24").unwrap();
631 assert_eq!(ipam.broadcast(), Ipv4Addr::new(10, 88, 0, 255));
632
633 let ipam16 = Ipam::new("172.20.0.0/16").unwrap();
634 assert_eq!(ipam16.broadcast(), Ipv4Addr::new(172, 20, 255, 255));
635 }
636
637 #[test]
638 fn test_ipam_rejects_zero_and_oversized_prefix() {
639 assert!(Ipam::new("0.0.0.0/0").is_err());
641 assert!(Ipam::new("10.0.0.0/31").is_err());
642 assert!(Ipam::new("10.0.0.0/32").is_err());
643 assert!(Ipam::new("10.0.0.0/1").is_ok());
645 assert!(Ipam::new("10.0.0.0/30").is_ok());
646 }
647
648 #[test]
649 fn test_ipam_gateway_overflow_is_rejected_not_panic() {
650 assert!(Ipam::new("255.255.255.255/30").is_err());
652 }
653
654 #[test]
655 fn test_ipam_capacity() {
656 let ipam = Ipam::new("10.88.0.0/24").unwrap();
657 assert_eq!(ipam.capacity(), 253);
659
660 let ipam28 = Ipam::new("10.88.0.0/28").unwrap();
661 assert_eq!(ipam28.capacity(), 13);
663 }
664
665 #[test]
666 fn test_ipam_allocate_first() {
667 let ipam = Ipam::new("10.88.0.0/24").unwrap();
668 let ip = ipam.allocate(&[]).unwrap();
669 assert_eq!(ip, Ipv4Addr::new(10, 88, 0, 2));
671 }
672
673 #[test]
674 fn test_ipam_allocate_sequential() {
675 let ipam = Ipam::new("10.88.0.0/24").unwrap();
676 let ip1 = ipam.allocate(&[]).unwrap();
677 let ip2 = ipam.allocate(&[ip1]).unwrap();
678 let ip3 = ipam.allocate(&[ip1, ip2]).unwrap();
679
680 assert_eq!(ip1, Ipv4Addr::new(10, 88, 0, 2));
681 assert_eq!(ip2, Ipv4Addr::new(10, 88, 0, 3));
682 assert_eq!(ip3, Ipv4Addr::new(10, 88, 0, 4));
683 }
684
685 #[test]
686 fn test_ipam_allocate_skips_gateway() {
687 let ipam = Ipam::new("10.88.0.0/24").unwrap();
688 let ip = ipam.allocate(&[]).unwrap();
690 assert_ne!(ip, ipam.gateway());
691 }
692
693 #[test]
694 fn test_ipam_allocate_exhausted() {
695 let ipam = Ipam::new("10.88.0.0/30").unwrap();
696 let ip1 = ipam.allocate(&[]).unwrap();
699 assert_eq!(ip1, Ipv4Addr::new(10, 88, 0, 2));
700
701 let result = ipam.allocate(&[ip1]);
702 assert!(result.is_err());
703 }
704
705 #[test]
706 fn test_ipam_allocate_top_of_range_no_overflow() {
707 let ipam = Ipam::new("255.255.255.252/30").unwrap();
710 let ip1 = ipam.allocate(&[]).unwrap();
712 assert_eq!(ip1, Ipv4Addr::new(255, 255, 255, 254));
713 assert!(ipam.allocate(&[ip1]).is_err());
714 }
715
716 #[test]
717 fn test_ipam_mac_from_ip() {
718 let ip = Ipv4Addr::new(10, 88, 0, 2);
719 assert_eq!(Ipam::mac_from_ip(&ip), "02:42:0a:58:00:02");
720
721 let ip2 = Ipv4Addr::new(192, 168, 1, 100);
722 assert_eq!(Ipam::mac_from_ip(&ip2), "02:42:c0:a8:01:64");
723 }
724
725 #[test]
728 fn test_network_config_new() {
729 let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
730 assert_eq!(net.name, "mynet");
731 assert_eq!(net.subnet, "10.88.0.0/24");
732 assert_eq!(net.gateway, Ipv4Addr::new(10, 88, 0, 1));
733 assert_eq!(net.driver, "bridge");
734 assert!(net.endpoints.is_empty());
735 }
736
737 #[test]
738 fn test_network_config_invalid_subnet() {
739 assert!(NetworkConfig::new("bad", "invalid").is_err());
740 }
741
742 #[test]
743 fn test_network_config_connect() {
744 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
745 let ep = net.connect("box-1", "web").unwrap();
746
747 assert_eq!(ep.box_id, "box-1");
748 assert_eq!(ep.box_name, "web");
749 assert_eq!(ep.ip_address, Ipv4Addr::new(10, 88, 0, 2));
750 assert_eq!(ep.mac_address, "02:42:0a:58:00:02");
751 assert_eq!(net.endpoints.len(), 1);
752 }
753
754 #[test]
755 fn test_network_config_connect_multiple() {
756 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
757 let ep1 = net.connect("box-1", "web").unwrap();
758 let ep2 = net.connect("box-2", "api").unwrap();
759
760 assert_eq!(ep1.ip_address, Ipv4Addr::new(10, 88, 0, 2));
761 assert_eq!(ep2.ip_address, Ipv4Addr::new(10, 88, 0, 3));
762 assert_eq!(net.endpoints.len(), 2);
763 }
764
765 #[test]
766 fn test_network_config_connect_duplicate() {
767 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
768 net.connect("box-1", "web").unwrap();
769 let result = net.connect("box-1", "web");
770 assert!(result.is_err());
771 }
772
773 #[test]
774 fn test_network_config_disconnect() {
775 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
776 net.connect("box-1", "web").unwrap();
777
778 let ep = net.disconnect("box-1").unwrap();
779 assert_eq!(ep.box_id, "box-1");
780 assert!(net.endpoints.is_empty());
781 }
782
783 #[test]
784 fn test_network_config_disconnect_not_connected() {
785 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
786 let result = net.disconnect("box-1");
787 assert!(result.is_err());
788 }
789
790 #[test]
791 fn test_network_config_connected_boxes() {
792 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
793 net.connect("box-1", "web").unwrap();
794 net.connect("box-2", "api").unwrap();
795
796 let boxes = net.connected_boxes();
797 assert_eq!(boxes.len(), 2);
798 }
799
800 #[test]
801 fn test_network_config_ip_reuse_after_disconnect() {
802 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
803 let ep1 = net.connect("box-1", "web").unwrap();
804 assert_eq!(ep1.ip_address, Ipv4Addr::new(10, 88, 0, 2));
805
806 net.disconnect("box-1").unwrap();
807
808 let ep2 = net.connect("box-2", "api").unwrap();
810 assert_eq!(ep2.ip_address, Ipv4Addr::new(10, 88, 0, 2));
811 }
812
813 #[test]
814 fn test_network_config_serialization() {
815 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
816 net.connect("box-1", "web").unwrap();
817
818 let json = serde_json::to_string(&net).unwrap();
819 let parsed: NetworkConfig = serde_json::from_str(&json).unwrap();
820
821 assert_eq!(parsed.name, "mynet");
822 assert_eq!(parsed.subnet, "10.88.0.0/24");
823 assert_eq!(parsed.endpoints.len(), 1);
824 assert!(parsed.endpoints.contains_key("box-1"));
825 }
826
827 #[test]
832 fn test_peer_endpoints_excludes_self() {
833 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
834 net.connect("box-1", "web").unwrap();
835 net.connect("box-2", "api").unwrap();
836 net.connect("box-3", "db").unwrap();
837
838 let peers = net.peer_endpoints("box-1");
839 assert_eq!(peers.len(), 2);
840 assert!(peers.iter().all(|(_, name)| name != "web"));
841 assert!(peers.iter().any(|(_, name)| name == "api"));
842 assert!(peers.iter().any(|(_, name)| name == "db"));
843 }
844
845 #[test]
846 fn test_connect_with_aliases_resolvable_as_peer() {
847 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
848 let ep = net
850 .connect_with_aliases("box-db", "proj-db", &["db".to_string()])
851 .unwrap();
852 assert_eq!(ep.aliases, vec!["db".to_string()]);
853 net.connect("box-web", "proj-web").unwrap();
854
855 let peers = net.peer_endpoints("box-web");
856 let db_ip = ep.ip_address.to_string();
857 assert!(peers
859 .iter()
860 .any(|(ip, name)| ip == &db_ip && name == "proj-db"));
861 assert!(peers.iter().any(|(ip, name)| ip == &db_ip && name == "db"));
862 }
863
864 #[test]
865 fn test_connect_alias_skips_empty_and_self_duplicate() {
866 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
867 let ep = net
869 .connect_with_aliases("b", "web", &["".to_string(), "web".to_string()])
870 .unwrap();
871 assert!(ep.aliases.is_empty());
872 }
873
874 #[test]
875 fn test_peer_endpoints_empty_when_alone() {
876 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
877 net.connect("box-1", "web").unwrap();
878
879 let peers = net.peer_endpoints("box-1");
880 assert!(peers.is_empty());
881 }
882
883 #[test]
884 fn test_peer_endpoints_returns_all_others() {
885 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
886 net.connect("box-1", "web").unwrap();
887 net.connect("box-2", "api").unwrap();
888
889 let peers = net.peer_endpoints("box-1");
890 assert_eq!(peers.len(), 1);
891 assert_eq!(peers[0].0, "10.88.0.3");
892 assert_eq!(peers[0].1, "api");
893 }
894
895 #[test]
896 fn test_peer_endpoints_nonexistent_excludes_nothing() {
897 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
898 net.connect("box-1", "web").unwrap();
899 net.connect("box-2", "api").unwrap();
900
901 let peers = net.peer_endpoints("nonexistent");
902 assert_eq!(peers.len(), 2);
903 }
904
905 #[test]
908 fn test_network_endpoint_serialization() {
909 let ep = NetworkEndpoint {
910 box_id: "abc123".to_string(),
911 box_name: "web".to_string(),
912 aliases: vec!["app".to_string()],
913 ip_address: Ipv4Addr::new(10, 88, 0, 2),
914 mac_address: "02:42:0a:58:00:02".to_string(),
915 };
916
917 let json = serde_json::to_string(&ep).unwrap();
918 let parsed: NetworkEndpoint = serde_json::from_str(&json).unwrap();
919 assert_eq!(parsed, ep);
920
921 let legacy = r#"{"box_id":"x","box_name":"web","ip_address":"10.88.0.3","mac_address":"02:42:0a:58:00:03"}"#;
923 let parsed_legacy: NetworkEndpoint = serde_json::from_str(legacy).unwrap();
924 assert!(parsed_legacy.aliases.is_empty());
925 }
926
927 #[test]
930 fn test_network_policy_default_allows_all() {
931 let policy = NetworkPolicy::default();
932 assert_eq!(policy.isolation, IsolationMode::None);
933 assert!(policy.is_peer_allowed("web", "db"));
934 assert!(policy.is_peer_allowed("any", "any"));
935 }
936
937 #[test]
938 fn test_network_policy_strict_denies_all() {
939 let policy = NetworkPolicy {
940 isolation: IsolationMode::Strict,
941 ..Default::default()
942 };
943 assert!(!policy.is_peer_allowed("web", "db"));
944 assert!(!policy.is_peer_allowed("any", "any"));
945 }
946
947 #[test]
948 fn test_network_policy_custom_allow_rule() {
949 let policy = NetworkPolicy {
950 isolation: IsolationMode::Custom,
951 egress: vec![PolicyRule {
952 from: "web".to_string(),
953 to: "db".to_string(),
954 ports: vec![],
955 protocol: "any".to_string(),
956 action: PolicyAction::Allow,
957 }],
958 ..Default::default()
959 };
960 assert!(policy.is_peer_allowed("web", "db"));
961 assert!(!policy.is_peer_allowed("web", "redis")); assert!(!policy.is_peer_allowed("api", "db")); }
964
965 #[test]
966 fn test_network_policy_custom_wildcard_from() {
967 let policy = NetworkPolicy {
968 isolation: IsolationMode::Custom,
969 egress: vec![PolicyRule {
970 from: "*".to_string(),
971 to: "db".to_string(),
972 ports: vec![],
973 protocol: "any".to_string(),
974 action: PolicyAction::Allow,
975 }],
976 ..Default::default()
977 };
978 assert!(policy.is_peer_allowed("web", "db"));
979 assert!(policy.is_peer_allowed("api", "db"));
980 assert!(!policy.is_peer_allowed("web", "redis"));
981 }
982
983 #[test]
984 fn test_network_policy_custom_wildcard_to() {
985 let policy = NetworkPolicy {
986 isolation: IsolationMode::Custom,
987 egress: vec![PolicyRule {
988 from: "web".to_string(),
989 to: "*".to_string(),
990 ports: vec![],
991 protocol: "any".to_string(),
992 action: PolicyAction::Allow,
993 }],
994 ..Default::default()
995 };
996 assert!(policy.is_peer_allowed("web", "db"));
997 assert!(policy.is_peer_allowed("web", "redis"));
998 assert!(!policy.is_peer_allowed("api", "db"));
999 }
1000
1001 #[test]
1002 fn test_network_policy_custom_deny_rule() {
1003 let policy = NetworkPolicy {
1004 isolation: IsolationMode::Custom,
1005 egress: vec![
1006 PolicyRule {
1007 from: "web".to_string(),
1008 to: "db".to_string(),
1009 ports: vec![],
1010 protocol: "any".to_string(),
1011 action: PolicyAction::Deny,
1012 },
1013 PolicyRule {
1014 from: "web".to_string(),
1015 to: "*".to_string(),
1016 ports: vec![],
1017 protocol: "any".to_string(),
1018 action: PolicyAction::Allow,
1019 },
1020 ],
1021 ..Default::default()
1022 };
1023 assert!(!policy.is_peer_allowed("web", "db"));
1025 assert!(policy.is_peer_allowed("web", "redis"));
1027 }
1028
1029 #[test]
1030 fn test_network_policy_custom_no_rules_denies() {
1031 let policy = NetworkPolicy {
1032 isolation: IsolationMode::Custom,
1033 egress: vec![],
1034 ..Default::default()
1035 };
1036 assert!(!policy.is_peer_allowed("web", "db"));
1037 }
1038
1039 #[test]
1040 fn test_network_policy_allowed_peers() {
1041 let policy = NetworkPolicy {
1042 isolation: IsolationMode::Custom,
1043 egress: vec![PolicyRule {
1044 from: "web".to_string(),
1045 to: "db".to_string(),
1046 ports: vec![],
1047 protocol: "any".to_string(),
1048 action: PolicyAction::Allow,
1049 }],
1050 ..Default::default()
1051 };
1052
1053 let peers = vec![
1054 ("10.88.0.3".to_string(), "db".to_string()),
1055 ("10.88.0.4".to_string(), "redis".to_string()),
1056 ];
1057
1058 let allowed = policy.allowed_peers("web", &peers);
1059 assert_eq!(allowed.len(), 1);
1060 assert_eq!(allowed[0].1, "db");
1061 }
1062
1063 #[test]
1064 fn test_network_policy_serde_roundtrip() {
1065 let policy = NetworkPolicy {
1066 isolation: IsolationMode::Custom,
1067 egress: vec![PolicyRule {
1068 from: "web".to_string(),
1069 to: "db".to_string(),
1070 ports: vec![5432],
1071 protocol: "tcp".to_string(),
1072 action: PolicyAction::Allow,
1073 }],
1074 ingress: vec![],
1075 };
1076 let json = serde_json::to_string(&policy).unwrap();
1077 let parsed: NetworkPolicy = serde_json::from_str(&json).unwrap();
1078 assert_eq!(parsed.isolation, IsolationMode::Custom);
1079 assert_eq!(parsed.egress.len(), 1);
1080 assert_eq!(parsed.egress[0].ports, vec![5432]);
1081 }
1082
1083 #[test]
1084 fn test_isolation_mode_serde() {
1085 let modes = vec![
1086 (IsolationMode::None, "\"none\""),
1087 (IsolationMode::Strict, "\"strict\""),
1088 (IsolationMode::Custom, "\"custom\""),
1089 ];
1090 for (mode, expected) in modes {
1091 let json = serde_json::to_string(&mode).unwrap();
1092 assert_eq!(json, expected);
1093 let parsed: IsolationMode = serde_json::from_str(&json).unwrap();
1094 assert_eq!(parsed, mode);
1095 }
1096 }
1097
1098 #[test]
1099 fn test_allowed_peer_endpoints_none_policy() {
1100 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
1101 net.connect("box-1", "web").unwrap();
1102 net.connect("box-2", "db").unwrap();
1103 net.connect("box-3", "redis").unwrap();
1104
1105 let peers = net.allowed_peer_endpoints("box-1");
1107 assert_eq!(peers.len(), 2);
1108 }
1109
1110 #[test]
1111 fn test_allowed_peer_endpoints_strict_policy() {
1112 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
1113 net.policy = NetworkPolicy {
1114 isolation: IsolationMode::Strict,
1115 ..Default::default()
1116 };
1117 net.connect("box-1", "web").unwrap();
1118 net.connect("box-2", "db").unwrap();
1119
1120 let peers = net.allowed_peer_endpoints("box-1");
1121 assert!(peers.is_empty());
1122 }
1123
1124 #[test]
1125 fn test_allowed_peer_endpoints_custom_policy() {
1126 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
1127 net.policy = NetworkPolicy {
1128 isolation: IsolationMode::Custom,
1129 egress: vec![PolicyRule {
1130 from: "web".to_string(),
1131 to: "db".to_string(),
1132 ports: vec![],
1133 protocol: "any".to_string(),
1134 action: PolicyAction::Allow,
1135 }],
1136 ..Default::default()
1137 };
1138 net.connect("box-1", "web").unwrap();
1139 net.connect("box-2", "db").unwrap();
1140 net.connect("box-3", "redis").unwrap();
1141
1142 let peers = net.allowed_peer_endpoints("box-1");
1143 assert_eq!(peers.len(), 1);
1144 assert_eq!(peers[0].1, "db");
1145 }
1146
1147 #[test]
1148 fn test_matches_pattern() {
1149 assert!(matches_pattern("*", "anything"));
1150 assert!(matches_pattern("web", "web"));
1151 assert!(!matches_pattern("web", "api"));
1152 }
1153
1154 #[test]
1155 fn test_policy_action_default() {
1156 assert_eq!(PolicyAction::default(), PolicyAction::Allow);
1157 }
1158
1159 #[test]
1162 fn test_policy_validate_none_ok() {
1163 let policy = NetworkPolicy::default();
1164 assert!(policy.validate().is_ok());
1165 }
1166
1167 #[test]
1168 fn test_policy_validate_strict_rejected() {
1169 let policy = NetworkPolicy {
1170 isolation: IsolationMode::Strict,
1171 ..Default::default()
1172 };
1173 let err = policy.validate().unwrap_err();
1174 assert!(err.contains("strict"));
1175 assert!(err.contains("not yet enforced"));
1176 }
1177
1178 #[test]
1179 fn test_policy_validate_custom_rejected() {
1180 let policy = NetworkPolicy {
1181 isolation: IsolationMode::Custom,
1182 egress: vec![PolicyRule {
1183 from: "web".to_string(),
1184 to: "db".to_string(),
1185 ports: vec![],
1186 protocol: "any".to_string(),
1187 action: PolicyAction::Allow,
1188 }],
1189 ..Default::default()
1190 };
1191 let err = policy.validate().unwrap_err();
1192 assert!(err.contains("custom"));
1193 assert!(err.contains("not yet enforced"));
1194 }
1195
1196 #[test]
1197 fn test_network_config_runtime_validation_accepts_supported_configuration() {
1198 let network = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
1199
1200 assert!(network.validate_runtime().is_ok());
1201 }
1202
1203 #[test]
1204 fn test_network_config_runtime_validation_rejects_unsupported_driver() {
1205 let mut network = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
1206 network.driver = "overlay".to_string();
1207
1208 let error = network.validate_runtime().unwrap_err();
1209
1210 assert!(error.contains("Unsupported network driver"));
1211 }
1212
1213 #[test]
1216 fn test_set_policy_none_ok() {
1217 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
1218 assert!(net.set_policy(NetworkPolicy::default()).is_ok());
1219 }
1220
1221 #[test]
1222 fn test_set_policy_strict_rejected() {
1223 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
1224 let result = net.set_policy(NetworkPolicy {
1225 isolation: IsolationMode::Strict,
1226 ..Default::default()
1227 });
1228 assert!(result.is_err());
1229 }
1230
1231 #[test]
1234 fn test_ipam6_new_valid() {
1235 let ipam = Ipam6::new("fd00::/64").unwrap();
1236 assert_eq!(
1237 ipam.gateway(),
1238 "fd00::1".parse::<std::net::Ipv6Addr>().unwrap()
1239 );
1240 assert_eq!(ipam.cidr(), "fd00::/64");
1241 }
1242
1243 #[test]
1244 fn test_ipam6_invalid_cidr() {
1245 assert!(Ipam6::new("fd00::").is_err());
1246 assert!(Ipam6::new("not-an-ip/64").is_err());
1247 assert!(Ipam6::new("fd00::/63").is_err()); assert!(Ipam6::new("fd00::/121").is_err()); }
1250
1251 #[test]
1252 fn test_ipam6_allocate_first() {
1253 let ipam = Ipam6::new("fd00::/64").unwrap();
1254 let ip = ipam.allocate(&[]).unwrap();
1255 assert_eq!(ip, "fd00::2".parse::<std::net::Ipv6Addr>().unwrap());
1256 }
1257
1258 #[test]
1259 fn test_ipam6_allocate_sequential() {
1260 let ipam = Ipam6::new("fd00::/64").unwrap();
1261 let ip1 = ipam.allocate(&[]).unwrap();
1262 let ip2 = ipam.allocate(&[ip1]).unwrap();
1263 let ip3 = ipam.allocate(&[ip1, ip2]).unwrap();
1264
1265 assert_eq!(ip1, "fd00::2".parse::<std::net::Ipv6Addr>().unwrap());
1266 assert_eq!(ip2, "fd00::3".parse::<std::net::Ipv6Addr>().unwrap());
1267 assert_eq!(ip3, "fd00::4".parse::<std::net::Ipv6Addr>().unwrap());
1268 }
1269
1270 #[test]
1271 fn test_ipam6_allocate_skips_gateway() {
1272 let ipam = Ipam6::new("fd00::/64").unwrap();
1273 let ip = ipam.allocate(&[]).unwrap();
1274 assert_ne!(ip, ipam.gateway());
1275 }
1276
1277 #[test]
1278 fn test_ipam6_slash120() {
1279 let ipam = Ipam6::new("fd00::/120").unwrap();
1280 let ip = ipam.allocate(&[]).unwrap();
1281 assert_eq!(ip, "fd00::2".parse::<std::net::Ipv6Addr>().unwrap());
1282 }
1283}