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
502 changes: 339 additions & 163 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ futures-core = "0.3.31"
futures-util = "0.3.31"
tower = { version = "0.5.3", features = ["util"] }
hyper-util = "0.1.20"
regex = "1.12.2"
serde_with = "3.16.1"
chrono = { version = "0.4.42", features = ["serde"] }

[build-dependencies]
chrono = { version = "0.4.43", default-features = false, features = ["clock"] }
Expand Down
90 changes: 90 additions & 0 deletions docs/netavark-create-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Netavark Create Input

## Overview

The `netavark create` command accepts a JSON configuration with three main sections:
- `network`: Network configuration details
- `used`: Information about already used resources
- `options`: Creation options and settings

## Network Fields

The `network` object contains the network configuration.

| Field | Description | Required |
|-------|-------------|----------|
| `name` | Name of the network. Must match the pattern `[a-zA-Z0-9][a-zA-Z0-9_.-]*` and cannot be empty. | Yes |
| `id` | Network ID. Must be 64-bit hexadecimal | Yes |
| `driver` | Network driver type (e.g., "bridge", "macvlan", "ipvlan") | Yes |
| `dns_enabled` | Boolean indicating whether DNS should be enabled for this network | Yes |
| `internal` | Boolean indicating whether the network should be internal | Yes |
| `ipv6_enabled` | Boolean indicating if IPv6 is enabled | Yes |
| `network_interface` | Name of the network interface on the host| No |
| `options` | Key-value map of driver-specific network options (e.g., "mtu", "vlan", "isolate", "metric", "vrf", "mode"). | No |
| `ipam_options` | Key-value map of IPAM (IP Address Management) options. Supported drivers: "host-local", "dhcp", "none". | No |
| `subnets` | Array of subnet configurations. Each subnet can include a subnet CIDR, optional gateway, and optional lease range. | No |
| `routes` | Array of static routes for the network. Each route includes destination (CIDR), gateway, and optional metric. | No |
| `network_dns_servers` | Array of IP addresses for DNS servers used by aardvark-dns. | No |
| `labels` | Key-value map of labels associated with the network. | No |

## Used Fields

The `used` object contains information about resources that are already in use and should be avoided.

| Field | Description | Required |
|-------|-------------|----------|
| `interfaces` | Array of network interface names that are already in use on the host. | Yes |
| `names` | Map of network names to their IDs for networks that already exist. Used to prevent duplicate network names. | Yes |
| `subnets` | Array of subnet CIDR ranges that are already in use on the host or by other network configurations. | Yes |

## Options Fields

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 |
| `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 |

## Example

```json
{
"network": {
"name": "example-network",
"id": "abc123def4567890123456789012345678901234567890123456789012345678",
"driver": "bridge",
"dns_enabled": false,
"internal": false,
"ipv6_enabled": false,
"subnets": [
{
"subnet": "10.100.0.0/24"
}
],
"options": {
"mtu": "1500"
},
"labels": {
"key1": "value1",
"key2": "value2"
}
},
"used": {
"interfaces": [],
"names": {},
"subnets": []
},
"options": {
"subnet_pools": [
{
"base": "10.89.0.0/16",
"size": 24
}
],
"default_interface_name": "podman",
"check_used_subnets": false
}
}
```
7 changes: 7 additions & 0 deletions docs/netavark.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ Instead of reading from STDIN, read the configuration to be applied from the giv

## COMMANDS

### netavark create

The create command takes a partially configured network and completes the remaining configuration.
The create command input is describled in [netavark-create-input.md](https://github.com/containers/netavark/blob/main/docs/netavark-create-input.md). The output of the
create command is a network config json.

### netavark setup

The setup command configures the given network namespace with the given configuration, creating any interfaces and firewall rules necessary.
Expand All @@ -24,6 +30,7 @@ The setup command configures the given network namespace with the given configur

The teardown command is the inverse of the setup command, undoing any configuration applied. Some interfaces may not be deleted (bridge interfaces, for example, will not be removed).


### CONFIGURATION FORMAT

The configuration accepted is the same for both setup and teardown. It is JSON formatted.
Expand Down
23 changes: 23 additions & 0 deletions src/commands/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use crate::error::NetavarkResult;
use crate::network;
use clap::Parser;
use std::ffi::OsString;

#[derive(Parser, Debug)]
pub struct Create {}

impl Create {
pub fn exec(
&self,
input_file: Option<OsString>,
plugin_directories: Option<Vec<OsString>>,
) -> NetavarkResult<()> {
let network_options = network::types::NetworkCreateConfig::load(input_file)?;

let network = network::create_config::new_network(network_options, &plugin_directories)?;
let response_json = serde_json::to_string(&network)?;
println!("{response_json}");

Ok(())
}
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::ffi::OsString;

use crate::error::{NetavarkError, NetavarkResult};

pub mod create;
pub mod dhcp_proxy;
pub mod firewall_reload;
pub mod firewalld_reload;
Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::ffi::OsString;

use clap::{Parser, Subcommand};

use netavark::commands::create;
use netavark::commands::dhcp_proxy;
use netavark::commands::firewall_reload;
use netavark::commands::firewalld_reload;
Expand Down Expand Up @@ -39,6 +40,8 @@ struct Opts {

#[derive(Subcommand, Debug)]
enum SubCommand {
/// Create a network config
Create(create::Create),
/// Configures the given network namespace with the given configuration.
Setup(setup::Setup),
/// Updates network dns servers for an already configured network.
Expand Down Expand Up @@ -68,6 +71,7 @@ fn main() {
.aardvark_binary
.unwrap_or_else(|| OsString::from("/usr/libexec/podman/aardvark-dns"));
let result = match opts.subcmd {
SubCommand::Create(create) => create.exec(opts.file, opts.plugin_directories),
SubCommand::Setup(setup) => setup.exec(
opts.file,
config,
Expand Down
67 changes: 58 additions & 9 deletions src/network/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ use super::{
ISOLATE_OPTION_FALSE, ISOLATE_OPTION_STRICT, ISOLATE_OPTION_TRUE,
NO_CONTAINER_INTERFACE_ERROR, OPTION_HOST_INTERFACE_NAME, OPTION_ISOLATE, OPTION_METRIC,
OPTION_MODE, OPTION_MTU, OPTION_NO_DEFAULT_ROUTE, OPTION_OUTBOUND_ADDR4,
OPTION_OUTBOUND_ADDR6, OPTION_VLAN, OPTION_VRF,
OPTION_OUTBOUND_ADDR6, OPTION_VLAN, OPTION_VRF, VALID_BRIDGE_OPTS,
},
core_utils::{self, get_ipam_addresses, is_using_systemd, join_netns, parse_option, CoreUtils},
driver::{self, DriverInfo},
Expand Down Expand Up @@ -125,14 +125,15 @@ impl driver::NetworkDriver for Bridge<'_> {
}
let ipam = get_ipam_addresses(self.info.per_network_opts, self.info.network)?;

let mode: Option<String> = parse_option(&self.info.network.options, OPTION_MODE)?;
let mtu: u32 = parse_option(&self.info.network.options, OPTION_MTU)?.unwrap_or(0);
let isolate: IsolateOption = get_isolate_option(&self.info.network.options)?;
let metric: u32 = parse_option(&self.info.network.options, OPTION_METRIC)?.unwrap_or(100);
let no_default_route: bool =
parse_option(&self.info.network.options, OPTION_NO_DEFAULT_ROUTE)?.unwrap_or(false);
let vrf: Option<String> = parse_option(&self.info.network.options, OPTION_VRF)?;
let vlan: Option<u16> = parse_option(&self.info.network.options, OPTION_VLAN)?;
let opts = parse_bridge_opts(&self.info.network.options, false)?;
let mode: Option<String> = opts.mode.clone();
let mtu: u32 = opts.mtu.unwrap_or(0);
let isolate: IsolateOption = opts.isolate;
let metric: u32 = opts.metric.unwrap_or(100);
let no_default_route: bool = opts.no_default_route.unwrap_or(false);
let vrf: Option<String> = opts.vrf.clone();
let vlan: Option<u16> = opts.vlan;

let host_interface_name = parse_option(
&self.info.per_network_opts.options,
OPTION_HOST_INTERFACE_NAME,
Expand Down Expand Up @@ -1173,3 +1174,51 @@ fn maybe_add_alias<'a>(names: &mut Vec<SafeString<'a>>, name: &'a str) {
Err(err) => log::warn!("invalid network alias {name:?}: {err}, ignoring this name"),
}
}

pub fn parse_bridge_opts(
opts: &Option<HashMap<String, String>>,
strict: bool,
) -> NetavarkResult<BridgeOptions> {
if strict {
if let Some(invalid_key) = opts
.as_ref()
.and_then(|m| m.keys().find(|k| !VALID_BRIDGE_OPTS.contains(&k.as_str())))
{
return Err(NetavarkError::msg(format!(
"unsupported bridge network option: {}",
invalid_key
)));
}
}
let mode = parse_option(opts, OPTION_MODE)?;
let mtu = parse_option(opts, OPTION_MTU)?;
let isolate: IsolateOption = get_isolate_option(opts)?;
let metric = parse_option(opts, OPTION_METRIC)?;
let no_default_route = parse_option(opts, OPTION_NO_DEFAULT_ROUTE)?;
let vrf = parse_option(opts, OPTION_VRF)?;
let vlan = parse_option(opts, OPTION_VLAN)?;
if vlan.is_some() && vlan.unwrap() > 4094 {
return Err(NetavarkError::msg(
"vlan must be between 0 and 4094".to_string(),
));
}
Ok(BridgeOptions {
mode,
mtu,
isolate,
metric,
no_default_route,
vrf,
vlan,
})
}

pub struct BridgeOptions {
pub mode: Option<String>,
pub mtu: Option<u32>,
pub isolate: IsolateOption,
pub metric: Option<u32>,
pub no_default_route: Option<bool>,
pub vrf: Option<String>,
pub vlan: Option<u16>,
}
27 changes: 27 additions & 0 deletions src/network/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,37 @@ pub const OPTION_HOST_INTERFACE_NAME: &str = "host_interface_name";
pub const OPTION_OUTBOUND_ADDR4: &str = "outbound_addr4";
pub const OPTION_OUTBOUND_ADDR6: &str = "outbound_addr6";

pub const MACVLAN_MODE_PRIVATE: &str = "private";
pub const MACVLAN_MODE_VEPA: &str = "vepa";
pub const MACVLAN_MODE_BRIDGE: &str = "bridge";
pub const MACVLAN_MODE_PASSTHRU: &str = "passthru";

/// 100 is the default metric for most Linux networking tools.
pub const DEFAULT_METRIC: u32 = 100;

pub const NO_CONTAINER_INTERFACE_ERROR: &str = "no container interface name given";

/// make sure this is the same rootful default as used in podman.
pub const DEFAULT_CONFIG_DIR: &str = "/run/containers/networks";

pub const MAX_INTERFACE_NAME_LEN: usize = 15;

// ValidMacVlanModes is the list of valid option constants for the macvlan driver.
pub const VALID_BRIDGE_OPTS: &[&str] = &[
OPTION_MODE,
OPTION_MTU,
OPTION_ISOLATE,
OPTION_METRIC,
OPTION_NO_DEFAULT_ROUTE,
OPTION_VRF,
OPTION_VLAN,
];

// ValidMacVlanModes is the list of valid option constants for the macvlan driver.
pub const VALID_VLAN_OPTS: &[&str] = &[
OPTION_MODE,
OPTION_MTU,
OPTION_METRIC,
OPTION_NO_DEFAULT_ROUTE,
OPTION_BCLIM,
];
71 changes: 71 additions & 0 deletions src/network/create_config/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use crate::network::types;
use chrono::Utc;
use std::ffi::OsString;

use crate::error::{NetavarkError, NetavarkResult};

use super::driver::*;
use super::validation::*;

pub fn new_network(
create: types::NetworkCreateConfig,
plugin_directories: &Option<Vec<OsString>>,
) -> NetavarkResult<types::Network> {
use crate::network::constants;
let mut network = create.network;
validate_name_id(&network.name, &network.id, &create.used.names)?;
if let Some(ref if_name) = network.network_interface {
validate_interface_name(if_name)?;
}
validate_ipam_driver(&network.ipam_options, &network.subnets)?;

let mut check_used = false;

match network.driver.as_str() {
constants::DRIVER_BRIDGE => {
let (check_used_val, check_bridge_conflict) = setup_bridge_options(&mut network)?;
check_used = check_used_val && create.create_opts.check_used_subnets;
create_bridge(
&mut network,
&create.used,
check_used,
check_bridge_conflict,
create.create_opts,
)?;
}
constants::DRIVER_IPVLAN | constants::DRIVER_MACVLAN => {
create_ipvlan_macvlan(&mut network)?;
}
_ => exec_plugin_driver(&mut network, plugin_directories)?,
}

if network
.ipam_options
.as_ref()
.and_then(|ipam_opts| ipam_opts.get("driver"))
.map(|driver| driver == constants::IPAM_NONE)
.unwrap_or(false)
{
log::debug!(
"dns disabled for network {:?} because ipam driver is set to none",
network.name
);
network.dns_enabled = false;
}
if network.network_dns_servers.is_some() && !network.dns_enabled {
return Err(NetavarkError::msg(
"cannot set NetworkDNSServers if DNS is not enabled for the network",
));
}

let add_gateway = !network.internal || network.dns_enabled;
validate_subnets(&mut network, add_gateway, check_used, &create.used.subnets)?;

if network.routes.is_some() {
validate_routes(&mut network)?;
}

network.created = Some(Utc::now());

Ok(network)
}
Loading