Skip to main content

a3s_box_core/
network.rs

1//! Network types for container-to-container communication.
2//!
3//! Provides network configuration, endpoint tracking, and IP address
4//! management (IPAM) for connecting boxes via passt-based virtio-net.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt;
9use std::net::Ipv4Addr;
10
11/// Network mode for a box.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
13#[serde(rename_all = "snake_case")]
14pub enum NetworkMode {
15    /// TSI mode (default) — no network interfaces, socket syscalls proxied via vsock.
16    #[default]
17    Tsi,
18
19    /// Bridge mode — real eth0 via passt, container-to-container communication.
20    Bridge {
21        /// Network name to join.
22        network: String,
23    },
24
25    /// No networking at all.
26    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/// Configuration for a user-defined network.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct NetworkConfig {
42    /// Network name (unique identifier).
43    pub name: String,
44
45    /// Subnet in CIDR notation (e.g., "10.88.0.0/24").
46    pub subnet: String,
47
48    /// Gateway IP address (e.g., "10.88.0.1").
49    pub gateway: Ipv4Addr,
50
51    /// Network driver (currently only "bridge" is supported).
52    #[serde(default = "default_driver")]
53    pub driver: String,
54
55    /// User-defined labels.
56    #[serde(default)]
57    pub labels: HashMap<String, String>,
58
59    /// Connected endpoints (box_id → endpoint).
60    #[serde(default)]
61    pub endpoints: HashMap<String, NetworkEndpoint>,
62
63    /// Creation timestamp (RFC 3339).
64    pub created_at: String,
65
66    /// Network isolation policy.
67    #[serde(default)]
68    pub policy: NetworkPolicy,
69}
70
71fn default_driver() -> String {
72    "bridge".to_string()
73}
74
75/// A box's connection to a network.
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct NetworkEndpoint {
78    /// Box ID.
79    pub box_id: String,
80
81    /// Box name (for DNS resolution).
82    pub box_name: String,
83
84    /// Assigned IPv4 address.
85    pub ip_address: Ipv4Addr,
86
87    /// Assigned MAC address (hex string, e.g., "02:42:0a:58:00:02").
88    pub mac_address: String,
89}
90
91/// Network isolation policy.
92///
93/// Controls which boxes can communicate with each other on a network.
94#[derive(Debug, Clone, Serialize, Deserialize, Default)]
95pub struct NetworkPolicy {
96    /// Isolation mode (default: None — all boxes can communicate).
97    #[serde(default)]
98    pub isolation: IsolationMode,
99
100    /// Ingress rules (who can receive traffic from whom).
101    /// Only used when isolation is `Custom`.
102    #[serde(default)]
103    pub ingress: Vec<PolicyRule>,
104
105    /// Egress rules (who can send traffic to whom).
106    /// Only used when isolation is `Custom`.
107    #[serde(default)]
108    pub egress: Vec<PolicyRule>,
109}
110
111/// Network isolation mode.
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
113#[serde(rename_all = "snake_case")]
114pub enum IsolationMode {
115    /// No isolation — all boxes on the network can communicate freely (default).
116    #[default]
117    None,
118    /// Strict isolation — no box-to-box communication allowed (only gateway/external).
119    Strict,
120    /// Custom rules — use ingress/egress rules to control traffic.
121    Custom,
122}
123
124/// A network policy rule that allows traffic between specific boxes or ports.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct PolicyRule {
127    /// Source box name pattern (e.g., "web", "*" for any).
128    #[serde(default = "wildcard")]
129    pub from: String,
130
131    /// Destination box name pattern (e.g., "db", "*" for any).
132    #[serde(default = "wildcard")]
133    pub to: String,
134
135    /// Allowed ports (empty = all ports).
136    #[serde(default)]
137    pub ports: Vec<u16>,
138
139    /// Protocol: "tcp", "udp", or "any" (default).
140    #[serde(default = "default_protocol")]
141    pub protocol: String,
142
143    /// Rule action: allow or deny.
144    #[serde(default)]
145    pub action: PolicyAction,
146}
147
148/// Policy rule action.
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
150#[serde(rename_all = "snake_case")]
151pub enum PolicyAction {
152    /// Allow the traffic (default).
153    #[default]
154    Allow,
155    /// Deny the traffic.
156    Deny,
157}
158
159fn wildcard() -> String {
160    "*".to_string()
161}
162
163fn default_protocol() -> String {
164    "any".to_string()
165}
166
167impl NetworkPolicy {
168    /// Validate that the policy can be enforced at runtime.
169    ///
170    /// Currently only `IsolationMode::None` is supported. Strict and Custom
171    /// modes require iptables/nftables integration which is not yet implemented.
172    /// Rejecting early prevents a false sense of security.
173    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    /// Check if a box is allowed to communicate with a peer.
190    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                // Check egress rules (from box_name to peer_name)
196                self.evaluate_rules(&self.egress, box_name, peer_name)
197            }
198        }
199    }
200
201    /// Evaluate a set of rules. Default-deny: if no rule matches, deny.
202    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        // No matching rule → deny in Custom mode
209        false
210    }
211
212    /// Get the list of allowed peers for a box, given all peer names.
213    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
225/// Simple wildcard pattern matching: "*" matches anything, otherwise exact match.
226fn matches_pattern(pattern: &str, name: &str) -> bool {
227    pattern == "*" || pattern == name
228}
229
230/// Simple sequential IPAM (IP Address Management) for a subnet.
231#[derive(Debug)]
232pub struct Ipam {
233    /// Network address (e.g., 10.88.0.0).
234    network: Ipv4Addr,
235    /// Prefix length (e.g., 24).
236    prefix_len: u8,
237    /// Gateway (first usable, e.g., 10.88.0.1).
238    gateway: Ipv4Addr,
239}
240
241impl Ipam {
242    /// Create a new IPAM from a CIDR string (e.g., "10.88.0.0/24").
243    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        // Gateway is network + 1
264        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    /// Get the gateway address.
275    pub fn gateway(&self) -> Ipv4Addr {
276        self.gateway
277    }
278
279    /// Get the subnet CIDR string.
280    pub fn cidr(&self) -> String {
281        format!("{}/{}", self.network, self.prefix_len)
282    }
283
284    /// Calculate the broadcast address.
285    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    /// Total number of usable host addresses (excluding network, gateway, broadcast).
293    pub fn capacity(&self) -> u32 {
294        let host_bits = 32 - self.prefix_len as u32;
295        let total = (1u32 << host_bits) - 1; // exclude network address
296        total.saturating_sub(2) // exclude gateway and broadcast
297    }
298
299    /// Allocate the next available IP, given a set of already-used IPs.
300    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        // Start from network + 2 (skip network and gateway)
306        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    /// Generate a deterministic MAC address from an IPv4 address.
321    /// Uses the locally-administered prefix 02:42 (same as Docker).
322    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/// Simple sequential IPAM for IPv6 subnets.
332///
333/// Supports /64 to /120 prefix lengths. Allocates addresses sequentially
334/// starting from network::1 (gateway) + 1.
335#[derive(Debug)]
336pub struct Ipam6 {
337    /// Network address (e.g., fd00::0).
338    network: std::net::Ipv6Addr,
339    /// Prefix length (e.g., 64).
340    prefix_len: u8,
341    /// Gateway (network::1).
342    gateway: std::net::Ipv6Addr,
343}
344
345impl Ipam6 {
346    /// Create a new IPv6 IPAM from a CIDR string (e.g., "fd00::/64").
347    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        // Gateway is network + 1
368        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    /// Get the gateway address.
379    pub fn gateway(&self) -> std::net::Ipv6Addr {
380        self.gateway
381    }
382
383    /// Get the subnet CIDR string.
384    pub fn cidr(&self) -> String {
385        format!("{}/{}", self.network, self.prefix_len)
386    }
387
388    /// Allocate the next available IPv6 address, given a set of already-used IPs.
389    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; // broadcast equivalent
393        let gateway_u128 = u128::from(self.gateway);
394
395        // Start from network + 2 (skip network and gateway)
396        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    /// Create a new network with the given name and subnet.
414    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    /// Allocate an IP and register a new endpoint for a box.
430    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    /// Remove a box from this network.
455    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    /// Set the network policy, validating that it can be enforced.
465    ///
466    /// Returns an error if the policy uses an isolation mode that is not
467    /// yet implemented at the packet-filtering level.
468    pub fn set_policy(&mut self, policy: NetworkPolicy) -> Result<(), String> {
469        policy.validate()?;
470        self.policy = policy;
471        Ok(())
472    }
473
474    /// Get all connected endpoints.
475    pub fn connected_boxes(&self) -> Vec<&NetworkEndpoint> {
476        self.endpoints.values().collect()
477    }
478
479    /// Get peer endpoints for DNS discovery (all endpoints except the given box).
480    ///
481    /// Returns `(ip_address, box_name)` pairs for all endpoints other than `exclude_box_id`.
482    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    /// Get peer endpoints filtered by the network's isolation policy.
491    ///
492    /// Like `peer_endpoints`, but only returns peers that the given box
493    /// is allowed to communicate with according to the network policy.
494    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    // --- NetworkMode tests ---
515
516    #[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    // --- IPAM tests ---
554
555    #[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        // /24 = 256 total, minus network(1) minus gateway(1) minus broadcast(1) = 253
589        assert_eq!(ipam.capacity(), 253);
590
591        let ipam28 = Ipam::new("10.88.0.0/28").unwrap();
592        // /28 = 16 total, minus network(1) = 15, minus gateway(1) minus broadcast(1) = 13
593        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        // First allocation: network+2 (skip network and gateway)
601        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        // Gateway is 10.88.0.1, first alloc should be .2
620        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        // /30 = 4 total: .0 (network), .1 (gateway), .2 (host), .3 (broadcast)
628        // Only 1 usable host
629        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    // --- NetworkConfig tests ---
646
647    #[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        // After disconnect, the IP should be reusable
729        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    // --- NetworkEndpoint tests ---
748
749    // --- peer_endpoints tests ---
750
751    #[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    // --- NetworkEndpoint tests ---
797
798    #[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    // --- NetworkPolicy tests ---
813
814    #[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")); // no rule → deny
847        assert!(!policy.is_peer_allowed("api", "db")); // from doesn't match
848    }
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        // First matching rule wins: web→db is denied
909        assert!(!policy.is_peer_allowed("web", "db"));
910        // web→redis matches the wildcard allow
911        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        // Default policy (None) → all peers visible
991        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    // --- NetworkPolicy::validate tests ---
1045
1046    #[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    // --- NetworkConfig::set_policy tests ---
1082
1083    #[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    // --- Ipam6 tests ---
1100
1101    #[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()); // below 64
1116        assert!(Ipam6::new("fd00::/121").is_err()); // above 120
1117    }
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}