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 pub ip_address: Ipv4Addr,
86
87 pub mac_address: String,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, Default)]
95pub struct NetworkPolicy {
96 #[serde(default)]
98 pub isolation: IsolationMode,
99
100 #[serde(default)]
103 pub ingress: Vec<PolicyRule>,
104
105 #[serde(default)]
108 pub egress: Vec<PolicyRule>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
113#[serde(rename_all = "snake_case")]
114pub enum IsolationMode {
115 #[default]
117 None,
118 Strict,
120 Custom,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct PolicyRule {
127 #[serde(default = "wildcard")]
129 pub from: String,
130
131 #[serde(default = "wildcard")]
133 pub to: String,
134
135 #[serde(default)]
137 pub ports: Vec<u16>,
138
139 #[serde(default = "default_protocol")]
141 pub protocol: String,
142
143 #[serde(default)]
145 pub action: PolicyAction,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
150#[serde(rename_all = "snake_case")]
151pub enum PolicyAction {
152 #[default]
154 Allow,
155 Deny,
157}
158
159fn wildcard() -> String {
160 "*".to_string()
161}
162
163fn default_protocol() -> String {
164 "any".to_string()
165}
166
167impl NetworkPolicy {
168 pub fn validate(&self) -> Result<(), String> {
174 match self.isolation {
175 IsolationMode::None => Ok(()),
176 IsolationMode::Strict => Err(
177 "network policy isolation mode 'strict' is not yet enforced at runtime; \
178 packets will NOT be filtered. Remove the policy or use isolation=none"
179 .to_string(),
180 ),
181 IsolationMode::Custom => Err(
182 "network policy isolation mode 'custom' is not yet enforced at runtime; \
183 ingress/egress rules will NOT be applied. Remove the policy or use isolation=none"
184 .to_string(),
185 ),
186 }
187 }
188
189 pub fn is_peer_allowed(&self, box_name: &str, peer_name: &str) -> bool {
191 match self.isolation {
192 IsolationMode::None => true,
193 IsolationMode::Strict => false,
194 IsolationMode::Custom => {
195 self.evaluate_rules(&self.egress, box_name, peer_name)
197 }
198 }
199 }
200
201 fn evaluate_rules(&self, rules: &[PolicyRule], from: &str, to: &str) -> bool {
203 for rule in rules {
204 if matches_pattern(&rule.from, from) && matches_pattern(&rule.to, to) {
205 return rule.action == PolicyAction::Allow;
206 }
207 }
208 false
210 }
211
212 pub fn allowed_peers<'a>(
214 &self,
215 box_name: &str,
216 peers: &'a [(String, String)],
217 ) -> Vec<&'a (String, String)> {
218 peers
219 .iter()
220 .filter(|(_, peer_name)| self.is_peer_allowed(box_name, peer_name))
221 .collect()
222 }
223}
224
225fn matches_pattern(pattern: &str, name: &str) -> bool {
227 pattern == "*" || pattern == name
228}
229
230#[derive(Debug)]
232pub struct Ipam {
233 network: Ipv4Addr,
235 prefix_len: u8,
237 gateway: Ipv4Addr,
239}
240
241impl Ipam {
242 pub fn new(cidr: &str) -> Result<Self, String> {
244 let parts: Vec<&str> = cidr.split('/').collect();
245 if parts.len() != 2 {
246 return Err(format!("invalid CIDR notation: {}", cidr));
247 }
248
249 let network: Ipv4Addr = parts[0]
250 .parse()
251 .map_err(|e| format!("invalid network address '{}': {}", parts[0], e))?;
252 let prefix_len: u8 = parts[1]
253 .parse()
254 .map_err(|e| format!("invalid prefix length '{}': {}", parts[1], e))?;
255
256 if prefix_len > 30 {
257 return Err(format!(
258 "prefix length {} too large (max 30 for usable hosts)",
259 prefix_len
260 ));
261 }
262
263 let net_u32 = u32::from(network);
265 let gateway = Ipv4Addr::from(net_u32 + 1);
266
267 Ok(Self {
268 network,
269 prefix_len,
270 gateway,
271 })
272 }
273
274 pub fn gateway(&self) -> Ipv4Addr {
276 self.gateway
277 }
278
279 pub fn cidr(&self) -> String {
281 format!("{}/{}", self.network, self.prefix_len)
282 }
283
284 pub fn broadcast(&self) -> Ipv4Addr {
286 let net_u32 = u32::from(self.network);
287 let host_bits = 32 - self.prefix_len as u32;
288 let broadcast = net_u32 | ((1u32 << host_bits) - 1);
289 Ipv4Addr::from(broadcast)
290 }
291
292 pub fn capacity(&self) -> u32 {
294 let host_bits = 32 - self.prefix_len as u32;
295 let total = (1u32 << host_bits) - 1; total.saturating_sub(2) }
298
299 pub fn allocate(&self, used: &[Ipv4Addr]) -> Result<Ipv4Addr, String> {
301 let net_u32 = u32::from(self.network);
302 let broadcast_u32 = u32::from(self.broadcast());
303 let gateway_u32 = u32::from(self.gateway);
304
305 let mut candidate = net_u32 + 2;
307 while candidate < broadcast_u32 {
308 if candidate != gateway_u32 {
309 let ip = Ipv4Addr::from(candidate);
310 if !used.contains(&ip) {
311 return Ok(ip);
312 }
313 }
314 candidate += 1;
315 }
316
317 Err("no available IP addresses in subnet".to_string())
318 }
319
320 pub fn mac_from_ip(ip: &Ipv4Addr) -> String {
323 let octets = ip.octets();
324 format!(
325 "02:42:{:02x}:{:02x}:{:02x}:{:02x}",
326 octets[0], octets[1], octets[2], octets[3]
327 )
328 }
329}
330
331#[derive(Debug)]
336pub struct Ipam6 {
337 network: std::net::Ipv6Addr,
339 prefix_len: u8,
341 gateway: std::net::Ipv6Addr,
343}
344
345impl Ipam6 {
346 pub fn new(cidr: &str) -> Result<Self, String> {
348 let parts: Vec<&str> = cidr.split('/').collect();
349 if parts.len() != 2 {
350 return Err(format!("invalid IPv6 CIDR notation: {}", cidr));
351 }
352
353 let network: std::net::Ipv6Addr = parts[0]
354 .parse()
355 .map_err(|e| format!("invalid IPv6 network address '{}': {}", parts[0], e))?;
356 let prefix_len: u8 = parts[1]
357 .parse()
358 .map_err(|e| format!("invalid prefix length '{}': {}", parts[1], e))?;
359
360 if !(64..=120).contains(&prefix_len) {
361 return Err(format!(
362 "IPv6 prefix length {} out of range (64..=120)",
363 prefix_len
364 ));
365 }
366
367 let net_u128 = u128::from(network);
369 let gateway = std::net::Ipv6Addr::from(net_u128 + 1);
370
371 Ok(Self {
372 network,
373 prefix_len,
374 gateway,
375 })
376 }
377
378 pub fn gateway(&self) -> std::net::Ipv6Addr {
380 self.gateway
381 }
382
383 pub fn cidr(&self) -> String {
385 format!("{}/{}", self.network, self.prefix_len)
386 }
387
388 pub fn allocate(&self, used: &[std::net::Ipv6Addr]) -> Result<std::net::Ipv6Addr, String> {
390 let net_u128 = u128::from(self.network);
391 let host_bits = 128 - self.prefix_len as u32;
392 let max_host = (1u128 << host_bits) - 1; let gateway_u128 = u128::from(self.gateway);
394
395 let mut offset = 2u128;
397 while offset < max_host {
398 let candidate_u128 = net_u128 + offset;
399 if candidate_u128 != gateway_u128 {
400 let ip = std::net::Ipv6Addr::from(candidate_u128);
401 if !used.contains(&ip) {
402 return Ok(ip);
403 }
404 }
405 offset += 1;
406 }
407
408 Err("no available IPv6 addresses in subnet".to_string())
409 }
410}
411
412impl NetworkConfig {
413 pub fn new(name: &str, subnet: &str) -> Result<Self, String> {
415 let ipam = Ipam::new(subnet)?;
416
417 Ok(Self {
418 name: name.to_string(),
419 subnet: ipam.cidr(),
420 gateway: ipam.gateway(),
421 driver: "bridge".to_string(),
422 labels: HashMap::new(),
423 endpoints: HashMap::new(),
424 created_at: chrono::Utc::now().to_rfc3339(),
425 policy: NetworkPolicy::default(),
426 })
427 }
428
429 pub fn connect(&mut self, box_id: &str, box_name: &str) -> Result<NetworkEndpoint, String> {
431 if self.endpoints.contains_key(box_id) {
432 return Err(format!(
433 "box '{}' is already connected to network '{}'",
434 box_id, self.name
435 ));
436 }
437
438 let ipam = Ipam::new(&self.subnet)?;
439 let used: Vec<Ipv4Addr> = self.endpoints.values().map(|e| e.ip_address).collect();
440 let ip = ipam.allocate(&used)?;
441 let mac = Ipam::mac_from_ip(&ip);
442
443 let endpoint = NetworkEndpoint {
444 box_id: box_id.to_string(),
445 box_name: box_name.to_string(),
446 ip_address: ip,
447 mac_address: mac,
448 };
449
450 self.endpoints.insert(box_id.to_string(), endpoint.clone());
451 Ok(endpoint)
452 }
453
454 pub fn disconnect(&mut self, box_id: &str) -> Result<NetworkEndpoint, String> {
456 self.endpoints.remove(box_id).ok_or_else(|| {
457 format!(
458 "box '{}' is not connected to network '{}'",
459 box_id, self.name
460 )
461 })
462 }
463
464 pub fn set_policy(&mut self, policy: NetworkPolicy) -> Result<(), String> {
469 policy.validate()?;
470 self.policy = policy;
471 Ok(())
472 }
473
474 pub fn connected_boxes(&self) -> Vec<&NetworkEndpoint> {
476 self.endpoints.values().collect()
477 }
478
479 pub fn peer_endpoints(&self, exclude_box_id: &str) -> Vec<(String, String)> {
483 self.endpoints
484 .values()
485 .filter(|ep| ep.box_id != exclude_box_id)
486 .map(|ep| (ep.ip_address.to_string(), ep.box_name.clone()))
487 .collect()
488 }
489
490 pub fn allowed_peer_endpoints(&self, exclude_box_id: &str) -> Vec<(String, String)> {
495 let box_name = self
496 .endpoints
497 .get(exclude_box_id)
498 .map(|ep| ep.box_name.as_str())
499 .unwrap_or("");
500
501 let all_peers = self.peer_endpoints(exclude_box_id);
502 self.policy
503 .allowed_peers(box_name, &all_peers)
504 .into_iter()
505 .cloned()
506 .collect()
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
517 fn test_network_mode_default_is_tsi() {
518 let mode = NetworkMode::default();
519 assert_eq!(mode, NetworkMode::Tsi);
520 }
521
522 #[test]
523 fn test_network_mode_display() {
524 assert_eq!(NetworkMode::Tsi.to_string(), "tsi");
525 assert_eq!(NetworkMode::None.to_string(), "none");
526 assert_eq!(
527 NetworkMode::Bridge {
528 network: "mynet".to_string()
529 }
530 .to_string(),
531 "bridge:mynet"
532 );
533 }
534
535 #[test]
536 fn test_network_mode_serialization() {
537 let mode = NetworkMode::Bridge {
538 network: "test-net".to_string(),
539 };
540 let json = serde_json::to_string(&mode).unwrap();
541 let parsed: NetworkMode = serde_json::from_str(&json).unwrap();
542 assert_eq!(parsed, mode);
543 }
544
545 #[test]
546 fn test_network_mode_tsi_serialization() {
547 let mode = NetworkMode::Tsi;
548 let json = serde_json::to_string(&mode).unwrap();
549 let parsed: NetworkMode = serde_json::from_str(&json).unwrap();
550 assert_eq!(parsed, NetworkMode::Tsi);
551 }
552
553 #[test]
556 fn test_ipam_new_valid() {
557 let ipam = Ipam::new("10.88.0.0/24").unwrap();
558 assert_eq!(ipam.gateway(), Ipv4Addr::new(10, 88, 0, 1));
559 assert_eq!(ipam.cidr(), "10.88.0.0/24");
560 }
561
562 #[test]
563 fn test_ipam_new_slash16() {
564 let ipam = Ipam::new("172.20.0.0/16").unwrap();
565 assert_eq!(ipam.gateway(), Ipv4Addr::new(172, 20, 0, 1));
566 }
567
568 #[test]
569 fn test_ipam_invalid_cidr() {
570 assert!(Ipam::new("10.88.0.0").is_err());
571 assert!(Ipam::new("not-an-ip/24").is_err());
572 assert!(Ipam::new("10.88.0.0/33").is_err());
573 assert!(Ipam::new("10.88.0.0/31").is_err());
574 }
575
576 #[test]
577 fn test_ipam_broadcast() {
578 let ipam = Ipam::new("10.88.0.0/24").unwrap();
579 assert_eq!(ipam.broadcast(), Ipv4Addr::new(10, 88, 0, 255));
580
581 let ipam16 = Ipam::new("172.20.0.0/16").unwrap();
582 assert_eq!(ipam16.broadcast(), Ipv4Addr::new(172, 20, 255, 255));
583 }
584
585 #[test]
586 fn test_ipam_capacity() {
587 let ipam = Ipam::new("10.88.0.0/24").unwrap();
588 assert_eq!(ipam.capacity(), 253);
590
591 let ipam28 = Ipam::new("10.88.0.0/28").unwrap();
592 assert_eq!(ipam28.capacity(), 13);
594 }
595
596 #[test]
597 fn test_ipam_allocate_first() {
598 let ipam = Ipam::new("10.88.0.0/24").unwrap();
599 let ip = ipam.allocate(&[]).unwrap();
600 assert_eq!(ip, Ipv4Addr::new(10, 88, 0, 2));
602 }
603
604 #[test]
605 fn test_ipam_allocate_sequential() {
606 let ipam = Ipam::new("10.88.0.0/24").unwrap();
607 let ip1 = ipam.allocate(&[]).unwrap();
608 let ip2 = ipam.allocate(&[ip1]).unwrap();
609 let ip3 = ipam.allocate(&[ip1, ip2]).unwrap();
610
611 assert_eq!(ip1, Ipv4Addr::new(10, 88, 0, 2));
612 assert_eq!(ip2, Ipv4Addr::new(10, 88, 0, 3));
613 assert_eq!(ip3, Ipv4Addr::new(10, 88, 0, 4));
614 }
615
616 #[test]
617 fn test_ipam_allocate_skips_gateway() {
618 let ipam = Ipam::new("10.88.0.0/24").unwrap();
619 let ip = ipam.allocate(&[]).unwrap();
621 assert_ne!(ip, ipam.gateway());
622 }
623
624 #[test]
625 fn test_ipam_allocate_exhausted() {
626 let ipam = Ipam::new("10.88.0.0/30").unwrap();
627 let ip1 = ipam.allocate(&[]).unwrap();
630 assert_eq!(ip1, Ipv4Addr::new(10, 88, 0, 2));
631
632 let result = ipam.allocate(&[ip1]);
633 assert!(result.is_err());
634 }
635
636 #[test]
637 fn test_ipam_mac_from_ip() {
638 let ip = Ipv4Addr::new(10, 88, 0, 2);
639 assert_eq!(Ipam::mac_from_ip(&ip), "02:42:0a:58:00:02");
640
641 let ip2 = Ipv4Addr::new(192, 168, 1, 100);
642 assert_eq!(Ipam::mac_from_ip(&ip2), "02:42:c0:a8:01:64");
643 }
644
645 #[test]
648 fn test_network_config_new() {
649 let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
650 assert_eq!(net.name, "mynet");
651 assert_eq!(net.subnet, "10.88.0.0/24");
652 assert_eq!(net.gateway, Ipv4Addr::new(10, 88, 0, 1));
653 assert_eq!(net.driver, "bridge");
654 assert!(net.endpoints.is_empty());
655 }
656
657 #[test]
658 fn test_network_config_invalid_subnet() {
659 assert!(NetworkConfig::new("bad", "invalid").is_err());
660 }
661
662 #[test]
663 fn test_network_config_connect() {
664 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
665 let ep = net.connect("box-1", "web").unwrap();
666
667 assert_eq!(ep.box_id, "box-1");
668 assert_eq!(ep.box_name, "web");
669 assert_eq!(ep.ip_address, Ipv4Addr::new(10, 88, 0, 2));
670 assert_eq!(ep.mac_address, "02:42:0a:58:00:02");
671 assert_eq!(net.endpoints.len(), 1);
672 }
673
674 #[test]
675 fn test_network_config_connect_multiple() {
676 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
677 let ep1 = net.connect("box-1", "web").unwrap();
678 let ep2 = net.connect("box-2", "api").unwrap();
679
680 assert_eq!(ep1.ip_address, Ipv4Addr::new(10, 88, 0, 2));
681 assert_eq!(ep2.ip_address, Ipv4Addr::new(10, 88, 0, 3));
682 assert_eq!(net.endpoints.len(), 2);
683 }
684
685 #[test]
686 fn test_network_config_connect_duplicate() {
687 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
688 net.connect("box-1", "web").unwrap();
689 let result = net.connect("box-1", "web");
690 assert!(result.is_err());
691 }
692
693 #[test]
694 fn test_network_config_disconnect() {
695 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
696 net.connect("box-1", "web").unwrap();
697
698 let ep = net.disconnect("box-1").unwrap();
699 assert_eq!(ep.box_id, "box-1");
700 assert!(net.endpoints.is_empty());
701 }
702
703 #[test]
704 fn test_network_config_disconnect_not_connected() {
705 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
706 let result = net.disconnect("box-1");
707 assert!(result.is_err());
708 }
709
710 #[test]
711 fn test_network_config_connected_boxes() {
712 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
713 net.connect("box-1", "web").unwrap();
714 net.connect("box-2", "api").unwrap();
715
716 let boxes = net.connected_boxes();
717 assert_eq!(boxes.len(), 2);
718 }
719
720 #[test]
721 fn test_network_config_ip_reuse_after_disconnect() {
722 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
723 let ep1 = net.connect("box-1", "web").unwrap();
724 assert_eq!(ep1.ip_address, Ipv4Addr::new(10, 88, 0, 2));
725
726 net.disconnect("box-1").unwrap();
727
728 let ep2 = net.connect("box-2", "api").unwrap();
730 assert_eq!(ep2.ip_address, Ipv4Addr::new(10, 88, 0, 2));
731 }
732
733 #[test]
734 fn test_network_config_serialization() {
735 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
736 net.connect("box-1", "web").unwrap();
737
738 let json = serde_json::to_string(&net).unwrap();
739 let parsed: NetworkConfig = serde_json::from_str(&json).unwrap();
740
741 assert_eq!(parsed.name, "mynet");
742 assert_eq!(parsed.subnet, "10.88.0.0/24");
743 assert_eq!(parsed.endpoints.len(), 1);
744 assert!(parsed.endpoints.contains_key("box-1"));
745 }
746
747 #[test]
752 fn test_peer_endpoints_excludes_self() {
753 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
754 net.connect("box-1", "web").unwrap();
755 net.connect("box-2", "api").unwrap();
756 net.connect("box-3", "db").unwrap();
757
758 let peers = net.peer_endpoints("box-1");
759 assert_eq!(peers.len(), 2);
760 assert!(peers.iter().all(|(_, name)| name != "web"));
761 assert!(peers.iter().any(|(_, name)| name == "api"));
762 assert!(peers.iter().any(|(_, name)| name == "db"));
763 }
764
765 #[test]
766 fn test_peer_endpoints_empty_when_alone() {
767 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
768 net.connect("box-1", "web").unwrap();
769
770 let peers = net.peer_endpoints("box-1");
771 assert!(peers.is_empty());
772 }
773
774 #[test]
775 fn test_peer_endpoints_returns_all_others() {
776 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
777 net.connect("box-1", "web").unwrap();
778 net.connect("box-2", "api").unwrap();
779
780 let peers = net.peer_endpoints("box-1");
781 assert_eq!(peers.len(), 1);
782 assert_eq!(peers[0].0, "10.88.0.3");
783 assert_eq!(peers[0].1, "api");
784 }
785
786 #[test]
787 fn test_peer_endpoints_nonexistent_excludes_nothing() {
788 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
789 net.connect("box-1", "web").unwrap();
790 net.connect("box-2", "api").unwrap();
791
792 let peers = net.peer_endpoints("nonexistent");
793 assert_eq!(peers.len(), 2);
794 }
795
796 #[test]
799 fn test_network_endpoint_serialization() {
800 let ep = NetworkEndpoint {
801 box_id: "abc123".to_string(),
802 box_name: "web".to_string(),
803 ip_address: Ipv4Addr::new(10, 88, 0, 2),
804 mac_address: "02:42:0a:58:00:02".to_string(),
805 };
806
807 let json = serde_json::to_string(&ep).unwrap();
808 let parsed: NetworkEndpoint = serde_json::from_str(&json).unwrap();
809 assert_eq!(parsed, ep);
810 }
811
812 #[test]
815 fn test_network_policy_default_allows_all() {
816 let policy = NetworkPolicy::default();
817 assert_eq!(policy.isolation, IsolationMode::None);
818 assert!(policy.is_peer_allowed("web", "db"));
819 assert!(policy.is_peer_allowed("any", "any"));
820 }
821
822 #[test]
823 fn test_network_policy_strict_denies_all() {
824 let policy = NetworkPolicy {
825 isolation: IsolationMode::Strict,
826 ..Default::default()
827 };
828 assert!(!policy.is_peer_allowed("web", "db"));
829 assert!(!policy.is_peer_allowed("any", "any"));
830 }
831
832 #[test]
833 fn test_network_policy_custom_allow_rule() {
834 let policy = NetworkPolicy {
835 isolation: IsolationMode::Custom,
836 egress: vec![PolicyRule {
837 from: "web".to_string(),
838 to: "db".to_string(),
839 ports: vec![],
840 protocol: "any".to_string(),
841 action: PolicyAction::Allow,
842 }],
843 ..Default::default()
844 };
845 assert!(policy.is_peer_allowed("web", "db"));
846 assert!(!policy.is_peer_allowed("web", "redis")); assert!(!policy.is_peer_allowed("api", "db")); }
849
850 #[test]
851 fn test_network_policy_custom_wildcard_from() {
852 let policy = NetworkPolicy {
853 isolation: IsolationMode::Custom,
854 egress: vec![PolicyRule {
855 from: "*".to_string(),
856 to: "db".to_string(),
857 ports: vec![],
858 protocol: "any".to_string(),
859 action: PolicyAction::Allow,
860 }],
861 ..Default::default()
862 };
863 assert!(policy.is_peer_allowed("web", "db"));
864 assert!(policy.is_peer_allowed("api", "db"));
865 assert!(!policy.is_peer_allowed("web", "redis"));
866 }
867
868 #[test]
869 fn test_network_policy_custom_wildcard_to() {
870 let policy = NetworkPolicy {
871 isolation: IsolationMode::Custom,
872 egress: vec![PolicyRule {
873 from: "web".to_string(),
874 to: "*".to_string(),
875 ports: vec![],
876 protocol: "any".to_string(),
877 action: PolicyAction::Allow,
878 }],
879 ..Default::default()
880 };
881 assert!(policy.is_peer_allowed("web", "db"));
882 assert!(policy.is_peer_allowed("web", "redis"));
883 assert!(!policy.is_peer_allowed("api", "db"));
884 }
885
886 #[test]
887 fn test_network_policy_custom_deny_rule() {
888 let policy = NetworkPolicy {
889 isolation: IsolationMode::Custom,
890 egress: vec![
891 PolicyRule {
892 from: "web".to_string(),
893 to: "db".to_string(),
894 ports: vec![],
895 protocol: "any".to_string(),
896 action: PolicyAction::Deny,
897 },
898 PolicyRule {
899 from: "web".to_string(),
900 to: "*".to_string(),
901 ports: vec![],
902 protocol: "any".to_string(),
903 action: PolicyAction::Allow,
904 },
905 ],
906 ..Default::default()
907 };
908 assert!(!policy.is_peer_allowed("web", "db"));
910 assert!(policy.is_peer_allowed("web", "redis"));
912 }
913
914 #[test]
915 fn test_network_policy_custom_no_rules_denies() {
916 let policy = NetworkPolicy {
917 isolation: IsolationMode::Custom,
918 egress: vec![],
919 ..Default::default()
920 };
921 assert!(!policy.is_peer_allowed("web", "db"));
922 }
923
924 #[test]
925 fn test_network_policy_allowed_peers() {
926 let policy = NetworkPolicy {
927 isolation: IsolationMode::Custom,
928 egress: vec![PolicyRule {
929 from: "web".to_string(),
930 to: "db".to_string(),
931 ports: vec![],
932 protocol: "any".to_string(),
933 action: PolicyAction::Allow,
934 }],
935 ..Default::default()
936 };
937
938 let peers = vec![
939 ("10.88.0.3".to_string(), "db".to_string()),
940 ("10.88.0.4".to_string(), "redis".to_string()),
941 ];
942
943 let allowed = policy.allowed_peers("web", &peers);
944 assert_eq!(allowed.len(), 1);
945 assert_eq!(allowed[0].1, "db");
946 }
947
948 #[test]
949 fn test_network_policy_serde_roundtrip() {
950 let policy = NetworkPolicy {
951 isolation: IsolationMode::Custom,
952 egress: vec![PolicyRule {
953 from: "web".to_string(),
954 to: "db".to_string(),
955 ports: vec![5432],
956 protocol: "tcp".to_string(),
957 action: PolicyAction::Allow,
958 }],
959 ingress: vec![],
960 };
961 let json = serde_json::to_string(&policy).unwrap();
962 let parsed: NetworkPolicy = serde_json::from_str(&json).unwrap();
963 assert_eq!(parsed.isolation, IsolationMode::Custom);
964 assert_eq!(parsed.egress.len(), 1);
965 assert_eq!(parsed.egress[0].ports, vec![5432]);
966 }
967
968 #[test]
969 fn test_isolation_mode_serde() {
970 let modes = vec![
971 (IsolationMode::None, "\"none\""),
972 (IsolationMode::Strict, "\"strict\""),
973 (IsolationMode::Custom, "\"custom\""),
974 ];
975 for (mode, expected) in modes {
976 let json = serde_json::to_string(&mode).unwrap();
977 assert_eq!(json, expected);
978 let parsed: IsolationMode = serde_json::from_str(&json).unwrap();
979 assert_eq!(parsed, mode);
980 }
981 }
982
983 #[test]
984 fn test_allowed_peer_endpoints_none_policy() {
985 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
986 net.connect("box-1", "web").unwrap();
987 net.connect("box-2", "db").unwrap();
988 net.connect("box-3", "redis").unwrap();
989
990 let peers = net.allowed_peer_endpoints("box-1");
992 assert_eq!(peers.len(), 2);
993 }
994
995 #[test]
996 fn test_allowed_peer_endpoints_strict_policy() {
997 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
998 net.policy = NetworkPolicy {
999 isolation: IsolationMode::Strict,
1000 ..Default::default()
1001 };
1002 net.connect("box-1", "web").unwrap();
1003 net.connect("box-2", "db").unwrap();
1004
1005 let peers = net.allowed_peer_endpoints("box-1");
1006 assert!(peers.is_empty());
1007 }
1008
1009 #[test]
1010 fn test_allowed_peer_endpoints_custom_policy() {
1011 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
1012 net.policy = NetworkPolicy {
1013 isolation: IsolationMode::Custom,
1014 egress: vec![PolicyRule {
1015 from: "web".to_string(),
1016 to: "db".to_string(),
1017 ports: vec![],
1018 protocol: "any".to_string(),
1019 action: PolicyAction::Allow,
1020 }],
1021 ..Default::default()
1022 };
1023 net.connect("box-1", "web").unwrap();
1024 net.connect("box-2", "db").unwrap();
1025 net.connect("box-3", "redis").unwrap();
1026
1027 let peers = net.allowed_peer_endpoints("box-1");
1028 assert_eq!(peers.len(), 1);
1029 assert_eq!(peers[0].1, "db");
1030 }
1031
1032 #[test]
1033 fn test_matches_pattern() {
1034 assert!(matches_pattern("*", "anything"));
1035 assert!(matches_pattern("web", "web"));
1036 assert!(!matches_pattern("web", "api"));
1037 }
1038
1039 #[test]
1040 fn test_policy_action_default() {
1041 assert_eq!(PolicyAction::default(), PolicyAction::Allow);
1042 }
1043
1044 #[test]
1047 fn test_policy_validate_none_ok() {
1048 let policy = NetworkPolicy::default();
1049 assert!(policy.validate().is_ok());
1050 }
1051
1052 #[test]
1053 fn test_policy_validate_strict_rejected() {
1054 let policy = NetworkPolicy {
1055 isolation: IsolationMode::Strict,
1056 ..Default::default()
1057 };
1058 let err = policy.validate().unwrap_err();
1059 assert!(err.contains("strict"));
1060 assert!(err.contains("not yet enforced"));
1061 }
1062
1063 #[test]
1064 fn test_policy_validate_custom_rejected() {
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![],
1071 protocol: "any".to_string(),
1072 action: PolicyAction::Allow,
1073 }],
1074 ..Default::default()
1075 };
1076 let err = policy.validate().unwrap_err();
1077 assert!(err.contains("custom"));
1078 assert!(err.contains("not yet enforced"));
1079 }
1080
1081 #[test]
1084 fn test_set_policy_none_ok() {
1085 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
1086 assert!(net.set_policy(NetworkPolicy::default()).is_ok());
1087 }
1088
1089 #[test]
1090 fn test_set_policy_strict_rejected() {
1091 let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
1092 let result = net.set_policy(NetworkPolicy {
1093 isolation: IsolationMode::Strict,
1094 ..Default::default()
1095 });
1096 assert!(result.is_err());
1097 }
1098
1099 #[test]
1102 fn test_ipam6_new_valid() {
1103 let ipam = Ipam6::new("fd00::/64").unwrap();
1104 assert_eq!(
1105 ipam.gateway(),
1106 "fd00::1".parse::<std::net::Ipv6Addr>().unwrap()
1107 );
1108 assert_eq!(ipam.cidr(), "fd00::/64");
1109 }
1110
1111 #[test]
1112 fn test_ipam6_invalid_cidr() {
1113 assert!(Ipam6::new("fd00::").is_err());
1114 assert!(Ipam6::new("not-an-ip/64").is_err());
1115 assert!(Ipam6::new("fd00::/63").is_err()); assert!(Ipam6::new("fd00::/121").is_err()); }
1118
1119 #[test]
1120 fn test_ipam6_allocate_first() {
1121 let ipam = Ipam6::new("fd00::/64").unwrap();
1122 let ip = ipam.allocate(&[]).unwrap();
1123 assert_eq!(ip, "fd00::2".parse::<std::net::Ipv6Addr>().unwrap());
1124 }
1125
1126 #[test]
1127 fn test_ipam6_allocate_sequential() {
1128 let ipam = Ipam6::new("fd00::/64").unwrap();
1129 let ip1 = ipam.allocate(&[]).unwrap();
1130 let ip2 = ipam.allocate(&[ip1]).unwrap();
1131 let ip3 = ipam.allocate(&[ip1, ip2]).unwrap();
1132
1133 assert_eq!(ip1, "fd00::2".parse::<std::net::Ipv6Addr>().unwrap());
1134 assert_eq!(ip2, "fd00::3".parse::<std::net::Ipv6Addr>().unwrap());
1135 assert_eq!(ip3, "fd00::4".parse::<std::net::Ipv6Addr>().unwrap());
1136 }
1137
1138 #[test]
1139 fn test_ipam6_allocate_skips_gateway() {
1140 let ipam = Ipam6::new("fd00::/64").unwrap();
1141 let ip = ipam.allocate(&[]).unwrap();
1142 assert_ne!(ip, ipam.gateway());
1143 }
1144
1145 #[test]
1146 fn test_ipam6_slash120() {
1147 let ipam = Ipam6::new("fd00::/120").unwrap();
1148 let ip = ipam.allocate(&[]).unwrap();
1149 assert_eq!(ip, "fd00::2".parse::<std::net::Ipv6Addr>().unwrap());
1150 }
1151}