use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::net::Ipv4Addr;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum NetworkMode {
#[default]
Tsi,
Bridge {
network: String,
},
None,
}
impl fmt::Display for NetworkMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NetworkMode::Tsi => write!(f, "tsi"),
NetworkMode::Bridge { network } => write!(f, "bridge:{}", network),
NetworkMode::None => write!(f, "none"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
pub name: String,
pub subnet: String,
pub gateway: Ipv4Addr,
#[serde(default = "default_driver")]
pub driver: String,
#[serde(default)]
pub labels: HashMap<String, String>,
#[serde(default)]
pub endpoints: HashMap<String, NetworkEndpoint>,
pub created_at: String,
#[serde(default)]
pub policy: NetworkPolicy,
}
fn default_driver() -> String {
"bridge".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NetworkEndpoint {
pub box_id: String,
pub box_name: String,
#[serde(default)]
pub aliases: Vec<String>,
pub ip_address: Ipv4Addr,
pub mac_address: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NetworkPolicy {
#[serde(default)]
pub isolation: IsolationMode,
#[serde(default)]
pub ingress: Vec<PolicyRule>,
#[serde(default)]
pub egress: Vec<PolicyRule>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum IsolationMode {
#[default]
None,
Strict,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyRule {
#[serde(default = "wildcard")]
pub from: String,
#[serde(default = "wildcard")]
pub to: String,
#[serde(default)]
pub ports: Vec<u16>,
#[serde(default = "default_protocol")]
pub protocol: String,
#[serde(default)]
pub action: PolicyAction,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PolicyAction {
#[default]
Allow,
Deny,
}
fn wildcard() -> String {
"*".to_string()
}
fn default_protocol() -> String {
"any".to_string()
}
impl NetworkPolicy {
pub fn validate(&self) -> Result<(), String> {
match self.isolation {
IsolationMode::None => Ok(()),
IsolationMode::Strict => Err(
"network policy isolation mode 'strict' is not yet enforced at runtime; \
packets will NOT be filtered. Remove the policy or use isolation=none"
.to_string(),
),
IsolationMode::Custom => Err(
"network policy isolation mode 'custom' is not yet enforced at runtime; \
ingress/egress rules will NOT be applied. Remove the policy or use isolation=none"
.to_string(),
),
}
}
pub fn is_peer_allowed(&self, box_name: &str, peer_name: &str) -> bool {
match self.isolation {
IsolationMode::None => true,
IsolationMode::Strict => false,
IsolationMode::Custom => {
self.evaluate_rules(&self.egress, box_name, peer_name)
}
}
}
fn evaluate_rules(&self, rules: &[PolicyRule], from: &str, to: &str) -> bool {
for rule in rules {
if matches_pattern(&rule.from, from) && matches_pattern(&rule.to, to) {
return rule.action == PolicyAction::Allow;
}
}
false
}
pub fn allowed_peers<'a>(
&self,
box_name: &str,
peers: &'a [(String, String)],
) -> Vec<&'a (String, String)> {
peers
.iter()
.filter(|(_, peer_name)| self.is_peer_allowed(box_name, peer_name))
.collect()
}
}
fn matches_pattern(pattern: &str, name: &str) -> bool {
pattern == "*" || pattern == name
}
#[derive(Debug)]
pub struct Ipam {
network: Ipv4Addr,
prefix_len: u8,
gateway: Ipv4Addr,
}
impl Ipam {
pub fn new(cidr: &str) -> Result<Self, String> {
let parts: Vec<&str> = cidr.split('/').collect();
if parts.len() != 2 {
return Err(format!("invalid CIDR notation: {}", cidr));
}
let network: Ipv4Addr = parts[0]
.parse()
.map_err(|e| format!("invalid network address '{}': {}", parts[0], e))?;
let prefix_len: u8 = parts[1]
.parse()
.map_err(|e| format!("invalid prefix length '{}': {}", parts[1], e))?;
if prefix_len == 0 || prefix_len > 30 {
return Err(format!(
"prefix length {} out of range (must be 1-30 for a usable subnet)",
prefix_len
));
}
let net_u32 = u32::from(network);
let gateway =
Ipv4Addr::from(net_u32.checked_add(1).ok_or_else(|| {
format!("network address '{}' has no room for a gateway", network)
})?);
Ok(Self {
network,
prefix_len,
gateway,
})
}
pub fn gateway(&self) -> Ipv4Addr {
self.gateway
}
pub fn cidr(&self) -> String {
format!("{}/{}", self.network, self.prefix_len)
}
pub fn broadcast(&self) -> Ipv4Addr {
let net_u32 = u32::from(self.network);
let host_bits = 32 - self.prefix_len as u32;
let broadcast = net_u32 | ((1u32 << host_bits) - 1);
Ipv4Addr::from(broadcast)
}
pub fn capacity(&self) -> u32 {
let host_bits = 32 - self.prefix_len as u32;
let total = (1u32 << host_bits) - 1; total.saturating_sub(2) }
pub fn allocate(&self, used: &[Ipv4Addr]) -> Result<Ipv4Addr, String> {
let net_u32 = u32::from(self.network);
let broadcast_u32 = u32::from(self.broadcast());
let gateway_u32 = u32::from(self.gateway);
let mut candidate = match net_u32.checked_add(2) {
Some(c) => c,
None => return Err("no available IP addresses in subnet".to_string()),
};
while candidate < broadcast_u32 {
if candidate != gateway_u32 {
let ip = Ipv4Addr::from(candidate);
if !used.contains(&ip) {
return Ok(ip);
}
}
candidate = match candidate.checked_add(1) {
Some(c) => c,
None => break,
};
}
Err("no available IP addresses in subnet".to_string())
}
pub fn mac_from_ip(ip: &Ipv4Addr) -> String {
let octets = ip.octets();
format!(
"02:42:{:02x}:{:02x}:{:02x}:{:02x}",
octets[0], octets[1], octets[2], octets[3]
)
}
}
#[derive(Debug)]
pub struct Ipam6 {
network: std::net::Ipv6Addr,
prefix_len: u8,
gateway: std::net::Ipv6Addr,
}
impl Ipam6 {
pub fn new(cidr: &str) -> Result<Self, String> {
let parts: Vec<&str> = cidr.split('/').collect();
if parts.len() != 2 {
return Err(format!("invalid IPv6 CIDR notation: {}", cidr));
}
let network: std::net::Ipv6Addr = parts[0]
.parse()
.map_err(|e| format!("invalid IPv6 network address '{}': {}", parts[0], e))?;
let prefix_len: u8 = parts[1]
.parse()
.map_err(|e| format!("invalid prefix length '{}': {}", parts[1], e))?;
if !(64..=120).contains(&prefix_len) {
return Err(format!(
"IPv6 prefix length {} out of range (64..=120)",
prefix_len
));
}
let net_u128 = u128::from(network);
let gateway = std::net::Ipv6Addr::from(net_u128 + 1);
Ok(Self {
network,
prefix_len,
gateway,
})
}
pub fn gateway(&self) -> std::net::Ipv6Addr {
self.gateway
}
pub fn cidr(&self) -> String {
format!("{}/{}", self.network, self.prefix_len)
}
pub fn allocate(&self, used: &[std::net::Ipv6Addr]) -> Result<std::net::Ipv6Addr, String> {
let net_u128 = u128::from(self.network);
let host_bits = 128 - self.prefix_len as u32;
let max_host = (1u128 << host_bits) - 1; let gateway_u128 = u128::from(self.gateway);
let mut offset = 2u128;
while offset < max_host {
let candidate_u128 = net_u128 + offset;
if candidate_u128 != gateway_u128 {
let ip = std::net::Ipv6Addr::from(candidate_u128);
if !used.contains(&ip) {
return Ok(ip);
}
}
offset += 1;
}
Err("no available IPv6 addresses in subnet".to_string())
}
}
impl NetworkConfig {
pub fn new(name: &str, subnet: &str) -> Result<Self, String> {
let ipam = Ipam::new(subnet)?;
Ok(Self {
name: name.to_string(),
subnet: ipam.cidr(),
gateway: ipam.gateway(),
driver: "bridge".to_string(),
labels: HashMap::new(),
endpoints: HashMap::new(),
created_at: chrono::Utc::now().to_rfc3339(),
policy: NetworkPolicy::default(),
})
}
pub fn connect(&mut self, box_id: &str, box_name: &str) -> Result<NetworkEndpoint, String> {
self.connect_with_aliases(box_id, box_name, &[])
}
pub fn connect_with_aliases(
&mut self,
box_id: &str,
box_name: &str,
aliases: &[String],
) -> Result<NetworkEndpoint, String> {
if self.endpoints.contains_key(box_id) {
return Err(format!(
"box '{}' is already connected to network '{}'",
box_id, self.name
));
}
let ipam = Ipam::new(&self.subnet)?;
let used: Vec<Ipv4Addr> = self.endpoints.values().map(|e| e.ip_address).collect();
let ip = ipam.allocate(&used)?;
let mac = Ipam::mac_from_ip(&ip);
let endpoint = NetworkEndpoint {
box_id: box_id.to_string(),
box_name: box_name.to_string(),
aliases: aliases
.iter()
.filter(|a| !a.is_empty() && *a != box_name)
.cloned()
.collect(),
ip_address: ip,
mac_address: mac,
};
self.endpoints.insert(box_id.to_string(), endpoint.clone());
Ok(endpoint)
}
pub fn disconnect(&mut self, box_id: &str) -> Result<NetworkEndpoint, String> {
self.endpoints.remove(box_id).ok_or_else(|| {
format!(
"box '{}' is not connected to network '{}'",
box_id, self.name
)
})
}
pub fn set_policy(&mut self, policy: NetworkPolicy) -> Result<(), String> {
policy.validate()?;
self.policy = policy;
Ok(())
}
pub fn connected_boxes(&self) -> Vec<&NetworkEndpoint> {
self.endpoints.values().collect()
}
pub fn peer_endpoints(&self, exclude_box_id: &str) -> Vec<(String, String)> {
self.endpoints
.values()
.filter(|ep| ep.box_id != exclude_box_id)
.flat_map(|ep| {
let ip = ep.ip_address.to_string();
std::iter::once((ip.clone(), ep.box_name.clone()))
.chain(ep.aliases.iter().map(move |a| (ip.clone(), a.clone())))
})
.collect()
}
pub fn allowed_peer_endpoints(&self, exclude_box_id: &str) -> Vec<(String, String)> {
let box_name = self
.endpoints
.get(exclude_box_id)
.map(|ep| ep.box_name.as_str())
.unwrap_or("");
let all_peers = self.peer_endpoints(exclude_box_id);
self.policy
.allowed_peers(box_name, &all_peers)
.into_iter()
.cloned()
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_network_mode_default_is_tsi() {
let mode = NetworkMode::default();
assert_eq!(mode, NetworkMode::Tsi);
}
#[test]
fn test_network_mode_display() {
assert_eq!(NetworkMode::Tsi.to_string(), "tsi");
assert_eq!(NetworkMode::None.to_string(), "none");
assert_eq!(
NetworkMode::Bridge {
network: "mynet".to_string()
}
.to_string(),
"bridge:mynet"
);
}
#[test]
fn test_network_mode_serialization() {
let mode = NetworkMode::Bridge {
network: "test-net".to_string(),
};
let json = serde_json::to_string(&mode).unwrap();
let parsed: NetworkMode = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, mode);
}
#[test]
fn test_network_mode_tsi_serialization() {
let mode = NetworkMode::Tsi;
let json = serde_json::to_string(&mode).unwrap();
let parsed: NetworkMode = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, NetworkMode::Tsi);
}
#[test]
fn test_ipam_new_valid() {
let ipam = Ipam::new("10.88.0.0/24").unwrap();
assert_eq!(ipam.gateway(), Ipv4Addr::new(10, 88, 0, 1));
assert_eq!(ipam.cidr(), "10.88.0.0/24");
}
#[test]
fn test_ipam_new_slash16() {
let ipam = Ipam::new("172.20.0.0/16").unwrap();
assert_eq!(ipam.gateway(), Ipv4Addr::new(172, 20, 0, 1));
}
#[test]
fn test_ipam_invalid_cidr() {
assert!(Ipam::new("10.88.0.0").is_err());
assert!(Ipam::new("not-an-ip/24").is_err());
assert!(Ipam::new("10.88.0.0/33").is_err());
assert!(Ipam::new("10.88.0.0/31").is_err());
}
#[test]
fn test_ipam_broadcast() {
let ipam = Ipam::new("10.88.0.0/24").unwrap();
assert_eq!(ipam.broadcast(), Ipv4Addr::new(10, 88, 0, 255));
let ipam16 = Ipam::new("172.20.0.0/16").unwrap();
assert_eq!(ipam16.broadcast(), Ipv4Addr::new(172, 20, 255, 255));
}
#[test]
fn test_ipam_rejects_zero_and_oversized_prefix() {
assert!(Ipam::new("0.0.0.0/0").is_err());
assert!(Ipam::new("10.0.0.0/31").is_err());
assert!(Ipam::new("10.0.0.0/32").is_err());
assert!(Ipam::new("10.0.0.0/1").is_ok());
assert!(Ipam::new("10.0.0.0/30").is_ok());
}
#[test]
fn test_ipam_gateway_overflow_is_rejected_not_panic() {
assert!(Ipam::new("255.255.255.255/30").is_err());
}
#[test]
fn test_ipam_capacity() {
let ipam = Ipam::new("10.88.0.0/24").unwrap();
assert_eq!(ipam.capacity(), 253);
let ipam28 = Ipam::new("10.88.0.0/28").unwrap();
assert_eq!(ipam28.capacity(), 13);
}
#[test]
fn test_ipam_allocate_first() {
let ipam = Ipam::new("10.88.0.0/24").unwrap();
let ip = ipam.allocate(&[]).unwrap();
assert_eq!(ip, Ipv4Addr::new(10, 88, 0, 2));
}
#[test]
fn test_ipam_allocate_sequential() {
let ipam = Ipam::new("10.88.0.0/24").unwrap();
let ip1 = ipam.allocate(&[]).unwrap();
let ip2 = ipam.allocate(&[ip1]).unwrap();
let ip3 = ipam.allocate(&[ip1, ip2]).unwrap();
assert_eq!(ip1, Ipv4Addr::new(10, 88, 0, 2));
assert_eq!(ip2, Ipv4Addr::new(10, 88, 0, 3));
assert_eq!(ip3, Ipv4Addr::new(10, 88, 0, 4));
}
#[test]
fn test_ipam_allocate_skips_gateway() {
let ipam = Ipam::new("10.88.0.0/24").unwrap();
let ip = ipam.allocate(&[]).unwrap();
assert_ne!(ip, ipam.gateway());
}
#[test]
fn test_ipam_allocate_exhausted() {
let ipam = Ipam::new("10.88.0.0/30").unwrap();
let ip1 = ipam.allocate(&[]).unwrap();
assert_eq!(ip1, Ipv4Addr::new(10, 88, 0, 2));
let result = ipam.allocate(&[ip1]);
assert!(result.is_err());
}
#[test]
fn test_ipam_allocate_top_of_range_no_overflow() {
let ipam = Ipam::new("255.255.255.252/30").unwrap();
let ip1 = ipam.allocate(&[]).unwrap();
assert_eq!(ip1, Ipv4Addr::new(255, 255, 255, 254));
assert!(ipam.allocate(&[ip1]).is_err());
}
#[test]
fn test_ipam_mac_from_ip() {
let ip = Ipv4Addr::new(10, 88, 0, 2);
assert_eq!(Ipam::mac_from_ip(&ip), "02:42:0a:58:00:02");
let ip2 = Ipv4Addr::new(192, 168, 1, 100);
assert_eq!(Ipam::mac_from_ip(&ip2), "02:42:c0:a8:01:64");
}
#[test]
fn test_network_config_new() {
let net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
assert_eq!(net.name, "mynet");
assert_eq!(net.subnet, "10.88.0.0/24");
assert_eq!(net.gateway, Ipv4Addr::new(10, 88, 0, 1));
assert_eq!(net.driver, "bridge");
assert!(net.endpoints.is_empty());
}
#[test]
fn test_network_config_invalid_subnet() {
assert!(NetworkConfig::new("bad", "invalid").is_err());
}
#[test]
fn test_network_config_connect() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
let ep = net.connect("box-1", "web").unwrap();
assert_eq!(ep.box_id, "box-1");
assert_eq!(ep.box_name, "web");
assert_eq!(ep.ip_address, Ipv4Addr::new(10, 88, 0, 2));
assert_eq!(ep.mac_address, "02:42:0a:58:00:02");
assert_eq!(net.endpoints.len(), 1);
}
#[test]
fn test_network_config_connect_multiple() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
let ep1 = net.connect("box-1", "web").unwrap();
let ep2 = net.connect("box-2", "api").unwrap();
assert_eq!(ep1.ip_address, Ipv4Addr::new(10, 88, 0, 2));
assert_eq!(ep2.ip_address, Ipv4Addr::new(10, 88, 0, 3));
assert_eq!(net.endpoints.len(), 2);
}
#[test]
fn test_network_config_connect_duplicate() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.connect("box-1", "web").unwrap();
let result = net.connect("box-1", "web");
assert!(result.is_err());
}
#[test]
fn test_network_config_disconnect() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.connect("box-1", "web").unwrap();
let ep = net.disconnect("box-1").unwrap();
assert_eq!(ep.box_id, "box-1");
assert!(net.endpoints.is_empty());
}
#[test]
fn test_network_config_disconnect_not_connected() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
let result = net.disconnect("box-1");
assert!(result.is_err());
}
#[test]
fn test_network_config_connected_boxes() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.connect("box-1", "web").unwrap();
net.connect("box-2", "api").unwrap();
let boxes = net.connected_boxes();
assert_eq!(boxes.len(), 2);
}
#[test]
fn test_network_config_ip_reuse_after_disconnect() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
let ep1 = net.connect("box-1", "web").unwrap();
assert_eq!(ep1.ip_address, Ipv4Addr::new(10, 88, 0, 2));
net.disconnect("box-1").unwrap();
let ep2 = net.connect("box-2", "api").unwrap();
assert_eq!(ep2.ip_address, Ipv4Addr::new(10, 88, 0, 2));
}
#[test]
fn test_network_config_serialization() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.connect("box-1", "web").unwrap();
let json = serde_json::to_string(&net).unwrap();
let parsed: NetworkConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, "mynet");
assert_eq!(parsed.subnet, "10.88.0.0/24");
assert_eq!(parsed.endpoints.len(), 1);
assert!(parsed.endpoints.contains_key("box-1"));
}
#[test]
fn test_peer_endpoints_excludes_self() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.connect("box-1", "web").unwrap();
net.connect("box-2", "api").unwrap();
net.connect("box-3", "db").unwrap();
let peers = net.peer_endpoints("box-1");
assert_eq!(peers.len(), 2);
assert!(peers.iter().all(|(_, name)| name != "web"));
assert!(peers.iter().any(|(_, name)| name == "api"));
assert!(peers.iter().any(|(_, name)| name == "db"));
}
#[test]
fn test_connect_with_aliases_resolvable_as_peer() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
let ep = net
.connect_with_aliases("box-db", "proj-db", &["db".to_string()])
.unwrap();
assert_eq!(ep.aliases, vec!["db".to_string()]);
net.connect("box-web", "proj-web").unwrap();
let peers = net.peer_endpoints("box-web");
let db_ip = ep.ip_address.to_string();
assert!(peers
.iter()
.any(|(ip, name)| ip == &db_ip && name == "proj-db"));
assert!(peers.iter().any(|(ip, name)| ip == &db_ip && name == "db"));
}
#[test]
fn test_connect_alias_skips_empty_and_self_duplicate() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
let ep = net
.connect_with_aliases("b", "web", &["".to_string(), "web".to_string()])
.unwrap();
assert!(ep.aliases.is_empty());
}
#[test]
fn test_peer_endpoints_empty_when_alone() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.connect("box-1", "web").unwrap();
let peers = net.peer_endpoints("box-1");
assert!(peers.is_empty());
}
#[test]
fn test_peer_endpoints_returns_all_others() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.connect("box-1", "web").unwrap();
net.connect("box-2", "api").unwrap();
let peers = net.peer_endpoints("box-1");
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].0, "10.88.0.3");
assert_eq!(peers[0].1, "api");
}
#[test]
fn test_peer_endpoints_nonexistent_excludes_nothing() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.connect("box-1", "web").unwrap();
net.connect("box-2", "api").unwrap();
let peers = net.peer_endpoints("nonexistent");
assert_eq!(peers.len(), 2);
}
#[test]
fn test_network_endpoint_serialization() {
let ep = NetworkEndpoint {
box_id: "abc123".to_string(),
box_name: "web".to_string(),
aliases: vec!["app".to_string()],
ip_address: Ipv4Addr::new(10, 88, 0, 2),
mac_address: "02:42:0a:58:00:02".to_string(),
};
let json = serde_json::to_string(&ep).unwrap();
let parsed: NetworkEndpoint = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, ep);
let legacy = r#"{"box_id":"x","box_name":"web","ip_address":"10.88.0.3","mac_address":"02:42:0a:58:00:03"}"#;
let parsed_legacy: NetworkEndpoint = serde_json::from_str(legacy).unwrap();
assert!(parsed_legacy.aliases.is_empty());
}
#[test]
fn test_network_policy_default_allows_all() {
let policy = NetworkPolicy::default();
assert_eq!(policy.isolation, IsolationMode::None);
assert!(policy.is_peer_allowed("web", "db"));
assert!(policy.is_peer_allowed("any", "any"));
}
#[test]
fn test_network_policy_strict_denies_all() {
let policy = NetworkPolicy {
isolation: IsolationMode::Strict,
..Default::default()
};
assert!(!policy.is_peer_allowed("web", "db"));
assert!(!policy.is_peer_allowed("any", "any"));
}
#[test]
fn test_network_policy_custom_allow_rule() {
let policy = NetworkPolicy {
isolation: IsolationMode::Custom,
egress: vec![PolicyRule {
from: "web".to_string(),
to: "db".to_string(),
ports: vec![],
protocol: "any".to_string(),
action: PolicyAction::Allow,
}],
..Default::default()
};
assert!(policy.is_peer_allowed("web", "db"));
assert!(!policy.is_peer_allowed("web", "redis")); assert!(!policy.is_peer_allowed("api", "db")); }
#[test]
fn test_network_policy_custom_wildcard_from() {
let policy = NetworkPolicy {
isolation: IsolationMode::Custom,
egress: vec![PolicyRule {
from: "*".to_string(),
to: "db".to_string(),
ports: vec![],
protocol: "any".to_string(),
action: PolicyAction::Allow,
}],
..Default::default()
};
assert!(policy.is_peer_allowed("web", "db"));
assert!(policy.is_peer_allowed("api", "db"));
assert!(!policy.is_peer_allowed("web", "redis"));
}
#[test]
fn test_network_policy_custom_wildcard_to() {
let policy = NetworkPolicy {
isolation: IsolationMode::Custom,
egress: vec![PolicyRule {
from: "web".to_string(),
to: "*".to_string(),
ports: vec![],
protocol: "any".to_string(),
action: PolicyAction::Allow,
}],
..Default::default()
};
assert!(policy.is_peer_allowed("web", "db"));
assert!(policy.is_peer_allowed("web", "redis"));
assert!(!policy.is_peer_allowed("api", "db"));
}
#[test]
fn test_network_policy_custom_deny_rule() {
let policy = NetworkPolicy {
isolation: IsolationMode::Custom,
egress: vec![
PolicyRule {
from: "web".to_string(),
to: "db".to_string(),
ports: vec![],
protocol: "any".to_string(),
action: PolicyAction::Deny,
},
PolicyRule {
from: "web".to_string(),
to: "*".to_string(),
ports: vec![],
protocol: "any".to_string(),
action: PolicyAction::Allow,
},
],
..Default::default()
};
assert!(!policy.is_peer_allowed("web", "db"));
assert!(policy.is_peer_allowed("web", "redis"));
}
#[test]
fn test_network_policy_custom_no_rules_denies() {
let policy = NetworkPolicy {
isolation: IsolationMode::Custom,
egress: vec![],
..Default::default()
};
assert!(!policy.is_peer_allowed("web", "db"));
}
#[test]
fn test_network_policy_allowed_peers() {
let policy = NetworkPolicy {
isolation: IsolationMode::Custom,
egress: vec![PolicyRule {
from: "web".to_string(),
to: "db".to_string(),
ports: vec![],
protocol: "any".to_string(),
action: PolicyAction::Allow,
}],
..Default::default()
};
let peers = vec![
("10.88.0.3".to_string(), "db".to_string()),
("10.88.0.4".to_string(), "redis".to_string()),
];
let allowed = policy.allowed_peers("web", &peers);
assert_eq!(allowed.len(), 1);
assert_eq!(allowed[0].1, "db");
}
#[test]
fn test_network_policy_serde_roundtrip() {
let policy = NetworkPolicy {
isolation: IsolationMode::Custom,
egress: vec![PolicyRule {
from: "web".to_string(),
to: "db".to_string(),
ports: vec![5432],
protocol: "tcp".to_string(),
action: PolicyAction::Allow,
}],
ingress: vec![],
};
let json = serde_json::to_string(&policy).unwrap();
let parsed: NetworkPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.isolation, IsolationMode::Custom);
assert_eq!(parsed.egress.len(), 1);
assert_eq!(parsed.egress[0].ports, vec![5432]);
}
#[test]
fn test_isolation_mode_serde() {
let modes = vec![
(IsolationMode::None, "\"none\""),
(IsolationMode::Strict, "\"strict\""),
(IsolationMode::Custom, "\"custom\""),
];
for (mode, expected) in modes {
let json = serde_json::to_string(&mode).unwrap();
assert_eq!(json, expected);
let parsed: IsolationMode = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, mode);
}
}
#[test]
fn test_allowed_peer_endpoints_none_policy() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.connect("box-1", "web").unwrap();
net.connect("box-2", "db").unwrap();
net.connect("box-3", "redis").unwrap();
let peers = net.allowed_peer_endpoints("box-1");
assert_eq!(peers.len(), 2);
}
#[test]
fn test_allowed_peer_endpoints_strict_policy() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.policy = NetworkPolicy {
isolation: IsolationMode::Strict,
..Default::default()
};
net.connect("box-1", "web").unwrap();
net.connect("box-2", "db").unwrap();
let peers = net.allowed_peer_endpoints("box-1");
assert!(peers.is_empty());
}
#[test]
fn test_allowed_peer_endpoints_custom_policy() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
net.policy = NetworkPolicy {
isolation: IsolationMode::Custom,
egress: vec![PolicyRule {
from: "web".to_string(),
to: "db".to_string(),
ports: vec![],
protocol: "any".to_string(),
action: PolicyAction::Allow,
}],
..Default::default()
};
net.connect("box-1", "web").unwrap();
net.connect("box-2", "db").unwrap();
net.connect("box-3", "redis").unwrap();
let peers = net.allowed_peer_endpoints("box-1");
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].1, "db");
}
#[test]
fn test_matches_pattern() {
assert!(matches_pattern("*", "anything"));
assert!(matches_pattern("web", "web"));
assert!(!matches_pattern("web", "api"));
}
#[test]
fn test_policy_action_default() {
assert_eq!(PolicyAction::default(), PolicyAction::Allow);
}
#[test]
fn test_policy_validate_none_ok() {
let policy = NetworkPolicy::default();
assert!(policy.validate().is_ok());
}
#[test]
fn test_policy_validate_strict_rejected() {
let policy = NetworkPolicy {
isolation: IsolationMode::Strict,
..Default::default()
};
let err = policy.validate().unwrap_err();
assert!(err.contains("strict"));
assert!(err.contains("not yet enforced"));
}
#[test]
fn test_policy_validate_custom_rejected() {
let policy = NetworkPolicy {
isolation: IsolationMode::Custom,
egress: vec![PolicyRule {
from: "web".to_string(),
to: "db".to_string(),
ports: vec![],
protocol: "any".to_string(),
action: PolicyAction::Allow,
}],
..Default::default()
};
let err = policy.validate().unwrap_err();
assert!(err.contains("custom"));
assert!(err.contains("not yet enforced"));
}
#[test]
fn test_set_policy_none_ok() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
assert!(net.set_policy(NetworkPolicy::default()).is_ok());
}
#[test]
fn test_set_policy_strict_rejected() {
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
let result = net.set_policy(NetworkPolicy {
isolation: IsolationMode::Strict,
..Default::default()
});
assert!(result.is_err());
}
#[test]
fn test_ipam6_new_valid() {
let ipam = Ipam6::new("fd00::/64").unwrap();
assert_eq!(
ipam.gateway(),
"fd00::1".parse::<std::net::Ipv6Addr>().unwrap()
);
assert_eq!(ipam.cidr(), "fd00::/64");
}
#[test]
fn test_ipam6_invalid_cidr() {
assert!(Ipam6::new("fd00::").is_err());
assert!(Ipam6::new("not-an-ip/64").is_err());
assert!(Ipam6::new("fd00::/63").is_err()); assert!(Ipam6::new("fd00::/121").is_err()); }
#[test]
fn test_ipam6_allocate_first() {
let ipam = Ipam6::new("fd00::/64").unwrap();
let ip = ipam.allocate(&[]).unwrap();
assert_eq!(ip, "fd00::2".parse::<std::net::Ipv6Addr>().unwrap());
}
#[test]
fn test_ipam6_allocate_sequential() {
let ipam = Ipam6::new("fd00::/64").unwrap();
let ip1 = ipam.allocate(&[]).unwrap();
let ip2 = ipam.allocate(&[ip1]).unwrap();
let ip3 = ipam.allocate(&[ip1, ip2]).unwrap();
assert_eq!(ip1, "fd00::2".parse::<std::net::Ipv6Addr>().unwrap());
assert_eq!(ip2, "fd00::3".parse::<std::net::Ipv6Addr>().unwrap());
assert_eq!(ip3, "fd00::4".parse::<std::net::Ipv6Addr>().unwrap());
}
#[test]
fn test_ipam6_allocate_skips_gateway() {
let ipam = Ipam6::new("fd00::/64").unwrap();
let ip = ipam.allocate(&[]).unwrap();
assert_ne!(ip, ipam.gateway());
}
#[test]
fn test_ipam6_slash120() {
let ipam = Ipam6::new("fd00::/120").unwrap();
let ip = ipam.allocate(&[]).unwrap();
assert_eq!(ip, "fd00::2".parse::<std::net::Ipv6Addr>().unwrap());
}
}