Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/netavark-create-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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
}
Expand Down
11 changes: 10 additions & 1 deletion src/network/create_config/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
191 changes: 191 additions & 0 deletions src/network/create_config/subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ fn next_subnet(network: &Ipv4Net) -> NetavarkResult<Ipv4Net> {
.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<Ipv6Net> {
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],
Expand Down Expand Up @@ -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<Subnet> {
let mut last_error: Option<NetavarkError> = 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<Ipv6Net> {
// RFC4193: fd00::/8 prefix
Expand Down Expand Up @@ -137,3 +226,105 @@ pub fn get_free_ipv6_network_subnet(used_networks: &[IpNet]) -> NetavarkResult<S

Err(NetavarkError::msg("failed to get random ipv6 subnet"))
}

#[cfg(test)]
mod tests {
use super::*;

fn pool(base: &str, size: u32) -> 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::<Ipv6Net>().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::<IpNet>().unwrap());
}

#[test]
fn test_ipv6_pool_skip_used_subnets() {
let used: Vec<IpNet> = 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::<IpNet>().unwrap()
);
}

#[test]
fn test_ipv6_pool_fall_through_to_second_pool() {
let used: Vec<IpNet> = 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::<IpNet>().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::<IpNet>().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<IpNet> = (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<IpNet> = 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<IpNet> = 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");
}
}
}
3 changes: 3 additions & 0 deletions src/network/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ pub struct NetworkCreateConfig {
pub struct CreateOpts {
#[serde(rename = "subnet_pools")]
pub subnet_pools: Vec<SubnetPool>,
#[serde(rename = "subnet_pools_v6")]
#[serde(default)]
pub subnet_pools_v6: Vec<SubnetPool>,
#[serde(rename = "default_interface_name")]
pub default_interface_name: Option<String>,
#[serde(rename = "check_used_subnets")]
Expand Down
4 changes: 3 additions & 1 deletion test/testfiles/create/ipv6-auto-dualstack.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion test/testfiles/create/ipv6-enabled.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down