diff --git a/docs/netavark-create-input.md b/docs/netavark-create-input.md index e6aa21653..87aff2f73 100644 --- a/docs/netavark-create-input.md +++ b/docs/netavark-create-input.md @@ -43,7 +43,8 @@ The `options` object contains creation options and settings. | Field | Description | Required | |-------|-------------|----------| -| `subnet_pools` | Array of subnet pools from which to allocate subnets. Each pool contains a `base` (CIDR) and `size` (subnet size in bits). | Yes | +| `subnet_pools` | Array of IPv4 subnet pools from which to allocate subnets. Each pool contains a `base` (CIDR) and `size` (subnet size in bits). | Yes | +| `subnet_pools_v6` | Array of IPv6 subnet pools from which to allocate subnets. Each pool contains a `base` (IPv6 CIDR) and `size` (subnet prefix length). When empty, IPv6 subnets are generated randomly from fd00::/8 per RFC 4193. | No | | `default_interface_name` | Default prefix for auto-generated interface names (e.g., "podman" will generate "podman0", "podman1", etc.). | No | | `check_used_subnets` | Boolean flag indicating whether to check if subnets conflict with already used subnets. | Yes | @@ -83,6 +84,12 @@ The `options` object contains creation options and settings. "size": 24 } ], + "subnet_pools_v6": [ + { + "base": "fd00::/8", + "size": 64 + } + ], "default_interface_name": "podman", "check_used_subnets": false } diff --git a/src/network/create_config/driver.rs b/src/network/create_config/driver.rs index 0c5dd8db3..01914bc34 100644 --- a/src/network/create_config/driver.rs +++ b/src/network/create_config/driver.rs @@ -4,6 +4,7 @@ use crate::network::constants; use crate::network::core_utils::CoreUtils; use crate::network::create_config::subnet::{ get_free_ipv4_network_subnet, get_free_ipv6_network_subnet, + get_free_ipv6_network_subnet_from_pools, }; use crate::network::netlink::Socket; use crate::network::netlink_route::NetlinkRoute; @@ -115,7 +116,15 @@ pub fn create_bridge( updated_subnets.push(free_subnet); } if !ipv6 { - let free_subnet = get_free_ipv6_network_subnet(&used.subnets)?; + let free_subnet = if !opts.subnet_pools_v6.is_empty() { + get_free_ipv6_network_subnet_from_pools( + &used.subnets, + &opts.subnet_pools_v6, + check_used, + )? + } else { + get_free_ipv6_network_subnet(&used.subnets)? + }; updated_subnets.push(free_subnet); } if !ipv4 || !ipv6 { diff --git a/src/network/create_config/subnet.rs b/src/network/create_config/subnet.rs index e1353f2c4..34d63b113 100644 --- a/src/network/create_config/subnet.rs +++ b/src/network/create_config/subnet.rs @@ -37,6 +37,23 @@ fn next_subnet(network: &Ipv4Net) -> NetavarkResult { .map_err(|e| NetavarkError::msg(format!("Failed to create next subnet: {}", e))) } +/// Get the next IPv6 subnet by incrementing the network address by the subnet size. +fn next_subnet_v6(network: &Ipv6Net) -> NetavarkResult { + let prefix_len = network.prefix_len(); + let subnet_size = 1u128 << (128 - prefix_len); + + let network_addr: u128 = network.network().into(); + + let next_addr = network_addr + .checked_add(subnet_size) + .ok_or_else(|| NetavarkError::msg("IPv6 subnet address overflow"))?; + + let next_ip = Ipv6Addr::from(next_addr); + + Ipv6Net::new(next_ip, prefix_len) + .map_err(|e| NetavarkError::msg(format!("Failed to create next IPv6 subnet: {}", e))) +} + /// Get a free IPv4 network subnet from subnet pools. pub fn get_free_ipv4_network_subnet( used_networks: &[IpNet], @@ -101,6 +118,78 @@ pub fn get_free_ipv4_network_subnet( } } +/// Get a free IPv6 network subnet from subnet pools. +pub fn get_free_ipv6_network_subnet_from_pools( + used_networks: &[IpNet], + subnet_pools: &[SubnetPool], + check_used: bool, +) -> NetavarkResult { + let mut last_error: Option = None; + + const MAX_ITERATIONS: usize = 10000; + + for pool in subnet_pools { + let pool_v6 = match pool.base { + IpNet::V6(v6) => v6, + IpNet::V4(_) => continue, + }; + + let pool_base_network_addr: Ipv6Addr = pool_v6.network(); + let mut network = match Ipv6Net::new(pool_base_network_addr, pool.size as u8) { + Ok(net) => net, + Err(e) => { + last_error = Some(NetavarkError::msg(format!( + "Failed to create IPv6 network from pool: {}", + e + ))); + continue; + } + }; + + let mut iterations = 0; + loop { + if iterations >= MAX_ITERATIONS { + last_error = Some(NetavarkError::msg( + "exceeded maximum iterations searching for free IPv6 subnet in pool", + )); + break; + } + iterations += 1; + + let network_addr: Ipv6Addr = network.network(); + if !pool_v6.contains(&network_addr) { + break; + } + + let found = !(check_used + && network_intersects_with_networks(&IpNet::V6(network), used_networks)); + if found { + return Ok(Subnet { + gateway: None, + lease_range: None, + subnet: IpNet::V6(network), + }); + } + + match next_subnet_v6(&network) { + Ok(next) => network = next, + Err(e) => { + last_error = Some(e); + break; + } + } + } + } + + if let Some(err) = last_error { + Err(err) + } else { + Err(NetavarkError::msg( + "could not find free IPv6 subnet from subnet pools", + )) + } +} + // returns a random internal ipv6 subnet as described in RFC3879. fn get_random_ipv6_subnet() -> NetavarkResult { // RFC4193: fd00::/8 prefix @@ -137,3 +226,105 @@ pub fn get_free_ipv6_network_subnet(used_networks: &[IpNet]) -> NetavarkResult SubnetPool { + SubnetPool { + base: base.parse().unwrap(), + size, + } + } + + #[test] + fn test_next_subnet_v6() { + let net: Ipv6Net = "fd00::/64".parse().unwrap(); + let next = next_subnet_v6(&net).unwrap(); + assert_eq!(next, "fd00:0:0:1::/64".parse::().unwrap()); + } + + #[test] + fn test_ipv6_pool_allocate_from_single_pool() { + let pools = vec![pool("fd00::/8", 64)]; + let result = get_free_ipv6_network_subnet_from_pools(&[], &pools, true).unwrap(); + assert_eq!(result.subnet, "fd00::/64".parse::().unwrap()); + } + + #[test] + fn test_ipv6_pool_skip_used_subnets() { + let used: Vec = vec!["fd00::/64".parse().unwrap()]; + let pools = vec![pool("fd00::/8", 64)]; + let result = get_free_ipv6_network_subnet_from_pools(&used, &pools, true).unwrap(); + assert_eq!( + result.subnet, + "fd00:0:0:1::/64".parse::().unwrap() + ); + } + + #[test] + fn test_ipv6_pool_fall_through_to_second_pool() { + let used: Vec = vec!["fd10::/48".parse().unwrap()]; + let pools = vec![pool("fd10::/48", 64), pool("fd20::/48", 64)]; + let result = get_free_ipv6_network_subnet_from_pools(&used, &pools, true).unwrap(); + assert_eq!(result.subnet, "fd20::/64".parse::().unwrap()); + } + + #[test] + fn test_ipv6_pool_skips_ipv4_entries() { + let pools = vec![pool("10.89.0.0/16", 24), pool("fd00::/8", 64)]; + let result = get_free_ipv6_network_subnet_from_pools(&[], &pools, true).unwrap(); + assert_eq!(result.subnet, "fd00::/64".parse::().unwrap()); + } + + #[test] + fn test_ipv6_pool_no_pools_returns_error() { + let result = get_free_ipv6_network_subnet_from_pools(&[], &[], true); + assert!(result.is_err()); + } + + #[test] + fn test_ipv6_pool_max_iterations() { + // Use a huge pool (fd00::/8 with /64 subnets = 2^56 candidates) with all + // first 10000 subnets marked as used, forcing the iteration limit to trigger. + let used: Vec = (0u64..10000) + .map(|i| { + let addr = 0xfd00_0000_0000_0000_0000_0000_0000_0000u128 + (i as u128 * (1u128 << 64)); + let ip = Ipv6Addr::from(addr); + IpNet::V6(Ipv6Net::new(ip, 64).unwrap()) + }) + .collect(); + let pools = vec![pool("fd00::/8", 64)]; + let result = get_free_ipv6_network_subnet_from_pools(&used, &pools, true); + assert!(result.is_err()); + assert!( + result.unwrap_err().to_string().contains("maximum iterations"), + ); + } + + #[test] + fn test_network_intersects() { + let net: IpNet = "10.89.0.0/24".parse().unwrap(); + let used: Vec = vec!["10.89.0.0/16".parse().unwrap()]; + assert!(network_intersects_with_networks(&net, &used)); + } + + #[test] + fn test_network_does_not_intersect() { + let net: IpNet = "10.90.0.0/24".parse().unwrap(); + let used: Vec = vec!["10.89.0.0/24".parse().unwrap()]; + assert!(!network_intersects_with_networks(&net, &used)); + } + + #[test] + fn test_ipv6_random_subnet_is_ula() { + let result = get_free_ipv6_network_subnet(&[]).unwrap(); + if let IpNet::V6(v6) = result.subnet { + assert_eq!(v6.addr().octets()[0], 0xfd); + assert_eq!(v6.prefix_len(), 64); + } else { + panic!("expected IPv6 subnet"); + } + } +} diff --git a/src/network/types.rs b/src/network/types.rs index 912f213e3..723e4f105 100644 --- a/src/network/types.rs +++ b/src/network/types.rs @@ -85,6 +85,9 @@ pub struct NetworkCreateConfig { pub struct CreateOpts { #[serde(rename = "subnet_pools")] pub subnet_pools: Vec, + #[serde(rename = "subnet_pools_v6")] + #[serde(default)] + pub subnet_pools_v6: Vec, #[serde(rename = "default_interface_name")] pub default_interface_name: Option, #[serde(rename = "check_used_subnets")] diff --git a/test/testfiles/create/ipv6-auto-dualstack.json b/test/testfiles/create/ipv6-auto-dualstack.json index 51594d530..753ed93e5 100644 --- a/test/testfiles/create/ipv6-auto-dualstack.json +++ b/test/testfiles/create/ipv6-auto-dualstack.json @@ -14,7 +14,9 @@ }, "options": { "subnet_pools": [ - {"base": "10.89.0.0/16", "size": 24}, + {"base": "10.89.0.0/16", "size": 24} + ], + "subnet_pools_v6": [ {"base": "fd10::/8", "size": 64} ], "default_interface_name": "podman", diff --git a/test/testfiles/create/ipv6-enabled.json b/test/testfiles/create/ipv6-enabled.json index 0e0f9fa9a..1a7b9446e 100644 --- a/test/testfiles/create/ipv6-enabled.json +++ b/test/testfiles/create/ipv6-enabled.json @@ -14,7 +14,9 @@ }, "options": { "subnet_pools": [ - {"base": "10.89.0.0/16", "size": 24}, + {"base": "10.89.0.0/16", "size": 24} + ], + "subnet_pools_v6": [ {"base": "fd00::/64", "size": 64} ], "default_interface_name": "podman",