├── src ├── dns │ └── mod.rs ├── test │ ├── mod.rs │ ├── config │ │ ├── setupopts2.test.json │ │ ├── setupoptsmacvlan.test.json │ │ ├── setupopts.test.json │ │ ├── portmapping.json │ │ └── twoNetworks.json │ └── test.rs ├── firewall │ ├── varktables │ │ ├── mod.rs │ │ └── helpers.rs │ ├── fwnone.rs │ └── mod.rs ├── dhcp_proxy_client │ ├── commands │ │ ├── mod.rs │ │ ├── teardown.rs │ │ └── setup.rs │ └── client.rs ├── dhcp_proxy │ ├── mod.rs │ ├── types.rs │ └── ip.rs ├── lib.rs ├── network │ ├── validation.rs │ ├── mod.rs │ ├── constants.rs │ ├── driver.rs │ ├── internal_types.rs │ ├── plugin.rs │ └── dhcp.rs ├── commands │ ├── mod.rs │ ├── version.rs │ ├── update.rs │ ├── firewalld_reload.rs │ └── teardown.rs ├── proto │ └── proxy.proto ├── main.rs └── plugin.rs ├── OWNERS ├── .gitignore ├── test-dhcp ├── configfiles │ ├── no_mac.json │ ├── bad_mac.json │ ├── bad_interface.json │ └── basic.json ├── README.md ├── 001-basic.bats ├── setup.sh ├── dnsmasqfiles │ └── sample.conf ├── 004-server.bats ├── 003-teardown.bats ├── 005-renew.bats ├── README-manual_test.md └── 002-setup.bats ├── CODE-OF-CONDUCT.md ├── contrib ├── systemd │ └── system │ │ ├── netavark-dhcp-proxy.socket │ │ ├── netavark-dhcp-proxy.service.in │ │ └── netavark-firewalld-reload.service.in ├── container_images │ ├── Dockerfile.Rust │ ├── README.md │ └── build_and_publish_rust_image.sh └── cirrus │ ├── setup.sh │ ├── runner.sh │ ├── lib.sh │ └── cache_groom.sh ├── SECURITY.md ├── docs ├── publish-crate.md ├── Makefile ├── generate-code-coverage-report.md ├── netavark-dhcp-proxy.md └── netavark.1.md ├── test ├── 500-bridge-fwnone.bats ├── README.md ├── testfiles │ ├── macvlan-dhcp.json │ ├── bridge-unmanaged.json │ ├── bridge-managed-dhcp.json │ ├── internal.json │ ├── simplebridge.json │ ├── ipvlan.json │ ├── macvlan.json │ ├── ipvlan-internal.json │ ├── macvlan-internal.json │ ├── bridge-managed.json │ ├── bridge-nodefaultroute.json │ ├── bridge-vethname.json │ ├── simplebridge-vrf.json │ ├── bridge-vethname-exists.json │ ├── ipvlan-mtu.json │ ├── macvlan-mtu.json │ ├── ipvlan-nodefaultroute.json │ ├── macvlan-nodefaultroute.json │ ├── ipv6-bridge.json │ ├── invalid-port.json │ ├── isolate1.json │ ├── isolate2.json │ ├── isolate3.json │ ├── isolate4.json │ ├── metric.json │ ├── metric-macvlan.json │ ├── dualstack-bridge-network-container-dns-server.json │ ├── dualstack-bridge.json │ ├── dualstack-bridge-custom-dns-server.json │ ├── dualstack-bridge-multiple-custom-dns-server.json │ ├── bridge-staticroutes.json │ ├── ipv6-bridge-staticroutes.json │ ├── bridge-port-hostip.json │ ├── bridge-port-tcp-udp.json │ ├── ipvlan-staticroutes.json │ ├── macvlan-staticroutes.json │ ├── two-networks.json │ └── firewalld-dbus.conf ├── 600-bridge-vrf.bats ├── 610-bridge-vethname.bats ├── 001-basic.bats ├── 620-bridge-mode.bats ├── 500-plugin.bats └── 630-bridge-vlan.bats ├── perf-netavark.sh ├── .github ├── workflows │ ├── rerun_cirrus_cron.yml │ └── check_cirrus_cron.yml └── renovate.json5 ├── examples ├── error-plugin.rs ├── stderr-plugin.rs └── host-device-plugin.rs ├── README.md ├── Cargo.toml ├── .packit.yaml ├── hack └── get_ci_vm.sh ├── DISTRO_PACKAGE.md ├── rpm └── netavark.spec ├── RELEASE_NOTES.md └── Makefile /src/dns/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod aardvark; 2 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod netlink; 2 | pub mod test; 3 | -------------------------------------------------------------------------------- /src/firewall/varktables/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod helpers; 2 | pub(crate) mod types; 3 | -------------------------------------------------------------------------------- /src/dhcp_proxy_client/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod setup; 2 | pub mod teardown; 3 | // pub mod version; 4 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - baude 3 | - lsm5 4 | - Luap99 5 | - mheon 6 | reviewers: 7 | - flouthoc 8 | -------------------------------------------------------------------------------- /src/dhcp_proxy/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cache; 2 | pub mod dhcp_service; 3 | pub mod ip; 4 | pub mod lib; 5 | pub mod proxy_conf; 6 | pub mod types; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | target/ 3 | targets/ 4 | *.swp 5 | netavark.1 6 | vendor/ 7 | .idea/* 8 | contrib/systemd/*/*.service 9 | .vscode* 10 | -------------------------------------------------------------------------------- /test-dhcp/configfiles/no_mac.json: -------------------------------------------------------------------------------- 1 | { 2 | "host_iface": "veth0", 3 | "mac_addr": "", 4 | "domain_name": "example.com", 5 | "host_name": "foobar", 6 | "version": 0 7 | } 8 | -------------------------------------------------------------------------------- /test-dhcp/configfiles/bad_mac.json: -------------------------------------------------------------------------------- 1 | { 2 | "host_iface": "veth0", 3 | "mac_addr": "3c:e1:a1a:3f", 4 | "domain_name": "example.com", 5 | "host_name": "foobar", 6 | "version": 0 7 | } 8 | -------------------------------------------------------------------------------- /test-dhcp/configfiles/bad_interface.json: -------------------------------------------------------------------------------- 1 | { 2 | "host_iface": "badinterfacename", 3 | "mac_addr": "3c:e1:a1:c1:7a:3f", 4 | "domain_name": "example.com", 5 | "host_name": "foobar", 6 | "version": 0 7 | } 8 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## The Netavark Project Community Code of Conduct 2 | 3 | The Netavark project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/main/CODE-OF-CONDUCT.md). 4 | -------------------------------------------------------------------------------- /contrib/systemd/system/netavark-dhcp-proxy.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Netavark DHCP proxy socket 3 | 4 | [Socket] 5 | ListenStream=%t/podman/nv-proxy.sock 6 | SocketMode=0660 7 | 8 | [Install] 9 | WantedBy=sockets.target 10 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde; 3 | extern crate serde_json; 4 | 5 | pub mod commands; 6 | pub mod dhcp_proxy; 7 | pub mod dns; 8 | pub mod error; 9 | pub mod firewall; 10 | pub mod network; 11 | pub mod plugin; 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security and Disclosure Information Policy for the Netavark Project 2 | 3 | The Netavark Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects. 4 | -------------------------------------------------------------------------------- /test-dhcp/configfiles/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "host_iface": "veth1", 3 | "container_iface": "veth0", 4 | "container_mac_addr": "66:c0:05:74:df:d0", 5 | "domain_name": "example.com", 6 | "host_name": "foobar", 7 | "version": 0, 8 | "ns_path": "/run/netns/foobar" 9 | } 10 | -------------------------------------------------------------------------------- /src/network/validation.rs: -------------------------------------------------------------------------------- 1 | use crate::error::NetavarkResult; 2 | use log::debug; 3 | use std::fs::File; 4 | 5 | pub fn ns_checks(file: &str) -> NetavarkResult<()> { 6 | debug!("Validating network namespace..."); 7 | // TODO check for FS_MAGIC 8 | let _ = File::open(file)?.metadata()?; 9 | Ok(()) 10 | } 11 | -------------------------------------------------------------------------------- /contrib/container_images/Dockerfile.Rust: -------------------------------------------------------------------------------- 1 | # Source for quay.io/libpod/nv-rust 2 | # This version should always match the MSRV, when you update this also update 3 | # the version in the root README.md and Cargo.toml. 4 | FROM docker.io/library/rust:1.76 5 | RUN apt-get update && apt-get -y install protobuf-compiler libprotobuf-dev 6 | -------------------------------------------------------------------------------- /contrib/systemd/system/netavark-dhcp-proxy.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Netavark DHCP proxy service 3 | Requires=netavark-dhcp-proxy.socket 4 | After=netavark-dhcp-proxy.socket 5 | StartLimitIntervalSec=0 6 | 7 | [Service] 8 | Type=exec 9 | ExecStart=@@NETAVARK@@ dhcp-proxy -a 30 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /test-dhcp/README.md: -------------------------------------------------------------------------------- 1 | # netavark-dhcp-proxy integration test with bats 2 | 3 | ## Running tests 4 | 5 | To run the tests locally in your sandbox, you can use one of these methods: 6 | * bats ./test/001-basic.bats # runs just the specified test 7 | * bats ./test/ # runs all 8 | 9 | ## Requirements 10 | - bats 11 | - jq 12 | -------------------------------------------------------------------------------- /docs/publish-crate.md: -------------------------------------------------------------------------------- 1 | # Publishing netavark crate to crates.io 2 | ### Steps 3 | * Make sure you have already done `cargo login` on your current session with a valid token. 4 | * `cd netavark` 5 | * Git checkout the version which you want to publish. 6 | * `make crate-publish` 7 | * New version should be reflected here: https://crates.io/crates/netavark/versions 8 | -------------------------------------------------------------------------------- /test-dhcp/001-basic.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | 8 | # One might think this is a NOP and does nothing, so do I. But apparently 9 | # something really weird is going with that in CI. If we delete this file 10 | # the basic setup test will fail in CI. No idea why and I tried for to 11 | # long to make any sense of this. 12 | @test "NOP setup" { 13 | : 14 | } 15 | -------------------------------------------------------------------------------- /src/dhcp_proxy_client/commands/teardown.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use log::debug; 3 | use netavark::{ 4 | dhcp_proxy::lib::g_rpc::{Lease, NetworkConfig}, 5 | error::NetavarkError, 6 | }; 7 | 8 | #[derive(Parser, Debug)] 9 | pub struct Teardown {} 10 | 11 | impl Teardown { 12 | pub async fn exec(&self, p: &str, config: NetworkConfig) -> Result { 13 | debug!("Entering teardown"); 14 | config.clone().drop_lease(p).await 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/500-bridge-fwnone.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # bridge driver tests with none firewall driver 4 | # 5 | 6 | load helpers 7 | 8 | fw_driver=none 9 | 10 | @test "check none firewall driver is in use" { 11 | RUST_LOG=netavark=info NETAVARK_FW="none" run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) 12 | assert "${lines[0]}" "==" "[INFO netavark::firewall] Not using firewall" "none firewall driver is in use" 13 | } 14 | -------------------------------------------------------------------------------- /contrib/container_images/README.md: -------------------------------------------------------------------------------- 1 | # Rust image 2 | 3 | The point of this image is to verify the MSRV in CI so we can catch if a code or dependency change bumped the MSRV. 4 | If this is acceptable then update the version in the Dockerfile.Rust FROM line then build the new image see below. 5 | 6 | # Build and publish rust image 7 | 8 | Make sure you have valid `quay.io/libpod` credentials in order to push the image there. 9 | Then run the script `build_and_publish_rust_image.sh` to build and push it. 10 | -------------------------------------------------------------------------------- /contrib/systemd/system/netavark-firewalld-reload.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Listen for the firewalld reload event and reapply all netavark firewall rules. 3 | # This causes systemd to stop this unit when firewalld is stopped. 4 | PartOf=firewalld.service 5 | After=firewalld.service 6 | 7 | [Service] 8 | ExecStart=@@NETAVARK@@ firewalld-reload 9 | 10 | [Install] 11 | # If the unit is enabled add a wants to firewalld so it is only started when firewalld is started. 12 | WantedBy=firewalld.service 13 | -------------------------------------------------------------------------------- /perf-netavark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Netavark binary 4 | NETAVARK=${NETAVARK:-./bin/netavark} 5 | 6 | trap cleanup EXIT 7 | 8 | function cleanup() { 9 | kill -9 $netnspid 10 | rm -rf $TMP_CONFIG 11 | } 12 | 13 | TMP_CONFIG=$(mktemp -d) 14 | unshare -n sleep 100 & 15 | netnspid=$! 16 | 17 | # first arg is the fw driver 18 | if [ -n "$1" ]; then 19 | export NETAVARK_FW="$1" 20 | fi 21 | 22 | unshare -n perf stat $NETAVARK -f ./test/testfiles/simplebridge.json --config $TMP_CONFIG setup /proc/$netnspid/ns/net 23 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | 3 | use crate::error::{NetavarkError, NetavarkResult}; 4 | 5 | pub mod dhcp_proxy; 6 | pub mod firewalld_reload; 7 | pub mod setup; 8 | pub mod teardown; 9 | pub mod update; 10 | pub mod version; 11 | 12 | fn get_config_dir(dir: Option, cmd: &str) -> NetavarkResult { 13 | dir.ok_or_else(|| { 14 | NetavarkError::msg(format!( 15 | "--config not specified but required for netavark {}", 16 | cmd 17 | )) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | DATADIR ?= ${PREFIX}/share 3 | MANDIR ?= $(DATADIR)/man 4 | GO ?= go 5 | GOMD2MAN ?= go-md2man 6 | 7 | docs: $(patsubst %.md,%,$(wildcard *.1.md)) 8 | 9 | %.1: %.1.md 10 | $(GOMD2MAN) -in $^ -out $@ 11 | 12 | .PHONY: .install.md2man 13 | .install.md2man: 14 | $(GO) install github.com/cpuguy83/go-md2man/v2@latest 15 | 16 | .PHONY: install 17 | install: 18 | install -d ${DESTDIR}/${MANDIR}/man1 19 | install -m 0644 *.1 ${DESTDIR}/${MANDIR}/man1 20 | 21 | .PHONY: clean 22 | clean: 23 | $(RM) *.1 24 | -------------------------------------------------------------------------------- /src/dhcp_proxy_client/commands/setup.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use log::debug; 3 | use netavark::{ 4 | dhcp_proxy::lib::g_rpc::{Lease, NetworkConfig}, 5 | error::NetavarkError, 6 | }; 7 | 8 | #[derive(Parser, Debug)] 9 | pub struct Setup {} 10 | 11 | impl Setup { 12 | pub async fn exec(&self, p: &str, config: NetworkConfig) -> Result { 13 | debug!("Setting up..."); 14 | debug!("input: {:#?}", serde_json::to_string_pretty(&config)); 15 | 16 | config.clone().get_lease(p).await 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test-dhcp/setup.sh: -------------------------------------------------------------------------------- 1 | ip netns add new 2 | ip link add dev outside type veth peer name outsidebr 3 | ip link add dev inside type veth peer name insidebr 4 | ip link add brtest type bridge 5 | ip addr add 172.172.1.1/24 dev brtest 6 | ip link set outsidebr master brtest 7 | ip link set insidebr master brtest 8 | ip link set brtest up 9 | ip link set inside netns new 10 | ip link set outsidebr up 11 | ip link set insidebr up 12 | ip addr add 172.172.1.2/24 dev outside 13 | ip link set outside up 14 | ip netns exec new ip link set lo up 15 | ip netns exec new ip link set inside up 16 | -------------------------------------------------------------------------------- /contrib/container_images/build_and_publish_rust_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | PODMAN=${PODMAN:-podman} 6 | SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}") 7 | DOCKERFILE="$(dirname "${BASH_SOURCE[0]}")/Dockerfile.Rust" 8 | 9 | # get the tag from the Dockerfile so we do not duplicate it 10 | TAG=$(awk -F':' '/FROM/{print $NF}' $DOCKERFILE) 11 | if [[ -z "$TAG" ]]; then 12 | echo "Empty tag in $DOCKERFILE; the tag must specify the rust version to use" >&2 13 | exit 1 14 | fi 15 | 16 | FULL_IMAGE_NAME="quay.io/libpod/nv-rust:$TAG" 17 | 18 | $PODMAN build -t $FULL_IMAGE_NAME -f $DOCKERFILE 19 | $PODMAN push $FULL_IMAGE_NAME 20 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Netavark integration test with bats 2 | 3 | ## Running tests 4 | 5 | To run the tests locally in your sandbox, you can use one of these methods: 6 | * bats ./test/001-basic.bats # runs just the specified test 7 | * bats ./test/ # runs all 8 | 9 | The tests need root privileges to create network namespaces, so you either have to run the test as root or in a user namespace. Because the tests use iptables you also need write access to `/run/xtables.lock`. You can use `podman unshare --rootless-netns bats test/` to run the tests as rootless user. 10 | 11 | ## Requirements 12 | - jq 13 | - iproute2 14 | - iptables 15 | - firewalld 16 | - dbus-daemon 17 | - ncat 18 | -------------------------------------------------------------------------------- /.github/workflows/rerun_cirrus_cron.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # See also: https://github.com/containers/podman/blob/main/.github/workflows/rerun_cirrus_cron.yml 4 | 5 | on: 6 | # Note: This only applies to the default branch. 7 | schedule: 8 | # N/B: This should correspond to a period slightly after 9 | # the last job finishes running. See job defs. at: 10 | # https://cirrus-ci.com/settings/repository/5138144844840960 11 | - cron: '01 01 * * 1-5' 12 | # Debug: Allow triggering job manually in github-actions WebUI 13 | workflow_dispatch: {} 14 | 15 | jobs: 16 | # Ref: https://docs.github.com/en/actions/using-workflows/reusing-workflows 17 | call_cron_rerun: 18 | uses: containers/podman/.github/workflows/rerun_cirrus_cron.yml@main 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /test/testfiles/macvlan-dhcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | ], 8 | "interface_name": "eth0" 9 | } 10 | }, 11 | "network_info": { 12 | "podman": { 13 | "name": "podman", 14 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 15 | "driver": "macvlan", 16 | "network_interface": "enp9s0u2u1u2", 17 | "subnets": [ 18 | ], 19 | "ipv6_enabled": false, 20 | "internal": false, 21 | "dns_enabled": false, 22 | "ipam_options": { 23 | "driver": "dhcp" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/check_cirrus_cron.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # See also: 4 | # https://github.com/containers/podman/blob/main/.github/workflows/check_cirrus_cron.yml 5 | 6 | on: 7 | # Note: This only applies to the default branch. 8 | schedule: 9 | # N/B: This should correspond to a period slightly after 10 | # the last job finishes running. See job defs. at: 11 | # https://cirrus-ci.com/settings/repository/5138144844840960 12 | - cron: '03 03 * * 1-5' 13 | # Debug: Allow triggering job manually in github-actions WebUI 14 | workflow_dispatch: {} 15 | 16 | jobs: 17 | # Ref: https://docs.github.com/en/actions/using-workflows/reusing-workflows 18 | call_cron_failures: 19 | uses: containers/podman/.github/workflows/check_cirrus_cron.yml@main 20 | secrets: inherit 21 | -------------------------------------------------------------------------------- /test/testfiles/bridge-unmanaged.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "networks": { 5 | "podman": { 6 | "interface_name": "eth0", 7 | "static_ips": [ 8 | "10.88.0.2" 9 | ] 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "dns_enabled": false, 15 | "driver": "bridge", 16 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 17 | "internal": false, 18 | "ipv6_enabled": false, 19 | "name": "podman", 20 | "network_interface": "brtest0", 21 | "options": { 22 | "mode": "unmanaged" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/testfiles/bridge-managed-dhcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "networks": { 5 | "podman": { 6 | "interface_name": "eth0", 7 | "static_ips": [] 8 | } 9 | }, 10 | "network_info": { 11 | "podman": { 12 | "dns_enabled": false, 13 | "driver": "bridge", 14 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 15 | "internal": false, 16 | "ipv6_enabled": false, 17 | "name": "podman", 18 | "network_interface": "podman0", 19 | "subnets": [], 20 | "options": { 21 | "mode": "managed" 22 | }, 23 | "ipam_options": { 24 | "driver": "dhcp" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/testfiles/internal.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "networks": { 5 | "podman": { 6 | "interface_name": "eth0", 7 | "static_ips": [ 8 | "10.88.0.2" 9 | ] 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "dns_enabled": false, 15 | "driver": "bridge", 16 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 17 | "internal": true, 18 | "ipv6_enabled": false, 19 | "name": "podman", 20 | "network_interface": "podman0", 21 | "subnets": [ 22 | { 23 | "gateway": "10.88.0.1", 24 | "subnet": "10.88.0.0/16" 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/version.rs: -------------------------------------------------------------------------------- 1 | use crate::error::NetavarkResult; 2 | use clap::Parser; 3 | use serde::Serialize; 4 | 5 | #[derive(Parser, Debug)] 6 | pub struct Version {} 7 | 8 | #[derive(Debug, Serialize)] 9 | struct Info { 10 | version: &'static str, 11 | commit: &'static str, 12 | build_time: &'static str, 13 | target: &'static str, 14 | default_fw_driver: &'static str, 15 | } 16 | 17 | impl Version { 18 | pub fn exec(&self) -> NetavarkResult<()> { 19 | let info = Info { 20 | version: env!("CARGO_PKG_VERSION"), 21 | commit: env!("GIT_COMMIT"), 22 | build_time: env!("BUILD_TIMESTAMP"), 23 | target: env!("BUILD_TARGET"), 24 | default_fw_driver: env!("DEFAULT_FW"), 25 | }; 26 | 27 | let out = serde_json::to_string_pretty(&info)?; 28 | println!("{out}"); 29 | 30 | Ok(()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/testfiles/simplebridge.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "networks": { 5 | "podman": { 6 | "interface_name": "eth0", 7 | "static_ips": [ 8 | "10.88.0.2" 9 | ] 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "dns_enabled": false, 15 | "driver": "bridge", 16 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 17 | "internal": false, 18 | "ipv6_enabled": false, 19 | "name": "podman", 20 | "network_interface": "podman0", 21 | "subnets": [ 22 | { 23 | "gateway": "10.88.0.1", 24 | "subnet": "10.88.0.0/16" 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/testfiles/ipvlan.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2" 8 | ], 9 | "interface_name": "eth0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "name": "podman", 15 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 16 | "driver": "ipvlan", 17 | "network_interface": "dummy0", 18 | "subnets": [ 19 | { 20 | "subnet": "10.88.0.0/16", 21 | "gateway": "10.88.0.1" 22 | } 23 | ], 24 | "ipv6_enabled": false, 25 | "internal": false, 26 | "dns_enabled": true, 27 | "ipam_options": { 28 | "driver": "host-local" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/testfiles/macvlan.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2" 8 | ], 9 | "interface_name": "eth0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "name": "podman", 15 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 16 | "driver": "macvlan", 17 | "network_interface": "dummy0", 18 | "subnets": [ 19 | { 20 | "subnet": "10.88.0.0/16", 21 | "gateway": "10.88.0.1" 22 | } 23 | ], 24 | "ipv6_enabled": false, 25 | "internal": false, 26 | "dns_enabled": true, 27 | "ipam_options": { 28 | "driver": "host-local" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/testfiles/ipvlan-internal.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2" 8 | ], 9 | "interface_name": "eth0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "name": "podman", 15 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 16 | "driver": "ipvlan", 17 | "network_interface": "dummy0", 18 | "subnets": [ 19 | { 20 | "subnet": "10.88.0.0/16", 21 | "gateway": "10.88.0.1" 22 | } 23 | ], 24 | "ipv6_enabled": false, 25 | "internal": true, 26 | "dns_enabled": false, 27 | "ipam_options": { 28 | "driver": "host-local" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/testfiles/macvlan-internal.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2" 8 | ], 9 | "interface_name": "eth0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "name": "podman", 15 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 16 | "driver": "macvlan", 17 | "network_interface": "dummy0", 18 | "subnets": [ 19 | { 20 | "subnet": "10.88.0.0/16", 21 | "gateway": "10.88.0.1" 22 | } 23 | ], 24 | "ipv6_enabled": false, 25 | "internal": true, 26 | "dns_enabled": false, 27 | "ipam_options": { 28 | "driver": "host-local" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/config/setupopts2.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2" 8 | ], 9 | "interface_name": "ethsomething0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "name": "podman", 15 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 16 | "driver": "bridge", 17 | "network_interface": "podman0", 18 | "created": "2021-10-26T16:42:48.769170534+02:00", 19 | "subnets": [ 20 | { 21 | "subnet": "10.88.0.0/16", 22 | "gateway": "10.88.0.1" 23 | } 24 | ], 25 | "ipv6_enabled": false, 26 | "internal": false, 27 | "dns_enabled": false, 28 | "ipam_options": { 29 | "driver": "host-local" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/network/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod types; 2 | pub mod validation; 3 | use std::{ 4 | ffi::OsString, 5 | fs::File, 6 | io::{self, BufReader}, 7 | }; 8 | 9 | use crate::{ 10 | error::{NetavarkError, NetavarkResult}, 11 | wrap, 12 | }; 13 | pub mod bridge; 14 | pub mod constants; 15 | pub mod core_utils; 16 | mod dhcp; 17 | pub mod driver; 18 | pub mod internal_types; 19 | pub mod netlink; 20 | pub mod plugin; 21 | pub mod vlan; 22 | 23 | impl types::NetworkOptions { 24 | pub fn load(path: Option) -> NetavarkResult { 25 | wrap!(Self::load_inner(path), "failed to load network options") 26 | } 27 | 28 | fn load_inner(path: Option) -> Result { 29 | let opts = match path { 30 | Some(path) => serde_json::from_reader(BufReader::new(File::open(path)?)), 31 | None => serde_json::from_reader(io::stdin()), 32 | }?; 33 | Ok(opts) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/config/setupoptsmacvlan.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2" 8 | ], 9 | "interface_name": "ethsomething0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "name": "podman", 15 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 16 | "driver": "macvlan", 17 | "network_interface": "wlp0s20f3", 18 | "created": "2021-10-26T16:42:48.769170534+02:00", 19 | "subnets": [ 20 | { 21 | "subnet": "10.88.0.0/16", 22 | "gateway": "10.88.0.1" 23 | } 24 | ], 25 | "ipv6_enabled": false, 26 | "internal": false, 27 | "dns_enabled": false, 28 | "ipam_options": { 29 | "driver": "host-local" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/testfiles/bridge-managed.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "networks": { 5 | "podman": { 6 | "interface_name": "eth0", 7 | "static_ips": [ 8 | "10.88.0.2" 9 | ] 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "dns_enabled": false, 15 | "driver": "bridge", 16 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 17 | "internal": false, 18 | "ipv6_enabled": false, 19 | "name": "podman", 20 | "network_interface": "podman0", 21 | "subnets": [ 22 | { 23 | "gateway": "10.88.0.1", 24 | "subnet": "10.88.0.0/16" 25 | } 26 | ], 27 | "options": { 28 | "mode": "managed" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/testfiles/bridge-nodefaultroute.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "networks": { 5 | "podman": { 6 | "interface_name": "eth0", 7 | "static_ips": [ 8 | "10.88.0.2" 9 | ] 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "dns_enabled": false, 15 | "driver": "bridge", 16 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 17 | "internal": false, 18 | "ipv6_enabled": false, 19 | "name": "podman", 20 | "network_interface": "podman0", 21 | "subnets": [ 22 | { 23 | "gateway": "10.88.0.1", 24 | "subnet": "10.88.0.0/16" 25 | } 26 | ], 27 | "options": { 28 | "no_default_route": "true" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/testfiles/bridge-vethname.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "networks": { 5 | "podman": { 6 | "interface_name": "eth0", 7 | "static_ips": [ 8 | "10.88.0.2" 9 | ], 10 | "options": { 11 | "host_interface_name": "my-veth" 12 | } 13 | } 14 | }, 15 | "network_info": { 16 | "podman": { 17 | "dns_enabled": false, 18 | "driver": "bridge", 19 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 20 | "internal": false, 21 | "ipv6_enabled": false, 22 | "name": "podman", 23 | "network_interface": "podman0", 24 | "subnets": [ 25 | { 26 | "gateway": "10.88.0.1", 27 | "subnet": "10.88.0.0/16" 28 | } 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/testfiles/simplebridge-vrf.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "networks": { 5 | "podman": { 6 | "interface_name": "eth0", 7 | "static_ips": [ 8 | "10.88.0.2" 9 | ] 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "dns_enabled": false, 15 | "driver": "bridge", 16 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 17 | "internal": false, 18 | "ipv6_enabled": false, 19 | "name": "podman", 20 | "network_interface": "podman0", 21 | "subnets": [ 22 | { 23 | "gateway": "10.88.0.1", 24 | "subnet": "10.88.0.0/16" 25 | } 26 | ], 27 | "options": { 28 | "vrf": "test-vrf" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/config/setupopts.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "port_mappings": [ 5 | { 6 | "host_ip": "127.0.0.1", 7 | "container_port": 5000, 8 | "host_port": 5001, 9 | "range": 3, 10 | "protocol": "tcp" 11 | } 12 | ], 13 | "networks": { 14 | "defaultNetwork": { 15 | "interface_name": "eth0" 16 | } 17 | }, 18 | "network_info": { 19 | "defaultNetwork": { 20 | "dns_enabled": true, 21 | "driver": "bridge", 22 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 23 | "internal": false, 24 | "ipv6_enabled": true, 25 | "name": "defaultNetwork", 26 | "network_interface": "podman0", 27 | "subnets": [ 28 | { 29 | "gateway": "192.168.43.1", 30 | "subnet": "192.168.43.0/24" 31 | } 32 | ] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/testfiles/bridge-vethname-exists.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "networks": { 5 | "podman": { 6 | "interface_name": "eth0", 7 | "static_ips": [ 8 | "10.88.0.2" 9 | ], 10 | "options": { 11 | "host_interface_name": "podman0" 12 | } 13 | } 14 | }, 15 | "network_info": { 16 | "podman": { 17 | "dns_enabled": false, 18 | "driver": "bridge", 19 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 20 | "internal": false, 21 | "ipv6_enabled": false, 22 | "name": "podman", 23 | "network_interface": "podman0", 24 | "subnets": [ 25 | { 26 | "gateway": "10.88.0.1", 27 | "subnet": "10.88.0.0/16" 28 | } 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/testfiles/ipvlan-mtu.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2" 8 | ], 9 | "interface_name": "eth0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "name": "podman", 15 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 16 | "driver": "ipvlan", 17 | "network_interface": "dummy0", 18 | "subnets": [ 19 | { 20 | "subnet": "10.88.0.0/16", 21 | "gateway": "10.88.0.1" 22 | } 23 | ], 24 | "ipv6_enabled": false, 25 | "internal": false, 26 | "dns_enabled": false, 27 | "ipam_options": { 28 | "driver": "host-local" 29 | }, 30 | "options": { 31 | "mtu": "1400" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/testfiles/macvlan-mtu.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2" 8 | ], 9 | "interface_name": "eth0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "name": "podman", 15 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 16 | "driver": "macvlan", 17 | "network_interface": "dummy0", 18 | "subnets": [ 19 | { 20 | "subnet": "10.88.0.0/16", 21 | "gateway": "10.88.0.1" 22 | } 23 | ], 24 | "ipv6_enabled": false, 25 | "internal": false, 26 | "dns_enabled": false, 27 | "ipam_options": { 28 | "driver": "host-local" 29 | }, 30 | "options": { 31 | "mtu": "1400" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test-dhcp/dnsmasqfiles/sample.conf: -------------------------------------------------------------------------------- 1 | # Set the interface on which dnsmasq operates. 2 | # If not set, all the interfaces is used. 3 | interface=brtest 4 | 5 | # To disable dnsmasq's DNS server functionality. 6 | port=0 7 | 8 | # To enable dnsmasq's DHCP server functionality. 9 | dhcp-range=172.172.1.50,172.172.1.150,255.255.255.0,12h 10 | #dhcp-range=192.168.0.50,192.168.0.150,12h 11 | 12 | # Set static IPs of other PCs and the Router. 13 | dhcp-host=90:9f:44:d8:16:fc,iptime,192.168.0.1,infinite # Router 14 | dhcp-host=31:25:99:36:c2:bb,server-right,192.168.0.3,infinite # PC1 15 | dhcp-host=ac:97:0e:f2:6f:ab,yul-x230,192.168.0.13,infinite # PC2 16 | 17 | # Set gateway as Router. Following two lines are identical. 18 | #dhcp-option=option:router,192.168.0.1 19 | dhcp-option=3,172.172.1.1 20 | 21 | # Set DNS server as Router. 22 | dhcp-option=6,172.172.1.1 23 | 24 | # Logging. 25 | log-facility=/var/log/dnsmasq.log # logfile path. 26 | log-async 27 | log-queries # log queries. 28 | log-dhcp # log dhcp related messages. 29 | -------------------------------------------------------------------------------- /test/testfiles/ipvlan-nodefaultroute.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2" 8 | ], 9 | "interface_name": "eth0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "name": "podman", 15 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 16 | "driver": "ipvlan", 17 | "network_interface": "dummy0", 18 | "subnets": [ 19 | { 20 | "subnet": "10.88.0.0/16", 21 | "gateway": "10.88.0.1" 22 | } 23 | ], 24 | "ipv6_enabled": false, 25 | "internal": false, 26 | "dns_enabled": true, 27 | "ipam_options": { 28 | "driver": "host-local" 29 | }, 30 | "options": { 31 | "no_default_route": "true" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/testfiles/macvlan-nodefaultroute.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2" 8 | ], 9 | "interface_name": "eth0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "name": "podman", 15 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 16 | "driver": "macvlan", 17 | "network_interface": "dummy0", 18 | "subnets": [ 19 | { 20 | "subnet": "10.88.0.0/16", 21 | "gateway": "10.88.0.1" 22 | } 23 | ], 24 | "ipv6_enabled": false, 25 | "internal": false, 26 | "dns_enabled": true, 27 | "ipam_options": { 28 | "driver": "host-local" 29 | }, 30 | "options": { 31 | "no_default_route": "true" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/testfiles/ipv6-bridge.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "f031bf33eecba75d0d84952337b1ceef6a239eb8e94b48aee0993d0791345325", 3 | "container_name": "somename", 4 | "networks": { 5 | "podman1": { 6 | "static_ips": [ 7 | "fd10:88:a::2" 8 | ], 9 | "interface_name": "eth0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman1": { 14 | "name": "podman1", 15 | "id": "ec79dd0cad82083c8ac5cc23e9542e4ddea813dff60d68258d36e84f6393b63b", 16 | "driver": "bridge", 17 | "network_interface": "podman1", 18 | "subnets": [ 19 | { 20 | "subnet": "fd10:88:a::/64", 21 | "gateway": "fd10:88:a::1" 22 | } 23 | ], 24 | "ipv6_enabled": true, 25 | "internal": false, 26 | "dns_enabled": false, 27 | "ipam_options": { 28 | "driver": "host-local" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test-dhcp/004-server.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | @test "SIGINT Clean up" { 8 | read -r -d '\0' input_config < Result> { 22 | Err(new_error!("create error")) 23 | } 24 | 25 | fn setup( 26 | &self, 27 | _netns: String, 28 | _opts: types::NetworkPluginExec, 29 | ) -> Result> { 30 | Err(new_error!("setup error")) 31 | } 32 | 33 | fn teardown( 34 | &self, 35 | _netns: String, 36 | _opts: types::NetworkPluginExec, 37 | ) -> Result<(), Box> { 38 | Err(new_error!("teardown error")) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/testfiles/invalid-port.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "port_mappings": [ 5 | { 6 | "host_ip": "abcd", 7 | "container_port": 8080, 8 | "host_port": 8080, 9 | "range": 1, 10 | "protocol": "tcp" 11 | } 12 | ], 13 | "networks": { 14 | "podman": { 15 | "interface_name": "eth0", 16 | "static_ips": [ 17 | "10.88.0.2" 18 | ] 19 | } 20 | }, 21 | "network_info": { 22 | "podman": { 23 | "dns_enabled": false, 24 | "driver": "bridge", 25 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 26 | "internal": false, 27 | "ipv6_enabled": false, 28 | "name": "podman", 29 | "network_interface": "podman0", 30 | "subnets": [ 31 | { 32 | "gateway": "10.88.0.1", 33 | "subnet": "10.88.0.0/16" 34 | } 35 | ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/firewall/fwnone.rs: -------------------------------------------------------------------------------- 1 | use crate::firewall; 2 | use crate::firewall::NetavarkResult; 3 | use crate::network::internal_types::{ 4 | PortForwardConfig, SetupNetwork, TearDownNetwork, TeardownPortForward, 5 | }; 6 | 7 | // Iptables driver - uses direct iptables commands via the iptables crate. 8 | pub struct Fwnone {} 9 | 10 | pub fn new() -> NetavarkResult> { 11 | Ok(Box::new(Fwnone {})) 12 | } 13 | 14 | impl firewall::FirewallDriver for Fwnone { 15 | fn driver_name(&self) -> &str { 16 | firewall::NONE 17 | } 18 | 19 | fn setup_network(&self, _network_setup: SetupNetwork) -> NetavarkResult<()> { 20 | Ok(()) 21 | } 22 | 23 | // teardown_network should only be called in the case of 24 | // a complete teardown. 25 | fn teardown_network(&self, _tear: TearDownNetwork) -> NetavarkResult<()> { 26 | Ok(()) 27 | } 28 | 29 | fn setup_port_forward(&self, _setup_portfw: PortForwardConfig) -> NetavarkResult<()> { 30 | Ok(()) 31 | } 32 | 33 | fn teardown_port_forward(&self, _tear: TeardownPortForward) -> NetavarkResult<()> { 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/testfiles/isolate1.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "01f0b94d5f4c1", 3 | "container_name": "isolatedcontainer1", 4 | "networks": { 5 | "isolate1": { 6 | "interface_name": "eth1", 7 | "static_ips": [ 8 | "10.89.0.2", 9 | "fd90::2" 10 | ] 11 | } 12 | }, 13 | "network_info": { 14 | "isolate1": { 15 | "dns_enabled": false, 16 | "driver": "bridge", 17 | "id": "f3ac3c903b76f20e8a122b1eb2ba393ab2519ba516626be5b490b66dc96b54b7", 18 | "internal": false, 19 | "ipv6_enabled": false, 20 | "name": "isolate1", 21 | "network_interface": "isolate1", 22 | "subnets": [ 23 | { 24 | "gateway": "10.89.0.1", 25 | "subnet": "10.89.0.0/24" 26 | }, 27 | { 28 | "subnet": "fd90::/64", 29 | "gateway": "fd90::1" 30 | } 31 | ], 32 | "options": { 33 | "isolate": "true" 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/testfiles/isolate2.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "01f0b94d5f4c1", 3 | "container_name": "isolatedcontainer1", 4 | "networks": { 5 | "isolate2": { 6 | "interface_name": "eth2", 7 | "static_ips": [ 8 | "10.89.1.2", 9 | "fd99::2" 10 | ] 11 | } 12 | }, 13 | "network_info": { 14 | "isolate2": { 15 | "dns_enabled": false, 16 | "driver": "bridge", 17 | "id": "f3ac3c903b76f20e8a122b1eb2ba393ab2519ba516626be5b490b66dc96b54b7", 18 | "internal": false, 19 | "ipv6_enabled": false, 20 | "name": "isolate2", 21 | "network_interface": "isolate2", 22 | "subnets": [ 23 | { 24 | "gateway": "10.89.1.1", 25 | "subnet": "10.89.1.0/24" 26 | }, 27 | { 28 | "subnet": "fd99::/64", 29 | "gateway": "fd99::1" 30 | } 31 | ], 32 | "options": { 33 | "isolate": "true" 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/testfiles/isolate3.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "01f0b94d5f4c1", 3 | "container_name": "isolatedcontainer1", 4 | "networks": { 5 | "isolate3": { 6 | "interface_name": "eth3", 7 | "static_ips": [ 8 | "10.89.2.2", 9 | "fd92::2" 10 | ] 11 | } 12 | }, 13 | "network_info": { 14 | "isolate3": { 15 | "dns_enabled": false, 16 | "driver": "bridge", 17 | "id": "f3ac3c903b76f20e8a122b1eb2ba393ab2519ba516626be5b490b66dc96b54b7", 18 | "internal": false, 19 | "ipv6_enabled": false, 20 | "name": "isolate3", 21 | "network_interface": "isolate3", 22 | "subnets": [ 23 | { 24 | "gateway": "10.89.2.1", 25 | "subnet": "10.89.2.0/24" 26 | }, 27 | { 28 | "subnet": "fd92::/64", 29 | "gateway": "fd92::1" 30 | } 31 | ], 32 | "options": { 33 | "isolate": "strict" 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/testfiles/isolate4.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "01f0b94d5f4c1", 3 | "container_name": "isolatedcontainer1", 4 | "networks": { 5 | "isolate4": { 6 | "interface_name": "eth4", 7 | "static_ips": [ 8 | "10.89.3.2", 9 | "fd93::2" 10 | ] 11 | } 12 | }, 13 | "network_info": { 14 | "isolate4": { 15 | "dns_enabled": false, 16 | "driver": "bridge", 17 | "id": "f3ac3c903b76f20e8a122b1eb2ba393ab2519ba516626be5b490b66dc96b54b7", 18 | "internal": false, 19 | "ipv6_enabled": false, 20 | "name": "isolate4", 21 | "network_interface": "isolate4", 22 | "subnets": [ 23 | { 24 | "gateway": "10.89.3.1", 25 | "subnet": "10.89.3.0/24" 26 | }, 27 | { 28 | "subnet": "fd93::/64", 29 | "gateway": "fd93::1" 30 | } 31 | ], 32 | "options": { 33 | "isolate": "strict" 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test-dhcp/003-teardown.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | 8 | @test "basic teardown" { 9 | read -r -d '\0' input_config < Result> { 20 | eprintln!("stderr create"); 21 | Ok(network) 22 | } 23 | 24 | fn setup( 25 | &self, 26 | _netns: String, 27 | _opts: types::NetworkPluginExec, 28 | ) -> Result> { 29 | eprintln!("stderr setup"); 30 | 31 | // StatusBlock response 32 | let response = types::StatusBlock { 33 | dns_server_ips: None, 34 | dns_search_domains: None, 35 | interfaces: None, 36 | }; 37 | 38 | Ok(response) 39 | } 40 | 41 | fn teardown( 42 | &self, 43 | _netns: String, 44 | _opts: types::NetworkPluginExec, 45 | ) -> Result<(), Box> { 46 | eprintln!("stderr teardown"); 47 | 48 | Ok(()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/600-bridge-vrf.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # bridge driver tests with vrf 4 | # 5 | 6 | load helpers 7 | 8 | @test vrf - bridge with vrf { 9 | run_in_host_netns ip link add test-vrf type vrf table 10 10 | run_in_host_netns ip link set dev test-vrf up 11 | 12 | run_netavark --file ${TESTSDIR}/testfiles/simplebridge-vrf.json setup $(get_container_netns_path) 13 | 14 | # check if vrf exists 15 | run_in_host_netns ip -j --details link show podman0 16 | result="$output" 17 | assert_json "$result" ".[].linkinfo.info_slave_kind" "==" "vrf" "Bridge has a vrf set" 18 | assert_json "$result" ".[].master" "==" "test-vrf" "Bridge has the correct vrf set" 19 | } 20 | 21 | @test vrf - simple bridge { 22 | run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) 23 | run_in_host_netns ip -j --details link show podman0 24 | result="$output" 25 | assert_json "$result" ".[].linkinfo.info_slave_kind" "==" "null" "VRF is not set" 26 | } 27 | 28 | @test vrf - non existent vrf { 29 | expected_rc=1 run_netavark --file ${TESTSDIR}/testfiles/simplebridge-vrf.json setup $(get_container_netns_path) 30 | result="$output" 31 | assert_json "$result" ".error" "==" "get vrf to set up bridge interface: Netlink error: No such device (os error 19)" "Attempt to set a non existent vrf" 32 | } 33 | 34 | -------------------------------------------------------------------------------- /test/testfiles/bridge-staticroutes.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "6ce776ea58b5", 3 | "container_name": "testcontainer", 4 | "networks": { 5 | "podman": { 6 | "interface_name": "eth0", 7 | "static_ips": [ 8 | "10.88.0.2" 9 | ] 10 | } 11 | }, 12 | "network_info": { 13 | "podman": { 14 | "dns_enabled": false, 15 | "driver": "bridge", 16 | "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", 17 | "internal": false, 18 | "ipv6_enabled": false, 19 | "name": "podman", 20 | "network_interface": "podman0", 21 | "subnets": [ 22 | { 23 | "gateway": "10.88.0.1", 24 | "subnet": "10.88.0.0/16" 25 | } 26 | ], 27 | "routes": [ 28 | { 29 | "destination": "10.89.0.0/24", 30 | "gateway": "10.88.0.2" 31 | }, 32 | { 33 | "destination": "10.90.0.0/24", 34 | "gateway": "10.88.0.3" 35 | }, 36 | { 37 | "destination": "10.92.0.0/24", 38 | "gateway": "10.91.0.1" 39 | } 40 | ] 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/network/constants.rs: -------------------------------------------------------------------------------- 1 | //Following module contains all the network constants 2 | 3 | // default search domain 4 | pub static PODMAN_DEFAULT_SEARCH_DOMAIN: &str = "dns.podman"; 5 | 6 | // IPAM drivers 7 | pub const IPAM_HOSTLOCAL: &str = "host-local"; 8 | pub const IPAM_DHCP: &str = "dhcp"; 9 | pub const IPAM_NONE: &str = "none"; 10 | 11 | pub const DRIVER_BRIDGE: &str = "bridge"; 12 | pub const DRIVER_IPVLAN: &str = "ipvlan"; 13 | pub const DRIVER_MACVLAN: &str = "macvlan"; 14 | 15 | pub const OPTION_ISOLATE: &str = "isolate"; 16 | pub const ISOLATE_OPTION_TRUE: &str = "true"; 17 | pub const ISOLATE_OPTION_FALSE: &str = "false"; 18 | pub const ISOLATE_OPTION_STRICT: &str = "strict"; 19 | pub const OPTION_MTU: &str = "mtu"; 20 | pub const OPTION_MODE: &str = "mode"; 21 | pub const OPTION_METRIC: &str = "metric"; 22 | pub const OPTION_NO_DEFAULT_ROUTE: &str = "no_default_route"; 23 | pub const OPTION_BCLIM: &str = "bclim"; 24 | pub const OPTION_VRF: &str = "vrf"; 25 | pub const OPTION_VLAN: &str = "vlan"; 26 | pub const OPTION_HOST_INTERFACE_NAME: &str = "host_interface_name"; 27 | 28 | /// 100 is the default metric for most Linux networking tools. 29 | pub const DEFAULT_METRIC: u32 = 100; 30 | 31 | pub const NO_CONTAINER_INTERFACE_ERROR: &str = "no container interface name given"; 32 | 33 | /// make sure this is the same rootful default as used in podman. 34 | pub const DEFAULT_CONFIG_DIR: &str = "/run/containers/networks"; 35 | -------------------------------------------------------------------------------- /docs/netavark-dhcp-proxy.md: -------------------------------------------------------------------------------- 1 | % netavark-dhcp-proxy(1) 2 | 3 | ## NAME 4 | netavark-dhcp-proxy - a proxy for DHCP interactions with containers 5 | 6 | ## SYNOPSIS 7 | **netavark-dhcp-proxy** [*options*] *command* 8 | 9 | ## DESCRIPTION 10 | When using DHCP with MacVLAN and containers, you need the container to either have 11 | an init system with DHCP clients or some sort of proxy server that can act on behalf 12 | of the container. The netavark-dhcp-proxy is the latter and should be used in 13 | combination with Podman and Netavark when setting up containers that wish to use 14 | DHCP and MacVLAN networking. 15 | 16 | **netavark-dhcp-proxy [GLOBAL OPTIONS]** 17 | 18 | ## GLOBAL OPTIONS 19 | 20 | #### **--activity-timeout, -a** 21 | Time in seconds when the proxy should exit if it has no leases. The default time 22 | is *300* seconds. A value of *0* disables the activity timeout. 23 | 24 | #### **--dir**=*path* 25 | 26 | The directory option is a path to store the lease backup files. The default is 27 | */run/podman/*. The lease name is *nv-proxy.leases*. 28 | 29 | #### **--uds** 30 | Set the unix domain socket directory instead of using the default. The default is 31 | */run/podman*. The socket name is *nv-proxy.sock*. 32 | 33 | #### **--help**, **-h** 34 | 35 | Print usage statement 36 | 37 | #### **--version**, **-v** 38 | 39 | Print the version 40 | 41 | 42 | ## HISTORY 43 | Sep 2022, Originally compiled by Brent Baude 44 | -------------------------------------------------------------------------------- /test/610-bridge-vethname.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # bridge driver tests with static veth names 4 | # 5 | 6 | load helpers 7 | 8 | @test bridge - valid veth name { 9 | run_netavark --file ${TESTSDIR}/testfiles/bridge-vethname.json setup $(get_container_netns_path) 10 | 11 | run_in_host_netns ip -j --details link show my-veth 12 | link_info="$output" 13 | assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Host veth is up" 14 | assert_json "$link_info" ".[].linkinfo.info_kind" == "veth" "The veth interface is actually a veth" 15 | assert_json "$link_info" ".[].master" "==" "podman0" "veth is part of the correct bridge" 16 | 17 | run_netavark --file ${TESTSDIR}/testfiles/bridge-vethname.json teardown $(get_container_netns_path) 18 | 19 | # check if the interface gets removed 20 | expected_rc=1 run_in_host_netns ip -j --details link show my-veth 21 | assert "$output" "==" 'Device "my-veth" does not exist.' 22 | } 23 | 24 | @test bridge - existing veth name { 25 | expected_rc=1 run_netavark --file ${TESTSDIR}/testfiles/bridge-vethname-exists.json setup $(get_container_netns_path) 26 | assert_json ".error" "create veth pair: interface eth0 already exists on container namespace or podman0 exists on host namespace: Netlink error: File exists (os error 17)" 27 | 28 | expected_rc=1 run_in_host_netns ip -j --details link show my-veth 29 | assert "$output" "==" 'Device "my-veth" does not exist.' 30 | } 31 | -------------------------------------------------------------------------------- /test/001-basic.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | 8 | @test "netavark version" { 9 | run_netavark --version 10 | assert "$output" =~ "netavark 1\.[0-9]+\.[0-9]+(-rc|-dev)?" "expected version" 11 | 12 | run_netavark version 13 | json="$output" 14 | assert_json "$json" ".version" =~ "^1\.[0-9]+\.[0-9]+(-rc[0-9]|-dev)?" "correct version" 15 | assert_json "$json" ".commit" =~ "[0-9a-f]{40}" "shows commit sha" 16 | assert_json "$json" ".build_time" =~ "20.*" "show build date" 17 | assert_json "$json" ".target" =~ ".*" "contains target string" 18 | } 19 | 20 | @test "netavark error - invalid ns path" { 21 | expected_rc=1 run_netavark -f ${TESTSDIR}/testfiles/simplebridge.json setup /test/1 22 | assert_json ".error" "invalid namespace path: IO error: No such file or directory (os error 2)" "Namespace path does not exists" 23 | } 24 | 25 | @test "netavark error - invalid config path" { 26 | expected_rc=1 run_netavark -f /test/1 setup $(get_container_netns_path) 27 | assert_json ".error" "failed to load network options: IO error: No such file or directory (os error 2)" "Config file does not exists" 28 | } 29 | 30 | @test "netavark - check non utf-8 paths" { 31 | # do not use run_netavark here as it sets --config 32 | run_helper $NETAVARK --config $'/tmp/\xff.test' version 33 | json="$output" 34 | assert_json "$json" ".version" =~ "^1\.[0-9]+\.[0-9]+(-rc[0-9]|-dev)?" "correct version" 35 | } 36 | -------------------------------------------------------------------------------- /test/testfiles/ipv6-bridge-staticroutes.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "f031bf33eecba75d0d84952337b1ceef6a239eb8e94b48aee0993d0791345325", 3 | "container_name": "somename", 4 | "networks": { 5 | "podman1": { 6 | "static_ips": [ 7 | "fd10:88:a::2" 8 | ], 9 | "interface_name": "eth0" 10 | } 11 | }, 12 | "network_info": { 13 | "podman1": { 14 | "name": "podman1", 15 | "id": "ec79dd0cad82083c8ac5cc23e9542e4ddea813dff60d68258d36e84f6393b63b", 16 | "driver": "bridge", 17 | "network_interface": "podman1", 18 | "subnets": [ 19 | { 20 | "subnet": "fd10:88:a::/64", 21 | "gateway": "fd10:88:a::1" 22 | } 23 | ], 24 | "routes": [ 25 | { 26 | "destination": "fd10:89:b::/64", 27 | "gateway": "fd10:88:a::ac02" 28 | }, 29 | { 30 | "destination": "fd10:89:c::/64", 31 | "gateway": "fd10:88:a::ac03" 32 | }, 33 | { 34 | "destination": "fd10:51:b::/64", 35 | "gateway": "fd10:49:b::30" 36 | } 37 | ], 38 | "ipv6_enabled": true, 39 | "internal": false, 40 | "dns_enabled": false, 41 | "ipam_options": { 42 | "driver": "host-local" 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/proto/proxy.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package netavark_proxy; 3 | 4 | service NetavarkProxy { 5 | // Client side streaming to detect client disconnection 6 | rpc Setup(NetworkConfig) returns (Lease) {} 7 | rpc Teardown(NetworkConfig) returns (Lease) {} 8 | rpc Clean(Empty) returns (OperationResponse) {} 9 | } 10 | // Netavark sends the proxy the Network Configuration that it wants to setup 11 | message NetworkConfig { 12 | string host_iface = 1; 13 | string container_iface = 2; 14 | string container_mac_addr = 3; 15 | string domain_name = 4; 16 | string host_name = 5; 17 | Version version = 6; 18 | string ns_path = 7; 19 | string container_id = 8; 20 | 21 | } 22 | // Lease can either contain a IPv4 or IPv6 DHCP lease, and the common IP information 23 | message Lease { 24 | uint32 t1 = 1; 25 | uint32 t2 = 2; 26 | uint32 lease_time = 3; 27 | uint32 mtu = 4; 28 | string domain_name = 5; 29 | string mac_address= 6; 30 | bool isV6 = 10; 31 | string siaddr = 11; 32 | string yiaddr = 12; 33 | string srv_id = 16; 34 | string subnet_mask = 17; 35 | string broadcast_addr = 18; 36 | repeated string dns_servers = 19; 37 | repeated string gateways = 20; 38 | repeated string ntp_servers = 21; 39 | string host_name = 22; 40 | } 41 | 42 | // Empty Message to send when calling for a shutdown 43 | message Empty{} 44 | 45 | // Response to netavark on successful teardown 46 | message OperationResponse { 47 | bool success = 1; 48 | } 49 | 50 | enum Version { 51 | V4 = 0; 52 | V6 = 1; 53 | } 54 | 55 | message NvIpv4Addr { 56 | bytes octets = 1; 57 | } 58 | 59 | message NvIpv6Addr { 60 | bytes octets = 1; 61 | } 62 | -------------------------------------------------------------------------------- /test/testfiles/bridge-port-hostip.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "f922ffdda5718b26ea585a500d5ad05191da5461b06d6f62e4d1f66ca901a253", 3 | "container_name": "sharp_gould", 4 | "port_mappings": [ 5 | { 6 | "host_ip": "192.168.188.25", 7 | "container_port": 8080, 8 | "host_port": 8080, 9 | "range": 1, 10 | "protocol": "tcp" 11 | }, 12 | { 13 | "host_ip": "192.168.188.24", 14 | "container_port": 8080, 15 | "host_port": 8080, 16 | "range": 1, 17 | "protocol": "tcp" 18 | } 19 | ], 20 | "networks": { 21 | "podman": { 22 | "static_ips": [ 23 | "10.88.0.14" 24 | ], 25 | "aliases": [ 26 | "f922ffdda571" 27 | ], 28 | "interface_name": "eth0" 29 | } 30 | }, 31 | "network_info": { 32 | "podman": { 33 | "name": "podman", 34 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 35 | "driver": "bridge", 36 | "network_interface": "podman0", 37 | "created": "2024-09-05T15:00:04.45111926+02:00", 38 | "subnets": [ 39 | { 40 | "subnet": "10.88.0.0/16", 41 | "gateway": "10.88.0.1" 42 | } 43 | ], 44 | "ipv6_enabled": false, 45 | "internal": false, 46 | "dns_enabled": false, 47 | "ipam_options": { 48 | "driver": "host-local" 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/testfiles/bridge-port-tcp-udp.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "f922ffdda5718b26ea585a500d5ad05191da5461b06d6f62e4d1f66ca901a253", 3 | "container_name": "sharp_gould", 4 | "port_mappings": [ 5 | { 6 | "host_ip": "192.168.188.25", 7 | "container_port": 8080, 8 | "host_port": 8080, 9 | "range": 1, 10 | "protocol": "tcp" 11 | }, 12 | { 13 | "host_ip": "192.168.188.25", 14 | "container_port": 8080, 15 | "host_port": 8080, 16 | "range": 1, 17 | "protocol": "udp" 18 | } 19 | ], 20 | "networks": { 21 | "podman": { 22 | "static_ips": [ 23 | "10.88.0.14" 24 | ], 25 | "aliases": [ 26 | "f922ffdda571" 27 | ], 28 | "interface_name": "eth0" 29 | } 30 | }, 31 | "network_info": { 32 | "podman": { 33 | "name": "podman", 34 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 35 | "driver": "bridge", 36 | "network_interface": "podman0", 37 | "created": "2024-09-05T15:00:04.45111926+02:00", 38 | "subnets": [ 39 | { 40 | "subnet": "10.88.0.0/16", 41 | "gateway": "10.88.0.1" 42 | } 43 | ], 44 | "ipv6_enabled": false, 45 | "internal": false, 46 | "dns_enabled": false, 47 | "ipam_options": { 48 | "driver": "host-local" 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/testfiles/ipvlan-staticroutes.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2", 8 | "fd:1f1f::2" 9 | ], 10 | "interface_name": "eth0" 11 | } 12 | }, 13 | "network_info": { 14 | "podman": { 15 | "name": "podman", 16 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 17 | "driver": "ipvlan", 18 | "network_interface": "dummy0", 19 | "subnets": [ 20 | { 21 | "subnet": "10.88.0.0/16", 22 | "gateway": "10.88.0.1" 23 | }, 24 | { 25 | "subnet": "fd:1f1f::/64", 26 | "gateway": "fd:1f1f::1" 27 | } 28 | ], 29 | "routes": [ 30 | { 31 | "destination": "10.89.0.0/24", 32 | "gateway": "10.88.0.2" 33 | }, 34 | { 35 | "destination": "10.90.0.0/24", 36 | "gateway": "10.88.0.3" 37 | }, 38 | { 39 | "destination": "10.92.0.0/24", 40 | "gateway": "10.91.0.1" 41 | }, 42 | { 43 | "destination": "fd:2f2f::/64", 44 | "gateway": "fd:1f1f::20" 45 | } 46 | ], 47 | "ipv6_enabled": true, 48 | "internal": false, 49 | "dns_enabled": true, 50 | "ipam_options": { 51 | "driver": "host-local" 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/testfiles/macvlan-staticroutes.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "someID", 3 | "container_name": "someName", 4 | "networks": { 5 | "podman": { 6 | "static_ips": [ 7 | "10.88.0.2", 8 | "fd:1f1f::2" 9 | ], 10 | "interface_name": "eth0" 11 | } 12 | }, 13 | "network_info": { 14 | "podman": { 15 | "name": "podman", 16 | "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", 17 | "driver": "macvlan", 18 | "network_interface": "dummy0", 19 | "subnets": [ 20 | { 21 | "subnet": "10.88.0.0/16", 22 | "gateway": "10.88.0.1" 23 | }, 24 | { 25 | "subnet": "fd:1f1f::/64", 26 | "gateway": "fd:1f1f::1" 27 | } 28 | ], 29 | "routes": [ 30 | { 31 | "destination": "10.89.0.0/24", 32 | "gateway": "10.88.0.2" 33 | }, 34 | { 35 | "destination": "10.90.0.0/24", 36 | "gateway": "10.88.0.3" 37 | }, 38 | { 39 | "destination": "10.92.0.0/24", 40 | "gateway": "10.91.0.1" 41 | }, 42 | { 43 | "destination": "fd:2f2f::/64", 44 | "gateway": "fd:1f1f::20" 45 | } 46 | ], 47 | "ipv6_enabled": true, 48 | "internal": false, 49 | "dns_enabled": true, 50 | "ipam_options": { 51 | "driver": "host-local" 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contrib/cirrus/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script configures the CI runtime environment. It's intended 4 | # to be used by Cirrus-CI, not humans. 5 | 6 | set -e 7 | 8 | source $(dirname $0)/lib.sh 9 | 10 | # Only do this once 11 | if [[ -r "/etc/ci_environment" ]]; then 12 | msg "It appears ${BASH_SOURCE[0]} already ran, exiting." 13 | exit 0 14 | fi 15 | trap "complete_setup" EXIT 16 | 17 | 18 | msg "************************************************************" 19 | msg "Setting up runtime environment" 20 | msg "************************************************************" 21 | show_env_vars 22 | 23 | req_env_vars AARDVARK_DNS_URL AARDVARK_DNS_BRANCH 24 | cd /usr/libexec/podman 25 | rm -vf aardvark-dns* 26 | if showrun curl --fail --location -o /tmp/aardvark-dns.zip "$AARDVARK_DNS_URL" && \ 27 | unzip -o /tmp/aardvark-dns.zip; then 28 | 29 | if [[ $(uname -m) != "x86_64" ]]; then 30 | showrun mv aardvark-dns.$(uname -m)-unknown-linux-gnu aardvark-dns 31 | fi 32 | showrun chmod a+x /usr/libexec/podman/aardvark-dns 33 | else 34 | warn "Error downloading/extracting the latest pre-compiled aardvark binary from CI" 35 | showrun cargo install \ 36 | --root /usr/libexec/podman \ 37 | --git https://github.com/containers/aardvark-dns \ 38 | --branch "$AARDVARK_DNS_BRANCH" 39 | mv -v /usr/libexec/podman/bin/aardvark-dns /usr/libexec/podman 40 | fi 41 | # show aardvark commit in CI logs 42 | showrun /usr/libexec/podman/aardvark-dns version 43 | 44 | # Warning, this isn't the end. An exit-handler is installed to finalize 45 | # setup of env. vars. This is required for runner.sh to operate properly. 46 | # See complete_setup() in lib.sh for details. 47 | -------------------------------------------------------------------------------- /src/test/test.rs: -------------------------------------------------------------------------------- 1 | //use super::*; 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use std::ffi::OsString; 6 | 7 | use netavark::network; 8 | #[test] 9 | // Test setup options loader 10 | fn test_setup_opts_load() { 11 | match network::types::NetworkOptions::load(Some(OsString::from( 12 | "src/test/config/setupopts.test.json", 13 | ))) { 14 | Ok(_) => {} 15 | Err(e) => panic!("{}", e), 16 | } 17 | } 18 | 19 | // Test if we can deserialize values correctly 20 | #[test] 21 | fn test_setup_opts_assert() { 22 | match network::types::NetworkOptions::load(Some(OsString::from( 23 | "src/test/config/setupopts.test.json", 24 | ))) { 25 | Ok(setupopts) => { 26 | assert_eq!(setupopts.container_name, "testcontainer") 27 | } 28 | Err(e) => panic!("{}", e), 29 | } 30 | } 31 | 32 | // Deserialize values correctly 33 | // Try mutating deserialized struct 34 | #[test] 35 | fn test_setup_opts_mutability() { 36 | match network::types::NetworkOptions::load(Some(OsString::from( 37 | "src/test/config/setupopts.test.json", 38 | ))) { 39 | Ok(mut setupopts) => { 40 | assert_eq!(setupopts.container_name, "testcontainer"); 41 | setupopts.container_name = "mutatedcontainername".to_string(); 42 | assert_eq!(setupopts.container_name, "mutatedcontainername"); 43 | } 44 | Err(e) => panic!("{}", e), 45 | } 46 | } 47 | 48 | // Test commands::setup::ns_checks works correctly 49 | #[test] 50 | fn test_ns_checks() { 51 | assert!(network::validation::ns_checks("src/test/config/setupopts.test.json").is_ok()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docs/netavark.1.md: -------------------------------------------------------------------------------- 1 | % netavark(1) 2 | 3 | ## NAME 4 | netavark - Configure a given network namespace for use by a container 5 | 6 | ## SYNOPSIS 7 | **netavark** [*options*] *command* *network namespace path* 8 | 9 | ## DESCRIPTION 10 | Netavark configures a network namespace according to a configuration read from STDIN. The configuration is JSON formatted. 11 | 12 | ## GLOBAL OPTIONS 13 | #### **--file**, **-f** 14 | 15 | Instead of reading from STDIN, read the configuration to be applied from the given file. **-f -** may also be used to flag reading from STDIN. 16 | 17 | ## COMMANDS 18 | 19 | ### netavark setup 20 | 21 | The setup command configures the given network namespace with the given configuration, creating any interfaces and firewall rules necessary. 22 | 23 | ### netavark teardown 24 | 25 | 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). 26 | 27 | ### CONFIGURATION FORMAT 28 | 29 | The configuration accepted is the same for both setup and teardown. It is JSON formatted. 30 | 31 | Format is https://github.com/containers/podman/blob/cd7b48198c38c5028540e85dc72dd3406f4318f0/libpod/network/types/network.go#L164-L173 but we will also send a Networks array including all the network definitions (https://github.com/containers/podman/blob/cd7b48198c38c5028540e85dc72dd3406f4318f0/libpod/network/types/network.go#L32-L62) 32 | TODO: Transcribe configuration into here in a nice tabular format 33 | 34 | ## EXAMPLE 35 | 36 | netavark setup /run/user/1000/podman/netns/d11d1f9c499d 37 | 38 | netavark -f /run/podman/828b0508ae64.conf teardown /run/podman/netns/828b0508ae64 39 | 40 | ## SEE ALSO 41 | podman(1) 42 | 43 | ## HISTORY 44 | September 2021, Originally compiled by Matt Heon 45 | 46 | -------------------------------------------------------------------------------- /src/test/config/twoNetworks.json: -------------------------------------------------------------------------------- 1 | { 2 | "container_id": "2d0fe608dc86d7ba65dcec724a335b618328f08daa62afd2ee7fa9d41f74e5a9", 3 | "container_name": "", 4 | "networks": { 5 | "podman1": { 6 | "static_ips": [ 7 | "10.0.0.2" 8 | ], 9 | "interface_name": "eth0" 10 | }, 11 | "podman2": { 12 | "static_ips": [ 13 | "10.1.0.2" 14 | ], 15 | "interface_name": "eth1" 16 | } 17 | }, 18 | "network_info": { 19 | "podman1": { 20 | "name": "podman1", 21 | "id": "4937f73ac0df011d4f2848d5f83f5c20b707e71a8d98789bbe80d8f64a815e79", 22 | "driver": "bridge", 23 | "network_interface": "podman1", 24 | "created": "2021-11-04T19:08:39.124321192+01:00", 25 | "subnets": [ 26 | { 27 | "subnet": "10.0.0.0/24", 28 | "gateway": "10.0.0.1" 29 | } 30 | ], 31 | "ipv6_enabled": false, 32 | "internal": false, 33 | "dns_enabled": false, 34 | "ipam_options": { 35 | "driver": "host-local" 36 | } 37 | }, 38 | "podman2": { 39 | "name": "podman2", 40 | "id": "488a7d9be4fa72a5b80811bd847aac1d99d1a09060739b4e08687949c957cda8", 41 | "driver": "bridge", 42 | "network_interface": "podman2", 43 | "created": "2021-11-04T19:08:39.124800596+01:00", 44 | "subnets": [ 45 | { 46 | "subnet": "10.1.0.0/24", 47 | "gateway": "10.1.0.1" 48 | } 49 | ], 50 | "ipv6_enabled": false, 51 | "internal": false, 52 | "dns_enabled": false, 53 | "ipam_options": { 54 | "driver": "host-local" 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test-dhcp/005-renew.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # Test that release is working after lease timeout 4 | # 5 | 6 | load helpers 7 | 8 | 9 | @test "release after timeout" { 10 | read -r -d '\0' input_config <, 20 | } 21 | 22 | impl Update { 23 | /// Updates network dns servers for an already configured network 24 | pub fn new(network_name: String, network_dns_servers: Vec) -> Self { 25 | Self { 26 | network_name, 27 | network_dns_servers, 28 | } 29 | } 30 | 31 | pub fn exec( 32 | &mut self, 33 | config_dir: Option, 34 | aardvark_bin: OsString, 35 | rootless: bool, 36 | ) -> NetavarkResult<()> { 37 | let dns_port = core_utils::get_netavark_dns_port()?; 38 | let config_dir = get_config_dir(config_dir, "update")?; 39 | if Path::new(&aardvark_bin).exists() { 40 | let path = Path::new(&config_dir).join("aardvark-dns"); 41 | 42 | let aardvark_interface = Aardvark::new(path, rootless, aardvark_bin, dns_port); 43 | // if empty network_dns_servers are passed, pass empty array instead of `[""]` 44 | if self.network_dns_servers.len() == 1 && self.network_dns_servers[0].is_empty() { 45 | self.network_dns_servers = Vec::new(); 46 | } 47 | if let Err(err) = aardvark_interface 48 | .modify_network_dns_servers(&self.network_name, &self.network_dns_servers) 49 | { 50 | return Err(NetavarkError::wrap( 51 | "unable to modify network dns servers", 52 | err, 53 | )); 54 | } 55 | } 56 | 57 | debug!("Network update complete"); 58 | Ok(()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "netavark" 3 | version = "1.14.0-dev" 4 | edition = "2021" 5 | authors = ["github.com/containers"] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | description = "A container network stack" 9 | homepage = "https://github.com/containers/netavark" 10 | repository = "https://github.com/containers/netavark" 11 | categories = ["virtualization"] 12 | exclude = ["/.cirrus.yml", "/.github/*", "/hack/*"] 13 | build = "build.rs" 14 | rust-version = "1.76" 15 | 16 | [package.metadata.vendor-filter] 17 | platforms = ["*-unknown-linux-*"] 18 | tier = "2" 19 | 20 | [[bin]] 21 | name = "netavark" 22 | path = "src/main.rs" 23 | 24 | [[bin]] 25 | name = "netavark-dhcp-proxy-client" 26 | path = "src/dhcp_proxy_client/client.rs" 27 | 28 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 29 | [features] 30 | default = ["serde", "deps-serde"] 31 | deps-serde = ["chrono/serde", "url/serde"] 32 | 33 | [dependencies] 34 | anyhow = "1.0.93" 35 | clap = { version = "~4.5.23", features = ["derive", "env"] } 36 | env_logger = "0.11.6" 37 | ipnet = { version = "2.11.0", features = ["serde"] } 38 | iptables = "0.5.2" 39 | libc = "0.2.157" 40 | log = "0.4.24" 41 | serde = { version = "1.0.213", features = ["derive"], optional = true } 42 | serde-value = "0.7.0" 43 | serde_json = "1.0.136" 44 | sysctl = "0.6.0" 45 | url = "2.5.3" 46 | zbus = { version = "4.3.1" } 47 | nix = { version = "0.29.0", features = ["sched", "signal", "user"] } 48 | rand = "0.8.5" 49 | sha2 = "0.10.8" 50 | netlink-packet-utils = "0.5.2" 51 | netlink-packet-route = "0.21.0" 52 | netlink-packet-core = "0.7.0" 53 | nftables = "0.5.0" 54 | fs2 = "0.4.3" 55 | netlink-sys = "0.8.7" 56 | tokio = { version = "1.43.0", features = ["rt", "rt-multi-thread", "signal", "fs"] } 57 | tokio-stream = { version = "0.1.17", features = ["net"] } 58 | tonic = "0.12.3" 59 | mozim = "0.2.5" 60 | prost = "0.13.4" 61 | futures-channel = "0.3.31" 62 | futures-core = "0.3.31" 63 | futures-util = "0.3.31" 64 | nispor = "1.2.22" 65 | tower = { version = "0.5.2", features = ["util"] } 66 | hyper-util = "0.1.10" 67 | 68 | [build-dependencies] 69 | chrono = { version = "0.4.39", default-features = false, features = ["clock"] } 70 | tonic-build = "0.12.3" 71 | 72 | [dev-dependencies] 73 | once_cell = "1.20.2" 74 | rand = "0.8.5" 75 | tempfile = "3.16.0" 76 | -------------------------------------------------------------------------------- /src/dhcp_proxy/types.rs: -------------------------------------------------------------------------------- 1 | use crate::dhcp_proxy::lib::g_rpc::NetworkConfig; 2 | use crate::error::NetavarkError; 3 | use ipnet::PrefixLenError; 4 | use mozim::DhcpError; 5 | use mozim::ErrorKind::InvalidArgument; 6 | use std::net::AddrParseError; 7 | use std::num::ParseIntError; 8 | use std::str::FromStr; 9 | use std::string::ToString; 10 | use tonic::{Code, Status}; 11 | impl FromStr for NetworkConfig { 12 | type Err = ParseIntError; 13 | 14 | fn from_str(_s: &str) -> Result { 15 | // s is actually a string so if we intend to generate 16 | // a `NetworkConfig` object from `s` parse `s` and populate 17 | // instead of default empty values 18 | Ok(NetworkConfig { 19 | host_iface: "".to_string(), 20 | container_mac_addr: "".to_string(), 21 | domain_name: "".to_string(), 22 | host_name: "".to_string(), 23 | version: 0, 24 | ns_path: "".to_string(), 25 | container_iface: "".to_string(), 26 | container_id: "".to_string(), 27 | }) 28 | } 29 | } 30 | 31 | #[derive(Debug, Clone)] 32 | pub struct ProxyError(String); 33 | 34 | pub trait CustomErr { 35 | fn new(msg: String) -> Self; 36 | } 37 | 38 | impl CustomErr for ProxyError { 39 | fn new(msg: String) -> Self { 40 | ProxyError(msg) 41 | } 42 | } 43 | 44 | impl std::fmt::Display for ProxyError { 45 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 46 | write!(f, "{}", self.0) 47 | } 48 | } 49 | 50 | impl From for Status { 51 | fn from(pe: ProxyError) -> Self { 52 | Status::new(Code::Unknown, pe.to_string()) 53 | } 54 | } 55 | 56 | impl From for ProxyError { 57 | fn from(cause: NetavarkError) -> Self { 58 | ProxyError::new(cause.to_string()) 59 | } 60 | } 61 | 62 | impl From for ProxyError { 63 | fn from(cause: PrefixLenError) -> Self { 64 | ProxyError::new(cause.to_string()) 65 | } 66 | } 67 | 68 | impl From for ProxyError { 69 | fn from(e: AddrParseError) -> Self { 70 | ProxyError::new(e.to_string()) 71 | } 72 | } 73 | 74 | impl From for DhcpError { 75 | fn from(e: ProxyError) -> Self { 76 | DhcpError::new(InvalidArgument, e.to_string()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/500-plugin.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # macvlan driver test 4 | # 5 | 6 | load helpers 7 | 8 | 9 | # create config for plugin with the name as first arg 10 | function get_conf() { 11 | cat <containers/automation//renovate/defaults.json5" 43 | ], 44 | 45 | /************************************************* 46 | *** Repository-specific configuration options *** 47 | *************************************************/ 48 | 49 | // Don't leave dep. update. PRs "hanging", assign them to people. 50 | "assignees": ["containers/netavark-maintainers"], 51 | 52 | /************************************************** 53 | ***** Manager-specific configuration options ***** 54 | **************************************************/ 55 | 56 | "dockerfile": { 57 | // Renovate has a hard-time managing base images for 58 | // Fedora and CentOS (see contrib/container_images/). Disable 59 | // all Dockerfile baes-image management. 60 | "enabled": false 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /contrib/cirrus/runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | # This script runs in the Cirrus CI environment, invoked from .cirrus.yml . 6 | # It can also be invoked manually in a `hack/get_ci_cm.sh` environment, 7 | # documentation of said usage is TBI. 8 | # 9 | # The principal deciding factor is the first argument. For any 10 | # given value 'xyz' there must be a function '_run_xyz' to handle that 11 | # argument. 12 | 13 | source $(dirname ${BASH_SOURCE[0]})/lib.sh 14 | 15 | _run_noarg() { 16 | die "runner.sh must be called with a single argument" 17 | } 18 | 19 | _run_build() { 20 | # Assume we're on a fast VM, compile everything needed by the 21 | # rest of CI since subsequent tasks may have limited resources. 22 | make all debug=1 23 | make build_unit # reuses some debug binaries 24 | make all # optimized/non-debug binaries 25 | # This will get scooped up and become part of the artifact archive. 26 | # Identify where the binary came from to benefit downstream consumers. 27 | cat | tee bin/netavark.info << EOF 28 | repo: $CIRRUS_REPO_CLONE_URL 29 | branch: $CIRRUS_BASE_BRANCH 30 | title: $CIRRUS_CHANGE_TITLE 31 | commit: $CIRRUS_CHANGE_IN_REPO 32 | build: https://cirrus-ci.com/build/$CIRRUS_BUILD_ID 33 | task: https://cirrus-ci.com/task/$CIRRUS_TASK_ID 34 | EOF 35 | } 36 | 37 | _run_build_aarch64() { 38 | _run_build 39 | } 40 | 41 | _run_validate() { 42 | make validate 43 | } 44 | 45 | _run_validate_aarch64() { 46 | _run_validate 47 | } 48 | 49 | _run_unit() { 50 | make unit 51 | } 52 | 53 | _run_unit_aarch64() { 54 | _run_unit 55 | } 56 | 57 | _run_integration() { 58 | make integration 59 | } 60 | 61 | _run_integration_aarch64() { 62 | _run_integration 63 | } 64 | 65 | show_env_vars 66 | 67 | msg "************************************************************" 68 | msg "Toolchain details" 69 | msg "************************************************************" 70 | 71 | rustc --version 72 | cargo --version 73 | 74 | msg "************************************************************" 75 | msg "Runner executing '$1' on $OS_REL_VER" 76 | msg "************************************************************" 77 | 78 | ((${SETUP_ENVIRONMENT:-0})) || \ 79 | die "Expecting setup.sh to have completed successfully" 80 | 81 | cd "${CIRRUS_WORKING_DIR}/" 82 | 83 | handler="_run_${1:-noarg}" 84 | 85 | if [ "$(type -t $handler)" != "function" ]; then 86 | die "Unknown/Unsupported runner.sh argument '$1'" 87 | fi 88 | 89 | $handler 90 | -------------------------------------------------------------------------------- /src/commands/firewalld_reload.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::{OsStr, OsString}, 3 | path::Path, 4 | }; 5 | 6 | use zbus::{blocking::Connection, proxy, CacheProperties}; 7 | 8 | use crate::{ 9 | error::{ErrorWrap, NetavarkResult}, 10 | firewall::{get_supported_firewall_driver, state::read_fw_config}, 11 | network::constants, 12 | }; 13 | 14 | #[proxy( 15 | interface = "org.fedoraproject.FirewallD1", 16 | default_service = "org.fedoraproject.FirewallD1", 17 | default_path = "/org/fedoraproject/FirewallD1" 18 | )] 19 | trait FirewallDDbus {} 20 | 21 | const SIGNAL_NAME: &str = "Reloaded"; 22 | 23 | pub fn listen(config_dir: Option) -> NetavarkResult<()> { 24 | let config_dir = Path::new( 25 | config_dir 26 | .as_deref() 27 | .unwrap_or(OsStr::new(constants::DEFAULT_CONFIG_DIR)), 28 | ); 29 | log::debug!("looking for firewall configs in {:?}", config_dir); 30 | 31 | let conn = Connection::system()?; 32 | let proxy = FirewallDDbusProxyBlocking::builder(&conn) 33 | .cache_properties(CacheProperties::No) 34 | .build()?; 35 | 36 | // Setup fw rules on start because we are started after firewalld 37 | // this means at the time firewalld stated the fw rules were flushed 38 | // and we need to add them back. 39 | // It is important to keep things like "systemctl restart firewalld" working. 40 | reload_rules(config_dir); 41 | 42 | // This loops forever until the process is killed or there is some dbus error. 43 | for _ in proxy.0.receive_signal(SIGNAL_NAME)? { 44 | log::debug!("got firewalld {} signal", SIGNAL_NAME); 45 | reload_rules(config_dir); 46 | } 47 | 48 | Ok(()) 49 | } 50 | 51 | fn reload_rules(config_dir: &Path) { 52 | if let Err(e) = reload_rules_inner(config_dir) { 53 | log::error!("failed to reload firewall rules: {e}"); 54 | } 55 | } 56 | 57 | fn reload_rules_inner(config_dir: &Path) -> NetavarkResult<()> { 58 | let conf = read_fw_config(config_dir).wrap("read firewall config")?; 59 | // If we got no conf there are no containers so nothing to do. 60 | if let Some(conf) = conf { 61 | let fw_driver = get_supported_firewall_driver(Some(conf.driver))?; 62 | 63 | for net in conf.net_confs { 64 | fw_driver.setup_network(net)?; 65 | } 66 | for port in &conf.port_confs { 67 | fw_driver.setup_port_forward(port.into())?; 68 | } 69 | log::info!("Successfully reloaded firewall rules"); 70 | } 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /contrib/cirrus/lib.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Library of common, shared utility functions. This file is intended 4 | # to be sourced by other scripts, not called directly. 5 | 6 | # BEGIN Global export of all variables 7 | set -a 8 | 9 | # Automation library installed at image-build time, 10 | # defining $AUTOMATION_LIB_PATH in this file. 11 | if [[ -r "/etc/automation_environment" ]]; then 12 | source /etc/automation_environment 13 | fi 14 | 15 | if [[ -n "$AUTOMATION_LIB_PATH" ]]; then 16 | source $AUTOMATION_LIB_PATH/common_lib.sh 17 | else 18 | ( 19 | echo "WARNING: It does not appear that containers/automation was installed." 20 | echo " Functionality of most of this library will be negatively impacted" 21 | echo " This ${BASH_SOURCE[0]} was loaded by ${BASH_SOURCE[1]}" 22 | ) > /dev/stderr 23 | fi 24 | 25 | # Unsafe env. vars for display 26 | SECRET_ENV_RE='(ACCOUNT)|(GC[EP]..+)|(SSH)|(PASSWORD)|(TOKEN)' 27 | 28 | # setup.sh calls make_cienv() to cache these values for the life of the VM 29 | if [[ -r "/etc/ci_environment" ]]; then 30 | source /etc/ci_environment 31 | else # set default values - see make_cienv() below 32 | # VM Images are built with this setup 33 | CARGO_HOME="${CARGO_HOME:-/var/cache/cargo}" 34 | source $CARGO_HOME/env 35 | 36 | # Make caching more effective - disable incremental compilation, 37 | # so that the Rust compiler doesn't waste time creating the 38 | # additional artifacts required for incremental builds. 39 | # Ref: https://github.com/marketplace/actions/rust-cache#cache-details 40 | CARGO_INCREMENTAL=0 41 | fi 42 | 43 | # END Global export of all variables 44 | set -a 45 | 46 | # Shortcut to automation library timeout/retry function 47 | retry() { err_retry 8 1000 "" "$@"; } # just over 4 minutes max 48 | 49 | # Helper to ensure a consistent environment across multiple CI scripts 50 | # containers, and shell environments (e.g. hack/get_ci_vm.sh) 51 | make_cienv(){ 52 | local envname 53 | local envval 54 | local SETUP_ENVIRONMENT=1 55 | for envname in CARGO_HOME PATH CIRRUS_WORKING_DIR SETUP_ENVIRONMENT; do 56 | envval="${!envname}" 57 | # Properly escape values to prevent injection 58 | printf -- "$envname=%q\n" "$envval" 59 | done 60 | } 61 | 62 | complete_setup(){ 63 | set +x 64 | msg "************************************************************" 65 | msg "Completing environment setup, writing vars:" 66 | msg "************************************************************" 67 | make_cienv | tee -a /etc/ci_environment 68 | } 69 | -------------------------------------------------------------------------------- /.packit.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # See the documentation for more information: 3 | # https://packit.dev/docs/configuration/ 4 | 5 | downstream_package_name: netavark 6 | upstream_tag_template: v{version} 7 | 8 | packages: 9 | netavark-fedora: 10 | pkg_tool: fedpkg 11 | specfile_path: rpm/netavark.spec 12 | netavark-centos: 13 | pkg_tool: centpkg 14 | specfile_path: rpm/netavark.spec 15 | 16 | srpm_build_deps: 17 | - cargo 18 | - make 19 | - openssl-devel 20 | 21 | jobs: 22 | - job: copr_build 23 | trigger: pull_request 24 | packages: [netavark-fedora] 25 | notifications: &copr_build_failure_notification 26 | failure_comment: 27 | message: "Ephemeral COPR build failed. @containers/packit-build please check." 28 | targets: 29 | fedora-all-x86_64: {} 30 | fedora-all-aarch64: {} 31 | fedora-eln-x86_64: 32 | additional_repos: 33 | - "https://kojipkgs.fedoraproject.org/repos/eln-build/latest/x86_64/" 34 | fedora-eln-aarch64: 35 | additional_repos: 36 | - "https://kojipkgs.fedoraproject.org/repos/eln-build/latest/aarch64/" 37 | enable_net: true 38 | 39 | - job: copr_build 40 | trigger: pull_request 41 | packages: [netavark-centos] 42 | notifications: *copr_build_failure_notification 43 | targets: 44 | - centos-stream-9-x86_64 45 | - centos-stream-9-aarch64 46 | - centos-stream-10-x86_64 47 | - centos-stream-10-aarch64 48 | enable_net: true 49 | 50 | # Run on commit to main branch 51 | - job: copr_build 52 | trigger: commit 53 | packages: [netavark-fedora] 54 | notifications: 55 | failure_comment: 56 | message: "podman-next COPR build failed. @containers/packit-build please check." 57 | branch: main 58 | owner: rhcontainerbot 59 | project: podman-next 60 | enable_net: true 61 | 62 | # Sync to Fedora 63 | - job: propose_downstream 64 | trigger: release 65 | packages: [netavark-fedora] 66 | update_release: false 67 | dist_git_branches: 68 | - fedora-all 69 | 70 | # Sync to CentOS Stream 71 | - job: propose_downstream 72 | trigger: release 73 | packages: [netavark-centos] 74 | update_release: false 75 | dist_git_branches: 76 | - c10s 77 | 78 | - job: koji_build 79 | trigger: commit 80 | packages: [netavark-fedora] 81 | sidetag_group: netavark-releases 82 | dist_git_branches: 83 | - fedora-all 84 | 85 | - job: bodhi_update 86 | trigger: koji_build 87 | packages: [netavark-fedora] 88 | sidetag_group: netavark-releases 89 | dependencies: 90 | - aardvark-dns 91 | dist_git_branches: 92 | - fedora-all 93 | -------------------------------------------------------------------------------- /hack/get_ci_vm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # For help and usage information, simply execute the script w/o any arguments. 5 | # 6 | # This script is intended to be run by Red Hat podman developers who need 7 | # to debug problems specifically related to Cirrus-CI automated testing. 8 | # It requires that you have been granted prior access to create VMs in 9 | # google-cloud. For non-Red Hat contributors, VMs are available as-needed, 10 | # with supervision upon request. 11 | 12 | set -e 13 | 14 | SCRIPT_FILEPATH=$(realpath "${BASH_SOURCE[0]}") 15 | SCRIPT_DIRPATH=$(dirname "$SCRIPT_FILEPATH") 16 | REPO_DIRPATH=$(realpath "$SCRIPT_DIRPATH/../") 17 | 18 | # Help detect what get_ci_vm container called this script 19 | GET_CI_VM="${GET_CI_VM:-0}" 20 | in_get_ci_vm() { 21 | if ((GET_CI_VM==0)); then 22 | echo "Error: $1 is not intended for use in this context" 23 | exit 2 24 | fi 25 | } 26 | 27 | # get_ci_vm APIv1 container entrypoint calls into this script 28 | # to obtain required repo. specific configuration options. 29 | if [[ "$1" == "--config" ]]; then 30 | in_get_ci_vm "$1" # handles GET_CI_VM==0 case 31 | case "$GET_CI_VM" in 32 | 1) 33 | cat < /dev/stderr 60 | ./contrib/cirrus/setup.sh 61 | else 62 | # Pass this repo and CLI args into container for VM creation/management 63 | mkdir -p $HOME/.config/gcloud/ssh 64 | mkdir -p $HOME/.aws 65 | podman run -it --rm \ 66 | --tz=local \ 67 | -e NAME="$USER" \ 68 | -e SRCDIR=/src \ 69 | -e GCLOUD_ZONE="$GCLOUD_ZONE" \ 70 | -e A_DEBUG="${A_DEBUG:-0}" \ 71 | -v $REPO_DIRPATH:/src:O \ 72 | -v $HOME/.config/gcloud:/root/.config/gcloud:z \ 73 | -v $HOME/.config/gcloud/ssh:/root/.ssh:z \ 74 | -v $HOME/.aws:/root/.aws:z \ 75 | quay.io/libpod/get_ci_vm:latest "$@" 76 | fi 77 | -------------------------------------------------------------------------------- /test-dhcp/README-manual_test.md: -------------------------------------------------------------------------------- 1 | # How to test the proxy and client manually 2 | 3 | The following instructions can help you manually test the proxy server and client. You will need dnsmasq which 4 | is used for DHCP services only. 5 | 6 | ## Setup network and namespace 7 | 8 | The first step is to set up an example virtual network with a bridge. Then one of the virtual ethernet devices 9 | needs to be put into a netns. The following should suffice: 10 | 11 | ``` 12 | $ ip netns add new 13 | $ ip link add dev outside type veth peer name outsidebr 14 | $ ip link add dev inside type veth peer name insidebr 15 | $ ip link add brtest type bridge 16 | $ ip addr add 172.172.1.1/24 dev brtest 17 | $ ip link set outsidebr master brtest 18 | $ ip link set insidebr master brtest 19 | $ ip link set brtest up 20 | $ ip link set inside netns new 21 | $ ip link set outsidebr up 22 | $ ip link set insidebr up 23 | $ ip addr add 172.172.1.2/24 dev outside 24 | $ ip link set outside up 25 | $ ip netns exec new ip link set lo up 26 | $ ip netns exec new ip link set inside up 27 | ``` 28 | 29 | Verify that all the interfaces are status of UP using `ip a` and `ip netns exec new ip a`. 30 | 31 | ## Start DNSMasq 32 | 33 | Open a terminal and from the git repository, edit *test/dnsmasqfiles/sample.conf*. Make sure the interface 34 | matches the interface of the bridge (brtest) in this case. 35 | 36 | ``` 37 | # Set the interface on which dnsmasq operates. 38 | # If not set, all the interfaces is used. 39 | interface=brtest 40 | 41 | # To disable dnsmasq's DNS server functionality. 42 | port=0 43 | ... 44 | ``` 45 | 46 | ``` 47 | $ sudo dnsmasq -d --log-debug --log-queries --conf-dir test/dnsmasqfiles 48 | ``` 49 | 50 | ## Start the nv-proxy server 51 | 52 | Open another terminal and build the server and client 53 | 54 | ``` 55 | $ make all 56 | ``` 57 | 58 | Then run the server with debug enabled: 59 | 60 | ``` 61 | $ sudo RUST_LOG=debug ./bin/netavark dhcp-proxy 62 | ``` 63 | 64 | Note: When doing debug of the client or server, it can be very nice to run the server in your IDE. This allows 65 | you to set breakpoints and see variable values. Here is an example of doing this with CLion. 66 | 67 | ![CLION setup](IDE.png) 68 | 69 | ## Run the client 70 | 71 | In another terminal, you can then run the client. You need to generate a config file first. A script is provided 72 | that will generate a basic config file for you. The script needs the interface name of the bridge, the name of the 73 | netns, and the name of the interface inside the netns respectively. Simply pipe the output to a file. 74 | 75 | ``` 76 | $ sudo sh ./contrib/script/basic.sh brtest new inside 77 | ``` 78 | 79 | Then run the client with debug enabled: 80 | ``` 81 | $ sudo RUST_LOG=debug ./bin/client -f setup|teardown foo 82 | ``` 83 | -------------------------------------------------------------------------------- /src/network/driver.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | dns::aardvark::AardvarkEntry, 3 | error::{NetavarkError, NetavarkResult}, 4 | firewall::FirewallDriver, 5 | }; 6 | 7 | use std::{ffi::OsString, net::IpAddr, os::fd::BorrowedFd, path::Path}; 8 | 9 | use super::{ 10 | bridge::Bridge, 11 | constants, netlink, 12 | plugin::PluginDriver, 13 | types::{Network, PerNetworkOptions, PortMapping, StatusBlock}, 14 | vlan::Vlan, 15 | }; 16 | use std::os::unix::fs::PermissionsExt; 17 | 18 | pub struct DriverInfo<'a> { 19 | pub firewall: &'a dyn FirewallDriver, 20 | pub container_id: &'a String, 21 | pub container_name: &'a String, 22 | pub container_dns_servers: &'a Option>, 23 | pub netns_host: BorrowedFd<'a>, 24 | pub netns_container: BorrowedFd<'a>, 25 | pub netns_path: &'a str, 26 | pub network: &'a Network, 27 | pub per_network_opts: &'a PerNetworkOptions, 28 | pub port_mappings: &'a Option>, 29 | pub dns_port: u16, 30 | pub config_dir: &'a Path, 31 | pub rootless: bool, 32 | pub container_hostname: &'a Option, 33 | } 34 | 35 | pub trait NetworkDriver { 36 | /// validate the driver options 37 | fn validate(&mut self) -> NetavarkResult<()>; 38 | /// setup the network interfaces/firewall rules for this driver 39 | fn setup( 40 | &self, 41 | netlink_sockets: (&mut netlink::Socket, &mut netlink::Socket), 42 | ) -> NetavarkResult<(StatusBlock, Option)>; 43 | /// teardown the network interfaces/firewall rules for this driver 44 | fn teardown( 45 | &self, 46 | netlink_sockets: (&mut netlink::Socket, &mut netlink::Socket), 47 | ) -> NetavarkResult<()>; 48 | 49 | /// return the network name 50 | fn network_name(&self) -> String; 51 | } 52 | 53 | pub fn get_network_driver<'a>( 54 | info: DriverInfo<'a>, 55 | plugins_directories: &Option>, 56 | ) -> NetavarkResult> { 57 | match info.network.driver.as_str() { 58 | constants::DRIVER_BRIDGE => Ok(Box::new(Bridge::new(info))), 59 | constants::DRIVER_IPVLAN | constants::DRIVER_MACVLAN => Ok(Box::new(Vlan::new(info))), 60 | 61 | name => { 62 | if let Some(dirs) = plugins_directories { 63 | for path in dirs.iter() { 64 | let path = Path::new(path).join(name); 65 | if let Ok(meta) = path.metadata() { 66 | if meta.is_file() && meta.permissions().mode() & 0o111 != 0 { 67 | return Ok(Box::new(PluginDriver::new(path, info))); 68 | } 69 | } 70 | } 71 | } 72 | 73 | Err(NetavarkError::Message(format!( 74 | "unknown network driver \"{}\"", 75 | info.network.driver 76 | ))) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/dhcp_proxy_client/client.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use commands::{setup, teardown}; 3 | use std::process; 4 | use tonic::{Code, Status}; 5 | 6 | use netavark::dhcp_proxy::lib::g_rpc::{Lease, NetworkConfig}; 7 | use netavark::dhcp_proxy::proxy_conf::{DEFAULT_NETWORK_CONFIG, DEFAULT_UDS_PATH}; 8 | use netavark::error::NetavarkError; 9 | 10 | pub mod commands; 11 | 12 | #[derive(Parser, Debug)] 13 | #[clap(version = env!("CARGO_PKG_VERSION"))] 14 | struct Opts { 15 | /// Use specific uds path 16 | #[clap(short, long)] 17 | uds: Option, 18 | /// Instead of reading from STDIN, read the configuration to be applied from the given file. 19 | #[clap(short, long)] 20 | file: Option, 21 | /// Netavark trig command 22 | #[clap(subcommand)] 23 | subcmd: SubCommand, 24 | } 25 | 26 | #[derive(Subcommand, Debug)] 27 | enum SubCommand { 28 | /// Configures the given network namespace with the given configuration. 29 | Setup(setup::Setup), 30 | /// Undo any configuration applied via setup command. 31 | Teardown(teardown::Teardown), 32 | // Display info about netavark. 33 | // Version(version::Version), 34 | } 35 | 36 | #[cfg(unix)] 37 | #[tokio::main] 38 | // This client assumes you use the default lease directory 39 | async fn main() -> Result<(), Box> { 40 | // This should be moved to somewhere central. We also need to add override logic. 41 | env_logger::builder().format_timestamp(None).init(); 42 | let opts = Opts::parse(); 43 | let file = opts 44 | .file 45 | .unwrap_or_else(|| DEFAULT_NETWORK_CONFIG.to_string()); 46 | let uds_path = opts.uds.unwrap_or_else(|| DEFAULT_UDS_PATH.to_string()); 47 | let input_config = NetworkConfig::load(&file)?; 48 | let result = match opts.subcmd { 49 | SubCommand::Setup(s) => s.exec(&uds_path, input_config).await, 50 | SubCommand::Teardown(t) => t.exec(&uds_path, input_config).await, 51 | }; 52 | let r = match result { 53 | Ok(r) => r, 54 | Err(e) => { 55 | eprintln!("Error: {e}"); 56 | match e { 57 | NetavarkError::DHCPProxy(status) => process_failure(status), 58 | _ => process::exit(1), 59 | } 60 | } 61 | }; 62 | 63 | let pp = ::serde_json::to_string_pretty(&r); 64 | // TODO this should probably return an empty lease so consumers 65 | // don't soil themselves 66 | println!("{}", pp.unwrap_or_else(|_| "".to_string())); 67 | Ok(()) 68 | } 69 | 70 | // 71 | // process_failure makes the client exit with a specific 72 | // error code 73 | // 74 | fn process_failure(status: Status) -> Lease { 75 | let mut rc: i32 = 1; 76 | 77 | match status.code() { 78 | Code::Unknown => { 79 | rc = 155; 80 | } 81 | Code::InvalidArgument => { 82 | rc = 156; 83 | } 84 | Code::DeadlineExceeded => {} 85 | Code::NotFound => { 86 | rc = 6; 87 | } 88 | _ => {} 89 | } 90 | process::exit(rc) 91 | } 92 | -------------------------------------------------------------------------------- /test/testfiles/firewalld-dbus.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | EXTERNAL 5 | unix:path=/tmp/dummy 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | 3 | use clap::{Parser, Subcommand}; 4 | 5 | use netavark::commands::dhcp_proxy; 6 | use netavark::commands::firewalld_reload; 7 | use netavark::commands::setup; 8 | use netavark::commands::teardown; 9 | use netavark::commands::update; 10 | use netavark::commands::version; 11 | 12 | #[derive(Parser, Debug)] 13 | #[clap(version = env!("CARGO_PKG_VERSION"))] 14 | struct Opts { 15 | /// Instead of reading from STDIN, read the configuration to be applied from the given file. 16 | #[clap(short, long)] 17 | file: Option, 18 | /// Select netavark's firewall driver 19 | // There is no suitable short argument like -F, so there is no short argument. 20 | #[clap(long, env = "NETAVARK_FW")] 21 | firewall_driver: Option, 22 | /// config directory for aardvark, usually path to a tmpfs. 23 | #[clap(short, long)] 24 | config: Option, 25 | /// Tells if current netavark invocation is for rootless container. 26 | #[clap(short, long)] 27 | rootless: Option, 28 | #[clap(short, long)] 29 | /// Path to the aardvark-dns binary. 30 | aardvark_binary: Option, 31 | /// Path to netavark plugin directories, can be set multiple times to specify more than one directory. 32 | #[clap(long, long = "plugin-directory")] 33 | plugin_directories: Option>, 34 | /// Netavark trig command 35 | #[clap(subcommand)] 36 | subcmd: SubCommand, 37 | } 38 | 39 | #[derive(Subcommand, Debug)] 40 | enum SubCommand { 41 | /// Configures the given network namespace with the given configuration. 42 | Setup(setup::Setup), 43 | /// Updates network dns servers for an already configured network. 44 | Update(update::Update), 45 | /// Undo any configuration applied via setup command. 46 | Teardown(teardown::Teardown), 47 | /// Display info about netavark. 48 | Version(version::Version), 49 | /// Start dhcp-proxy 50 | DHCPProxy(dhcp_proxy::Opts), 51 | /// Listen for the firewalld reload event and reload fw rules 52 | #[command(name = "firewalld-reload")] 53 | FirewallDReload, 54 | } 55 | 56 | fn main() { 57 | env_logger::builder().format_timestamp(None).init(); 58 | let opts = Opts::parse(); 59 | 60 | // aardvark config directory must be supplied by parent or it defaults to /tmp/aardvark 61 | let config = opts.config; 62 | let rootless = opts.rootless.unwrap_or(false); 63 | let aardvark_bin = opts 64 | .aardvark_binary 65 | .unwrap_or_else(|| OsString::from("/usr/libexec/podman/aardvark-dns")); 66 | let result = match opts.subcmd { 67 | SubCommand::Setup(setup) => setup.exec( 68 | opts.file, 69 | config, 70 | opts.firewall_driver, 71 | aardvark_bin, 72 | opts.plugin_directories, 73 | rootless, 74 | ), 75 | SubCommand::Teardown(teardown) => teardown.exec( 76 | opts.file, 77 | config, 78 | opts.firewall_driver, 79 | aardvark_bin, 80 | opts.plugin_directories, 81 | rootless, 82 | ), 83 | SubCommand::Update(mut update) => update.exec(config, aardvark_bin, rootless), 84 | SubCommand::Version(version) => version.exec(), 85 | SubCommand::DHCPProxy(proxy) => dhcp_proxy::serve(proxy), 86 | SubCommand::FirewallDReload => firewalld_reload::listen(config), 87 | }; 88 | 89 | match result { 90 | Ok(_) => {} 91 | Err(err) => { 92 | err.print_json(); 93 | std::process::exit(err.get_exit_code()); 94 | } 95 | } 96 | } 97 | 98 | #[cfg(test)] 99 | mod test; 100 | -------------------------------------------------------------------------------- /examples/host-device-plugin.rs: -------------------------------------------------------------------------------- 1 | //! This is just an example plugin, do not use it in production! 2 | 3 | use std::{collections::HashMap, os::fd::AsFd}; 4 | 5 | use netavark::{ 6 | network::{ 7 | core_utils::{open_netlink_sockets, CoreUtils}, 8 | netlink, types, 9 | }, 10 | new_error, 11 | plugin::{Info, Plugin, PluginExec, API_VERSION}, 12 | }; 13 | use netlink_packet_route::{address::AddressAttribute, link::LinkAttribute}; 14 | 15 | fn main() { 16 | let info = Info::new("0.1.0-dev".to_owned(), API_VERSION.to_owned(), None); 17 | 18 | PluginExec::new(Exec {}, info).exec(); 19 | } 20 | 21 | struct Exec {} 22 | 23 | impl Plugin for Exec { 24 | fn create( 25 | &self, 26 | network: types::Network, 27 | ) -> Result> { 28 | if network.network_interface.as_deref().unwrap_or_default() == "" { 29 | return Err(new_error!("no network interface is specified")); 30 | } 31 | 32 | Ok(network) 33 | } 34 | 35 | fn setup( 36 | &self, 37 | netns: String, 38 | opts: types::NetworkPluginExec, 39 | ) -> Result> { 40 | let (mut host, netns) = open_netlink_sockets(&netns)?; 41 | 42 | let name = opts.network.network_interface.unwrap_or_default(); 43 | 44 | let link = host.netlink.get_link(netlink::LinkID::Name(name.clone()))?; 45 | 46 | let mut mac_address = String::from(""); 47 | for nla in link.attributes { 48 | if let LinkAttribute::Address(ref addr) = nla { 49 | mac_address = CoreUtils::encode_address_to_hex(addr); 50 | } 51 | } 52 | 53 | let addresses = host.netlink.dump_addresses()?; 54 | let mut subnets = Vec::new(); 55 | for address in addresses { 56 | if address.header.index == link.header.index { 57 | for nla in address.attributes { 58 | if let AddressAttribute::Address(ip) = &nla { 59 | let net = ipnet::IpNet::new(*ip, address.header.prefix_len)?; 60 | subnets.push(types::NetAddress { 61 | gateway: None, 62 | ipnet: net, 63 | }) 64 | } 65 | } 66 | } 67 | } 68 | 69 | host.netlink 70 | .set_link_ns(link.header.index, netns.file.as_fd())?; 71 | 72 | // interfaces map, but we only ever expect one, for response 73 | let mut interfaces: HashMap = HashMap::new(); 74 | 75 | let interface = types::NetInterface { 76 | mac_address, 77 | subnets: Option::from(subnets), 78 | }; 79 | interfaces.insert(name, interface); 80 | 81 | // StatusBlock response 82 | let response = types::StatusBlock { 83 | dns_server_ips: None, 84 | dns_search_domains: None, 85 | interfaces: Some(interfaces), 86 | }; 87 | 88 | Ok(response) 89 | } 90 | 91 | fn teardown( 92 | &self, 93 | netns: String, 94 | opts: types::NetworkPluginExec, 95 | ) -> Result<(), Box> { 96 | // on teardown revert what was done in setup 97 | let (host, mut netns) = open_netlink_sockets(&netns)?; 98 | 99 | let name = opts.network.network_interface.unwrap_or_default(); 100 | 101 | let link = netns.netlink.get_link(netlink::LinkID::Name(name))?; 102 | 103 | netns 104 | .netlink 105 | .set_link_ns(link.header.index, host.file.as_fd())?; 106 | 107 | Ok(()) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /contrib/cirrus/cache_groom.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script is intended to be run from Cirrus-CI to prepare the 4 | # rust targets cache for re-use during subsequent runs. This mainly 5 | # involves removing files and directories which change frequently 6 | # but are cheap/quick to regenerate - i.e. prevent "cache-flapping". 7 | # Any other use of this script is not supported and may cause harm. 8 | # 9 | # WARNING: This script is re-used from $DEST_BRANCH by other 10 | # repositories. Namely aardvark-dns and possibly others. Check 11 | # before removing / changing / updating. 12 | 13 | set -eo pipefail 14 | 15 | source $(dirname ${BASH_SOURCE[0]})/lib.sh 16 | 17 | if [[ "$CIRRUS_CI" != true ]]; then 18 | die "Script is not intended for use outside of Cirrus-CI" 19 | fi 20 | 21 | req_env_vars CARGO_HOME CARGO_TARGET_DIR CIRRUS_BUILD_ID 22 | 23 | # Giant-meat-cleaver HACK: It's possible (with a long-running cache key) for 24 | # the targets and/or cargo cache to grow without-bound (gigabytes). Ref: 25 | # https://github.com/rust-lang/cargo/issues/5026 26 | # There isn't a good way to deal with this or account for outdated content 27 | # in some intelligent way w/o trolling through config and code files. So, 28 | # Any time the Cirrus-CI build ID is evenly divisible by some number (chosen 29 | # arbitrarily) clobber the whole thing and make the next run entirely 30 | # re-populate cache. This is ugly, but maybe the best option available :( 31 | if [[ "$CIRRUS_BRANCH" == "$DEST_BRANCH" ]] && ((CIRRUS_BUILD_ID%15==0)); then 32 | msg "It's a cache-clobber build, yay! This build has been randomly selected for" 33 | msg "a forced cache-wipe! Congratulations! This means the next build will be" 34 | msg "slow, and nobody will know who to to blame!. Lucky you! Hurray!" 35 | msg "(This is necessary to prevent branch-level cache from infinitely growing)" 36 | cd $CARGO_TARGET_DIR 37 | # Could use `cargo clean` for this, but it's easier to just clobber everything. 38 | rm -rf ./* ./.??* 39 | # In case somebody goes poking around, leave a calling-card hopefully leading 40 | # them back to this script. I don't know of a better way to handle this :S 41 | touch CACHE_WAS_CLOBBERED 42 | 43 | cd $CARGO_HOME 44 | rm -rf ./* ./.??* 45 | touch CACHE_WAS_CLOBBERED 46 | exit 0 47 | fi 48 | 49 | # The following applies to both PRs and branch-level cache. It attempts to remove 50 | # things which are non-essential and/or may change frequently. It stops short of 51 | # trolling through config & code files to determine what is relevant or not. 52 | # Ref: https://doc.rust-lang.org/nightly/cargo/guide/build-cache.html 53 | # https://github.com/Swatinem/rust-cache/tree/master/src 54 | cd $CARGO_TARGET_DIR 55 | for targetname in $(find ./ -type d -maxdepth 1 -mindepth 1); do 56 | msg "Grooming $CARGO_TARGET_DIR/$targetname..." 57 | cd $CARGO_TARGET_DIR/$targetname 58 | # Any top-level hidden files or directories 59 | showrun rm -rf ./.??* 60 | # Example targets 61 | showrun rm -rf ./target/debug/examples 62 | # Documentation 63 | showrun rm -rf ./target/doc 64 | # Internal to rust build process 65 | showrun rm -rf ./target/debug/deps ./target/debug/incremental ./target/debug/build 66 | done 67 | 68 | # The following only applies to dependent packages (crates). It follows recommendations 69 | # Ref: https://doc.rust-lang.org/nightly/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci 70 | # and probably shouldn't be extended beyond what's documented. This cache plays a major 71 | # role in built-time reduction, but must also be prevented from causing "cache-flapping". 72 | cd $CARGO_HOME 73 | for dirname in $(find ./ -type d -maxdepth 2 -mindepth 1); do 74 | case "$dirname" in 75 | ./bin) ;& # same steps as next item 76 | ./registry/index) ;& 77 | ./registry/cache) ;& 78 | ./git/db) continue ;; # Keep 79 | *) rm -rf $dirname ;; # Remove 80 | esac 81 | done 82 | -------------------------------------------------------------------------------- /DISTRO_PACKAGE.md: -------------------------------------------------------------------------------- 1 | # Netavark: A container network stack 2 | 3 | This document is currently written with Fedora as a reference. As Netavark 4 | gets shipped in other distros, this should become a distro-agnostic 5 | document. 6 | 7 | ## Fedora Users 8 | Netavark is available as an officlal Fedora package on Fedora 35 and newer versions 9 | and is only meant to be used with Podman v4 and newer releases. 10 | 11 | ```console 12 | $ sudo dnf install netavark 13 | ``` 14 | 15 | **NOTE:** Fedora 35 users will not be able to install Podman v4 using the default yum 16 | repositories. Please consult the Podman packaging docs for instructions on how 17 | to fetch Podman v4.0 on Fedora 35. 18 | 19 | 20 | After installation, if you would like to migrate all your containers to use 21 | Netavark, you will need to set `network_backend = "netavark"` under 22 | the `[network]` section in your containers.conf (typically located at: 23 | `/usr/share/containers/containers.conf` 24 | 25 | If you would like to test the latest unreleased upstream code, try the 26 | podman-next COPR 27 | 28 | ```console 29 | $ sudo dnf copr enable rhcontainerbot/podman-next 30 | 31 | $ sudo dnf install netavark 32 | ``` 33 | 34 | **CAUTION:** The podman-next COPR provides the latest unreleased sources of Podman, 35 | Netavark and Aardvark-dns as rpms which would override the versions provided by 36 | the official packages. 37 | 38 | ## Distro Packagers 39 | 40 | The vendored sources for netavark will be attached to each netavark release as 41 | a tarball. You can download them with the following: 42 | 43 | `https://github.com/containers/netavark/releases/download/v{version}/netavark-v{version}-vendor.tar.gz` 44 | 45 | And then create a cargo config file to point it to the vendor dir. 46 | ``` 47 | tar xvf %{SOURCE} 48 | mkdir -p .cargo 49 | cat >.cargo/config << EOF 50 | [source.crates-io] 51 | replace-with = "vendored-sources" 52 | 53 | [source.vendored-sources] 54 | directory = "vendor" 55 | EOF 56 | ``` 57 | 58 | The Fedora packaging sources for Netavark are available at the [Netavark 59 | dist-git](https://src.fedoraproject.org/rpms/netavark). 60 | 61 | The Fedora packaged versions of the rust crates that Netavark depends on are 62 | frequently out of date, for example, rtnetlink, sha2, zbus and zvariant at the 63 | time of initial package creation. So, the Fedora package builds Netavark using 64 | the dependencies vendored upstream, found in the `vendor` subdirectory. 65 | 66 | The `netavark` binary is installed to `/usr/libexec/podman/netavark`. 67 | 68 | ## Dependency on aardvark-dns 69 | The netavark package has a `Recommends` on the `aardvark-dns` package. The 70 | aardvark-dns package will be installed by default with netavark, but netavark 71 | will be functional without it. 72 | 73 | ## Relationship with the CNI Plugins package 74 | 75 | While Netavark is a replacement for CNI Plugins (available as 76 | `containernetworking-plugins` on Fedora), the `netavark` package should be 77 | recommended for new installations but will not conflict with 78 | `containernetworking-plugins`. To avoid that conflict, we have made the following 79 | changes to the Fedora packages. 80 | 81 | 1. netavark package includes: 82 | ``` 83 | Provides: container-network-stack = 2 84 | ``` 85 | 86 | 2. containernetworking-plugins package includes: 87 | ``` 88 | Provides: container-network-stack = 1 89 | ``` 90 | 91 | 3. containers-common package includes: 92 | ``` 93 | Requires: container-network-stack 94 | Recommends: netavark 95 | ``` 96 | 97 | ## Listing bundled dependencies 98 | If you need to list the bundled dependencies in your packaging sources, you can 99 | run the `cargo tree` command in the upstream source. 100 | For example, Fedora's packaging source uses: 101 | 102 | ``` 103 | $ cargo tree --prefix none | awk '{print "Provides: bundled(crate("$1")) = "$2}' | sort | uniq 104 | ``` 105 | -------------------------------------------------------------------------------- /test-dhcp/002-setup.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | 8 | @test "basic setup" { 9 | 10 | read -r -d '\0' input_config < NetavarkResult<()> { 15 | let exists = match driver.exists(table, chain, rule) { 16 | Ok(b) => b, 17 | Err(e) => return Err(NetavarkError::Message(e.to_string())), 18 | }; 19 | if exists { 20 | debug_rule_exists(table, chain, rule.to_string()); 21 | return Ok(()); 22 | } 23 | if let Err(e) = driver 24 | .append(table, chain, rule) 25 | .map(|_| debug_rule_create(table, chain, rule.to_string())) 26 | { 27 | return Err(NetavarkError::Message(format!( 28 | "unable to append rule '{rule}' to table '{table}': {e}", 29 | ))); 30 | } 31 | Result::Ok(()) 32 | } 33 | 34 | // add a chain if it does not exist, else do nothing 35 | pub fn add_chain_unique(driver: &IPTables, table: &str, new_chain: &str) -> NetavarkResult<()> { 36 | // Note: while there is an API provided to check if a chain exists in a table 37 | // by iptables, it, for some reason, is slow. Instead we just get a list of 38 | // chains in a table and iterate. Same is being done in golang implementations 39 | let exists = chain_exists(driver, table, new_chain)?; 40 | if exists { 41 | debug_chain_exists(table, new_chain); 42 | return Ok(()); 43 | } 44 | match driver 45 | .new_chain(table, new_chain) 46 | .map(|_| debug_chain_create(table, new_chain)) 47 | { 48 | Ok(_) => Ok(()), 49 | Err(e) => Err(NetavarkError::Message(e.to_string())), 50 | } 51 | } 52 | 53 | // returns a bool as to whether the chain exists 54 | fn chain_exists(driver: &IPTables, table: &str, chain: &str) -> NetavarkResult { 55 | let c = match driver.list_chains(table) { 56 | Ok(b) => b, 57 | Err(e) => return Err(NetavarkError::Message(e.to_string())), 58 | }; 59 | if c.iter().any(|i| i == chain) { 60 | debug_chain_exists(table, chain); 61 | return serde::__private::Result::Ok(true); 62 | } 63 | serde::__private::Result::Ok(false) 64 | } 65 | 66 | pub fn remove_if_rule_exists( 67 | driver: &IPTables, 68 | table: &str, 69 | chain: &str, 70 | rule: &str, 71 | ) -> NetavarkResult<()> { 72 | // If the rule is not present, do not error 73 | let exists = match driver.exists(table, chain, rule) { 74 | Ok(b) => b, 75 | Err(e) => return Err(NetavarkError::Message(e.to_string())), 76 | }; 77 | if !exists { 78 | debug_rule_no_exists(table, chain, rule.to_string()); 79 | return Ok(()); 80 | } 81 | if let Err(e) = driver.delete(table, chain, rule) { 82 | return Err(NetavarkError::Message(format!( 83 | "failed to remove rule '{rule}' from table '{chain}': {e}" 84 | ))); 85 | } 86 | Result::Ok(()) 87 | } 88 | 89 | fn debug_chain_create(table: &str, chain: &str) { 90 | debug!("chain {} created on table {}", chain, table); 91 | } 92 | 93 | fn debug_chain_exists(table: &str, chain: &str) { 94 | debug!("chain {} exists on table {}", chain, table); 95 | } 96 | 97 | pub fn debug_rule_create(table: &str, chain: &str, rule: String) { 98 | debug!( 99 | "rule {} created on table {} and chain {}", 100 | rule, table, chain 101 | ); 102 | } 103 | 104 | fn debug_rule_exists(table: &str, chain: &str, rule: String) { 105 | debug!( 106 | "rule {} exists on table {} and chain {}", 107 | rule, table, chain 108 | ); 109 | } 110 | 111 | fn debug_rule_no_exists(table: &str, chain: &str, rule: String) { 112 | debug!( 113 | "no rule {} exists on table {} and chain {}", 114 | rule, table, chain 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/plugin.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, env, error::Error, io}; 2 | 3 | use serde::Serialize; 4 | 5 | use crate::{error, network::types}; 6 | 7 | pub const API_VERSION: &str = "1.0.0"; 8 | 9 | // create new boxed error with string error message, also accepts format!() style arguments 10 | #[macro_export] 11 | macro_rules! new_error { 12 | ($msg:ident) => { 13 | Box::new(std::io::Error::new(std::io::ErrorKind::Other, $msg)) 14 | }; 15 | ($($arg:tt)*) => {{ 16 | Box::new(std::io::Error::new(std::io::ErrorKind::Other, format!($($arg)*))) 17 | }}; 18 | } 19 | 20 | /// Contains info about this plugin 21 | #[derive(Serialize)] 22 | pub struct Info { 23 | /// The version of this plugin. 24 | version: String, 25 | // The api version for the netavark plugin API. 26 | api_version: String, 27 | /// Optional fields you want to be displayed for the info command 28 | #[serde(flatten)] 29 | extra_info: Option>, 30 | } 31 | 32 | impl Info { 33 | pub fn new( 34 | version: String, 35 | api_version: String, 36 | extra_info: Option>, 37 | ) -> Self { 38 | Self { 39 | version, 40 | api_version, 41 | extra_info, 42 | } 43 | } 44 | } 45 | 46 | /// Define the plugin functions 47 | pub trait Plugin { 48 | // create a network config 49 | fn create(&self, network: types::Network) -> Result>; 50 | /// set up the network configuration 51 | fn setup( 52 | &self, 53 | netns: String, 54 | opts: types::NetworkPluginExec, 55 | ) -> Result>; 56 | /// tear down the network configuration 57 | fn teardown(&self, netns: String, opts: types::NetworkPluginExec) 58 | -> Result<(), Box>; 59 | } 60 | 61 | pub struct PluginExec { 62 | plugin: P, 63 | info: Info, 64 | } 65 | 66 | impl PluginExec

{ 67 | pub fn new(plugin: P, info: Info) -> Self { 68 | PluginExec { plugin, info } 69 | } 70 | 71 | pub fn exec(&self) { 72 | match self.inner_exec() { 73 | Ok(_) => {} 74 | Err(err) => { 75 | let e = error::JsonError { 76 | error: err.to_string(), 77 | }; 78 | serde_json::to_writer(io::stdout(), &e) 79 | .unwrap_or_else(|e| println!("failed to write json error: {e}: {err}")); 80 | std::process::exit(1); 81 | } 82 | }; 83 | } 84 | 85 | fn inner_exec(&self) -> Result<(), Box> { 86 | let mut args = env::args(); 87 | args.next() 88 | .ok_or_else(|| new_error!("zero arguments given"))?; 89 | 90 | // match subcommand 91 | match args.next().as_deref() { 92 | Some("create") => { 93 | let mut network = serde_json::from_reader(io::stdin())?; 94 | 95 | network = self.plugin.create(network)?; 96 | 97 | serde_json::to_writer(io::stdout(), &network)?; 98 | } 99 | Some("setup") => { 100 | let netns = args 101 | .next() 102 | .ok_or_else(|| new_error!("netns path argument is missing"))?; 103 | 104 | let opts = serde_json::from_reader(io::stdin())?; 105 | 106 | let status_block = self.plugin.setup(netns, opts)?; 107 | serde_json::to_writer(io::stdout(), &status_block)?; 108 | } 109 | Some("teardown") => { 110 | let netns = args 111 | .next() 112 | .ok_or_else(|| new_error!("netns path argument is missing"))?; 113 | 114 | let opts = serde_json::from_reader(io::stdin())?; 115 | self.plugin.teardown(netns, opts)?; 116 | } 117 | Some("info") => self.print_info()?, 118 | Some(unknown) => { 119 | return Err(new_error!("unknown subcommand: {}", unknown)); 120 | } 121 | None => self.print_info()?, 122 | }; 123 | Ok(()) 124 | } 125 | 126 | fn print_info(&self) -> Result<(), Box> { 127 | serde_json::to_writer(io::stdout(), &self.info)?; 128 | Ok(()) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /rpm/netavark.spec: -------------------------------------------------------------------------------- 1 | # Building from fedora dependencies not possible 2 | # Latest upstream rtnetlink frequently required 3 | # sha2, zbus, zvariant are currently out of date 4 | 5 | %global with_debug 1 6 | 7 | %if 0%{?with_debug} 8 | %global _find_debuginfo_dwz_opts %{nil} 9 | %global _dwz_low_mem_die_limit 0 10 | %else 11 | %global debug_package %{nil} 12 | %endif 13 | 14 | # Minimum X.Y dep for aardvark-dns 15 | %define major_minor %((v=%{version}; echo ${v%.*})) 16 | 17 | # Set default firewall to nftables on CentOS Stream 10+, RHEL 10+, Fedora 41+ 18 | # and default to iptables on all other environments 19 | # The `rhel` macro is defined on CentOS Stream, RHEL as well as Fedora ELN. 20 | %if (%{defined rhel} && 0%{?rhel} >= 10) || (%{defined fedora} && 0%{?fedora} >= 41) 21 | %define default_fw nftables 22 | %else 23 | %define default_fw iptables 24 | %endif 25 | 26 | Name: netavark 27 | # Set a different Epoch for copr builds 28 | %if %{defined copr_username} 29 | Epoch: 102 30 | %else 31 | Epoch: 2 32 | %endif 33 | Version: 0 34 | Release: %autorelease 35 | # The `AND` needs to be uppercase in the License for SPDX compatibility 36 | License: Apache-2.0 AND BSD-3-Clause AND MIT 37 | %if %{defined golang_arches_future} 38 | ExclusiveArch: %{golang_arches_future} 39 | %else 40 | ExclusiveArch: aarch64 ppc64le s390x x86_64 41 | %endif 42 | Summary: OCI network stack 43 | URL: https://github.com/containers/%{name} 44 | # Tarballs fetched from upstream's release page 45 | Source0: %{url}/archive/v%{version}.tar.gz 46 | Source1: %{url}/releases/download/v%{version}/%{name}-v%{version}-vendor.tar.gz 47 | BuildRequires: cargo 48 | BuildRequires: %{_bindir}/go-md2man 49 | # aardvark-dns and %%{name} are usually released in sync 50 | Requires: aardvark-dns >= %{epoch}:%{major_minor} 51 | Provides: container-network-stack = 2 52 | %if "%{default_fw}" == "nftables" 53 | Requires: nftables 54 | %else 55 | Requires: iptables 56 | %endif 57 | BuildRequires: make 58 | BuildRequires: protobuf-c 59 | BuildRequires: protobuf-compiler 60 | %if %{defined rhel} 61 | # rust-toolset requires the `local` repo enabled on non-koji ELN build environments 62 | BuildRequires: rust-toolset 63 | %else 64 | BuildRequires: rust-packaging 65 | BuildRequires: rust-srpm-macros 66 | %endif 67 | BuildRequires: git-core 68 | BuildRequires: systemd 69 | BuildRequires: systemd-devel 70 | 71 | %description 72 | %{summary} 73 | 74 | Netavark is a rust based network stack for containers. It is being 75 | designed to work with Podman but is also applicable for other OCI 76 | container management applications. 77 | 78 | Netavark is a tool for configuring networking for Linux containers. 79 | Its features include: 80 | * Configuration of container networks via JSON configuration file 81 | * Creation and management of required network interfaces, 82 | including MACVLAN networks 83 | * All required firewall configuration to perform NAT and port 84 | forwarding as required for containers 85 | * Support for iptables and firewalld at present, with support 86 | for nftables planned in a future release 87 | * Support for rootless containers 88 | * Support for IPv4 and IPv6 89 | * Support for container DNS resolution via aardvark-dns. 90 | 91 | %prep 92 | %autosetup -Sgit %{name}-%{version} 93 | # Following steps are only required on environments like koji which have no 94 | # network access and thus depend on the vendored tarball. Copr pulls 95 | # dependencies directly from the network. 96 | %if !%{defined copr_username} 97 | tar fx %{SOURCE1} 98 | %if 0%{?fedora} || 0%{?rhel} >= 10 99 | %cargo_prep -v vendor 100 | %else 101 | %cargo_prep -V 1 102 | %endif 103 | %endif 104 | 105 | %build 106 | NETAVARK_DEFAULT_FW=%{default_fw} %{__make} CARGO="%{__cargo}" build 107 | %if (0%{?fedora} || 0%{?rhel} >= 10) && !%{defined copr_username} 108 | %cargo_license_summary 109 | %{cargo_license} > LICENSE.dependencies 110 | %cargo_vendor_manifest 111 | %endif 112 | 113 | cd docs 114 | %{__make} 115 | 116 | %install 117 | %{__make} DESTDIR=%{buildroot} PREFIX=%{_prefix} install 118 | 119 | %preun 120 | %systemd_preun %{name}-dhcp-proxy.service 121 | %systemd_preun %{name}-firewalld-reload.service 122 | 123 | %postun 124 | %systemd_postun %{name}-dhcp-proxy.service 125 | %systemd_postun %{name}-firewalld-reload.service 126 | 127 | %files 128 | %license LICENSE 129 | %if (0%{?fedora} || 0%{?rhel} >= 10) && !%{defined copr_username} 130 | %license LICENSE.dependencies 131 | %license cargo-vendor.txt 132 | %endif 133 | %dir %{_libexecdir}/podman 134 | %{_libexecdir}/podman/%{name}* 135 | %{_mandir}/man1/%{name}.1* 136 | %{_unitdir}/%{name}-dhcp-proxy.service 137 | %{_unitdir}/%{name}-dhcp-proxy.socket 138 | %{_unitdir}/%{name}-firewalld-reload.service 139 | 140 | %changelog 141 | %autochangelog 142 | -------------------------------------------------------------------------------- /src/network/internal_types.rs: -------------------------------------------------------------------------------- 1 | use super::netlink; 2 | use crate::network::types; 3 | use std::net::IpAddr; 4 | 5 | /// Teardown contains options for tearing down behind a container 6 | #[derive(Debug)] 7 | pub struct TeardownPortForward<'a> { 8 | pub config: PortForwardConfig<'a>, 9 | /// remove network related information 10 | pub complete_teardown: bool, 11 | } 12 | 13 | /// SetupNetwork contains options for setting up a container 14 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 15 | pub struct SetupNetwork { 16 | /// subnets used for this network 17 | pub subnets: Option>, 18 | /// bridge interface name 19 | pub bridge_name: String, 20 | /// id for the network 21 | #[serde(default)] 22 | pub network_id: String, 23 | /// hash id for the network 24 | pub network_hash_name: String, 25 | /// isolation determines whether the network can communicate with others outside of its interface 26 | pub isolation: IsolateOption, 27 | /// port used for the dns server 28 | pub dns_port: u16, 29 | } 30 | 31 | #[derive(Debug)] 32 | pub struct TearDownNetwork { 33 | pub config: SetupNetwork, 34 | pub complete_teardown: bool, 35 | } 36 | 37 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 38 | pub struct PortForwardConfigGeneric { 39 | /// id of container 40 | pub container_id: String, 41 | /// id of the network 42 | #[serde(default)] 43 | pub network_id: String, 44 | /// port mappings 45 | pub port_mappings: Ports, 46 | /// name of network 47 | pub network_name: String, 48 | /// hash id for the network 49 | pub network_hash_name: String, 50 | /// ipv4 address of the container to bind to. 51 | /// If multiple v4 addresses are present, use the first one for this. 52 | /// At least one of container_ip_v6 and container_ip_v6 must be set. Both can 53 | /// be set at the same time as well. 54 | pub container_ip_v4: Option, 55 | /// subnet associated with the IPv4 address. 56 | /// Must be set if v4 address is set. 57 | pub subnet_v4: Option, 58 | /// ipv6 address of the container. 59 | /// If multiple v6 addresses are present, use the first one for this. 60 | /// At least one of container_ip_v6 and container_ip_v6 must be set. Both can 61 | /// be set at the same time as well. 62 | pub container_ip_v6: Option, 63 | /// subnet associated with the ipv6 address. 64 | /// Must be set if the v6 address is set. 65 | pub subnet_v6: Option, 66 | /// port used by DNS that should create forwarding rules 67 | /// forwarding is not setup if this is 53. 68 | pub dns_port: u16, 69 | /// dns servers IPs where forwarding rule to port 53 from dns_port are necessary 70 | pub dns_server_ips: IpAddresses, 71 | } 72 | 73 | // Some trickery to define two struct one with references and one with owned data, 74 | // basically the reference version should be used everywhere and the owned version 75 | // is only needed to deserialize the json data. 76 | pub type PortForwardConfigOwned = 77 | PortForwardConfigGeneric>, Vec>; 78 | pub type PortForwardConfig<'a> = 79 | PortForwardConfigGeneric<&'a Option>, &'a Vec>; 80 | 81 | impl<'a> From<&'a PortForwardConfigOwned> for PortForwardConfig<'a> { 82 | fn from(p: &'a PortForwardConfigOwned) -> PortForwardConfig<'a> { 83 | Self { 84 | container_id: p.container_id.clone(), 85 | network_id: p.network_id.clone(), 86 | port_mappings: &p.port_mappings, 87 | network_name: p.network_name.clone(), 88 | network_hash_name: p.network_hash_name.clone(), 89 | container_ip_v4: p.container_ip_v4, 90 | subnet_v4: p.subnet_v4, 91 | container_ip_v6: p.container_ip_v6, 92 | subnet_v6: p.subnet_v6, 93 | dns_port: p.dns_port, 94 | dns_server_ips: &p.dns_server_ips, 95 | } 96 | } 97 | } 98 | 99 | /// IPAMAddresses is used to pass ipam information around 100 | pub struct IPAMAddresses { 101 | // ip addresses for netlink 102 | pub container_addresses: Vec, 103 | // if using macvlan and dhcp, then true 104 | pub dhcp_enabled: bool, 105 | pub gateway_addresses: Vec, 106 | pub routes: Vec, 107 | pub ipv6_enabled: bool, 108 | // result for podman 109 | pub net_addresses: Vec, 110 | pub nameservers: Vec, 111 | } 112 | 113 | // IsolateOption is used to select isolate option value 114 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 115 | pub enum IsolateOption { 116 | Strict, 117 | Normal, 118 | Never, 119 | } 120 | -------------------------------------------------------------------------------- /src/firewall/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{NetavarkError, NetavarkResult}; 2 | use crate::network::internal_types::{ 3 | PortForwardConfig, SetupNetwork, TearDownNetwork, TeardownPortForward, 4 | }; 5 | use log::{debug, info}; 6 | use zbus::blocking::Connection; 7 | 8 | pub mod firewalld; 9 | pub mod fwnone; 10 | pub mod iptables; 11 | pub mod nft; 12 | pub mod state; 13 | mod varktables; 14 | 15 | const IPTABLES: &str = "iptables"; 16 | const FIREWALLD: &str = "firewalld"; 17 | const NFTABLES: &str = "nftables"; 18 | const NONE: &str = "none"; 19 | 20 | /// Firewall drivers have the ability to set up per-network firewall forwarding 21 | /// and port mappings. 22 | pub trait FirewallDriver { 23 | /// Set up firewall rules for the given network, 24 | fn setup_network(&self, network_setup: SetupNetwork) -> NetavarkResult<()>; 25 | /// Tear down firewall rules for the given network. 26 | fn teardown_network(&self, tear: TearDownNetwork) -> NetavarkResult<()>; 27 | 28 | /// Set up port-forwarding firewall rules for a given container. 29 | fn setup_port_forward(&self, setup_pw: PortForwardConfig) -> NetavarkResult<()>; 30 | /// Tear down port-forwarding firewall rules for a single container. 31 | fn teardown_port_forward(&self, teardown_pf: TeardownPortForward) -> NetavarkResult<()>; 32 | 33 | /// Return the name of the driver. 34 | fn driver_name(&self) -> &str; 35 | } 36 | 37 | /// Types of firewall backend 38 | enum FirewallImpl { 39 | Iptables, 40 | Firewalld(Connection), 41 | Nftables, 42 | Fwnone, 43 | } 44 | 45 | /// What firewall implementations does this system support? 46 | fn get_firewall_impl(driver_name: Option) -> NetavarkResult { 47 | // It respects "firewalld", "iptables", "nftables", "none". 48 | if let Some(driver) = driver_name { 49 | debug!("Forcibly using firewall driver {driver}"); 50 | match driver.to_lowercase().as_str() { 51 | FIREWALLD => { 52 | let conn = match Connection::system() { 53 | Ok(c) => c, 54 | Err(e) => { 55 | return Err(NetavarkError::wrap( 56 | "Error retrieving dbus connection for requested firewall backend", 57 | e.into(), 58 | )) 59 | } 60 | }; 61 | return Ok(FirewallImpl::Firewalld(conn)); 62 | } 63 | IPTABLES => return Ok(FirewallImpl::Iptables), 64 | NFTABLES => return Ok(FirewallImpl::Nftables), 65 | NONE => return Ok(FirewallImpl::Fwnone), 66 | any => { 67 | return Err(NetavarkError::Message(format!( 68 | "Must provide a valid firewall backend, got {any}" 69 | ))) 70 | } 71 | } 72 | } 73 | 74 | get_default_fw_impl() 75 | 76 | // Is firewalld running? 77 | // let conn = match Connection::system() { 78 | // Ok(conn) => conn, 79 | // Err(_) => return FirewallImpl::Iptables, 80 | // }; 81 | // match conn.call_method( 82 | // Some("org.freedesktop.DBus"), 83 | // "/org/freedesktop/DBus", 84 | // Some("org.freedesktop.DBus"), 85 | // "GetNameOwner", 86 | // &"org.fedoraproject.FirewallD1", 87 | // ) { 88 | // Ok(_) => FirewallImpl::Firewalld(conn), 89 | // Err(_) => FirewallImpl::Iptables, 90 | // } 91 | } 92 | 93 | #[cfg(default_fw = "nftables")] 94 | fn get_default_fw_impl() -> NetavarkResult { 95 | Ok(FirewallImpl::Nftables) 96 | } 97 | 98 | #[cfg(default_fw = "iptables")] 99 | fn get_default_fw_impl() -> NetavarkResult { 100 | Ok(FirewallImpl::Iptables) 101 | } 102 | 103 | #[cfg(default_fw = "none")] 104 | fn get_default_fw_impl() -> NetavarkResult { 105 | Ok(FirewallImpl::Fwnone) 106 | } 107 | 108 | /// Get the preferred firewall implementation for the current system 109 | /// configuration. 110 | pub fn get_supported_firewall_driver( 111 | driver_name: Option, 112 | ) -> NetavarkResult> { 113 | match get_firewall_impl(driver_name) { 114 | Ok(fw) => match fw { 115 | FirewallImpl::Iptables => { 116 | info!("Using iptables firewall driver"); 117 | iptables::new() 118 | } 119 | FirewallImpl::Firewalld(conn) => { 120 | info!("Using firewalld firewall driver"); 121 | firewalld::new(conn) 122 | } 123 | FirewallImpl::Nftables => { 124 | info!("Using nftables firewall driver"); 125 | nft::new() 126 | } 127 | FirewallImpl::Fwnone => { 128 | info!("Not using firewall"); 129 | fwnone::new() 130 | } 131 | }, 132 | Err(e) => Err(e), 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v1.13.0 4 | 5 | * Fixed bug where port forwarding rules might not be removed correctly on nftables 6 | * Add DNS DNAT rules first with nftables 7 | 8 | ## v1.12.2 9 | 10 | * Ensure DNS rules cover TCP for iptables and nftables 11 | * On ardvark-dns start, delete entries again on failure 12 | 13 | ## v1.12.1 14 | 15 | * Fixed problem with categories in Cargo.toml that prevented us from publishing v1.12.0 16 | 17 | ## v1.12.0 18 | 19 | * Dependency updates 20 | * Netavark-DHCP proxy: use dns servers from dhcp lease 21 | * Improved handling and visibility of errors from aardvark-dns 22 | * Use nftables as default driver for Fedora 41 23 | 24 | ## v1.11.0 25 | 26 | * Do not perform namespace detection for aardvark-dns updates as it is not needed 27 | * Fixed condition where ignored errors were being returned as real 28 | * With nftables, only dump netavark table rules 29 | * Fix port forward with strict RPF and multi-networks 30 | * updated dependencies 31 | 32 | ## v1.10.1 33 | 34 | * updated nftables to 0.3 35 | 36 | ## v1.10.0 37 | 38 | * added an nftables backend that allows its use on systems without iptables installed 39 | * added command line option to change firewall driver 40 | * show error if process is in wrong netns 41 | * removed unessesary unlock lockfile calls 42 | * updated dependencies 43 | 44 | ## v1.9.0 45 | 46 | * add firewalld-reload subcommand 47 | * bridge: force static mac on bridge interface 48 | * dependency updates 49 | * numerous fixes to test suite 50 | 51 | ## v1.8.0 52 | 53 | * iptables: improve error when ip6?tables commands are missing 54 | * docs: Convert markdown with go-md2man instead of mandown 55 | * iptables: drop invalid packages 56 | * bump rust edition to 2021 57 | * Add ACCEPT rules in firewall for bridge network with internal dns 58 | * Add vrf support for bridges 59 | 60 | ## v1.7.0 61 | 62 | * Fix misleading dns disabled log 63 | * Dependency updates 64 | * --config is now required when dns is used 65 | * netavark dhcp-proxy correctly renews the lease after dhcp time-out 66 | * bridge: isolate=strict option has been added 67 | * macvlan: bclim option has been added 68 | * "no_default_route" option has been added 69 | * static routes can now be configured 70 | 71 | ## v1.6.0 72 | 73 | * Now supports a driver plugin module for user defined network drivers 74 | * Initial MACVLAN DHCP support (additional unit file required for packagers) 75 | * Dependency updates 76 | 77 | ## v1.5.0 78 | 79 | * Removed crossbeam-utils 80 | * Dependency updates 81 | * Preliminary macvlan dhcp support (not fully supported yet) 82 | * Addition of ipvlan support 83 | 84 | ## v1.4.0 85 | 86 | * Added network update command 87 | * Corrected issue #491 to only teardown network forwarding when on complete teardown only 88 | * Fixed some rust documentation 89 | 90 | ## v1.3.0 91 | 92 | * Housekeep and code cleanup 93 | * macvlan: remove tmp interface when name already used in netns 94 | * Add support for route metrics 95 | * netlink: return better error if ipv6 is disabled 96 | * macvlan: fix name collision on hostns 97 | * Ignore dns-enabled for macvlan (BZ2137320) 98 | * better errors on teardown 99 | * allow customer dns servers for containers 100 | * do not set route for internal-only networks 101 | * do not use ipv6 autoconf 102 | 103 | ## v1.2.0 104 | 105 | * Reworked how netavark calls aardvark 106 | * Implemented locking when committing 107 | * Remove bridge only when no containers are attached 108 | * Updated versions of libraries where possible 109 | 110 | ## v1.1.0 111 | 112 | * Netavark is now capable of starting Aardvark on a port other than 53 (controlled by `dns_bind_port` 113 | in `containers.conf`). Firewall rules are added to ensure DNS still functions properly despite the port change. 114 | * Added the ability to isolate networks. Networks with the isolate option set cannot communicate with other networks 115 | with the isolate option set. 116 | * Improved the way Aardvark is launched to avoid potential race conditions where DNS would not be ready when containers 117 | were started. 118 | * Fixed a bug where Aardvark could not be run in environments with a read-only `/proc` (e.g. inside a container). 119 | 120 | ## v1.0.3 121 | 122 | * Updated dependenciess 123 | * Simplified option parsing for bridge/macvlan 124 | * Added support for an ipam `none` driver 125 | 126 | ## v1.0.2 127 | 128 | * Fix issue [#13533](https://github.com/containers/podman/issues/13533) - only use systemd when present 129 | * Dropped vergen dependency 130 | * Updated several dependency libraries 131 | * Allow macvlans to not require a default gateway 132 | 133 | ## v1.0.1 134 | 135 | * core,macvlan: add gateway as default route to macvlan interface 136 | * Add host_ip and container_ip version matching to iptables portforwardinhg 137 | * Remove vendor directory from upstream github repo 138 | 139 | ## v1.0.0 140 | 141 | * First official release of netavark 142 | 143 | ## v1.0.0-RC2 144 | 145 | * RC2 containers several bug fixes and code cleanup 146 | 147 | ## v1.0.0-RC1 148 | 149 | * This is the first release candidate of Netavark. All functionality should be working. 150 | -------------------------------------------------------------------------------- /src/network/plugin.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::Read, 3 | path::PathBuf, 4 | process::{Command, Stdio}, 5 | }; 6 | 7 | use crate::{ 8 | dns::aardvark::AardvarkEntry, 9 | error::{ErrorWrap, JsonError, NetavarkError, NetavarkResult}, 10 | wrap, 11 | }; 12 | 13 | use super::{ 14 | driver::{DriverInfo, NetworkDriver}, 15 | types, 16 | }; 17 | 18 | pub struct PluginDriver<'a> { 19 | path: PathBuf, 20 | info: DriverInfo<'a>, 21 | } 22 | 23 | impl<'a> PluginDriver<'a> { 24 | pub fn new(path: PathBuf, info: DriverInfo<'a>) -> Self { 25 | PluginDriver { path, info } 26 | } 27 | } 28 | 29 | impl NetworkDriver for PluginDriver<'_> { 30 | fn validate(&mut self) -> NetavarkResult<()> { 31 | // Note the the plugin API does not implement validate(). 32 | // This would just add an extra fork()/exec() overhead which seems 33 | // undesirable since most times it will work without errors. 34 | Ok(()) 35 | } 36 | 37 | fn setup( 38 | &self, 39 | _netlink_sockets: (&mut super::netlink::Socket, &mut super::netlink::Socket), 40 | ) -> NetavarkResult<(types::StatusBlock, Option)> { 41 | let result = self.exec_plugin(true, self.info.netns_path).wrap(format!( 42 | "plugin {:?} failed", 43 | &self.path.file_name().unwrap_or_default() 44 | ))?; 45 | // The unwrap should be safe, only if the exec_plugin has a bug this 46 | // could fail, in which case the test should catch it. 47 | Ok((result.unwrap(), None)) 48 | } 49 | 50 | fn teardown( 51 | &self, 52 | _netlink_sockets: (&mut super::netlink::Socket, &mut super::netlink::Socket), 53 | ) -> NetavarkResult<()> { 54 | self.exec_plugin(false, self.info.netns_path).wrap(format!( 55 | "plugin {:?} failed", 56 | &self.path.file_name().unwrap_or_default() 57 | ))?; 58 | Ok(()) 59 | } 60 | 61 | fn network_name(&self) -> String { 62 | self.info.network.name.clone() 63 | } 64 | } 65 | 66 | impl PluginDriver<'_> { 67 | fn exec_plugin(&self, setup: bool, netns: &str) -> NetavarkResult> { 68 | // problem we always need to clone since you can only deserialize owned data, 69 | // it is not a problem here but for the plugin it is required. 70 | // If performance becomes a concern we could use two types for it but the 71 | // maintenance overhead does not seem worth right now. 72 | let input = types::NetworkPluginExec { 73 | container_name: self.info.container_name.clone(), 74 | container_id: self.info.container_id.clone(), 75 | port_mappings: self.info.port_mappings.clone(), 76 | network: self.info.network.clone(), 77 | network_options: self.info.per_network_opts.clone(), 78 | }; 79 | 80 | let mut child = Command::new(&self.path) 81 | .arg(if setup { "setup" } else { "teardown" }) 82 | .arg(netns) 83 | .stdin(Stdio::piped()) 84 | .stdout(Stdio::piped()) 85 | .stderr(Stdio::inherit()) 86 | .spawn()?; 87 | 88 | let stdin = child.stdin.take().unwrap(); 89 | serde_json::to_writer(&stdin, &input)?; 90 | // Close stdin here to avoid that the plugin waits forever for an EOF. 91 | // And then we would wait for the child to exit which would cause a hang. 92 | drop(stdin); 93 | 94 | // Note: We need to buffer the output and then deserialize into the correct type after 95 | // the plugin exits, Since the plugin can return two different json types depending on 96 | // the exit code. 97 | let mut buffer: Vec = Vec::new(); 98 | 99 | let mut stdout = child.stdout.take().unwrap(); 100 | // Do not handle error here, we have to wait for the child first. 101 | let result = stdout.read_to_end(&mut buffer); 102 | 103 | let exit_status = wrap!(child.wait(), "wait for plugin to exit")?; 104 | if let Some(rc) = exit_status.code() { 105 | // make sure the buffer is correct 106 | wrap!(result, "read into buffer")?; 107 | if rc == 0 { 108 | // read status block and setup 109 | if setup { 110 | let status = serde_json::from_slice(&buffer)?; 111 | return Ok(Some(status)); 112 | } else { 113 | return Ok(None); 114 | } 115 | } else { 116 | // exit code not 0 => error 117 | let err: JsonError = serde_json::from_slice(&buffer)?; 118 | return Err(NetavarkError::msg(format!( 119 | "exit code {}, message: {}", 120 | rc, err.error 121 | ))); 122 | } 123 | } 124 | // If we could not get the exit code then the process was killed by a signal. 125 | // I don't think it is necessary to read and return the signal so we just return a generic error. 126 | Err(NetavarkError::msg("plugin killed by signal")) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/630-bridge-vlan.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # bridge driver tests with explicit modes 4 | # 5 | 6 | load helpers 7 | 8 | # accepts the mode (managed/unmanaged) as first arg and 9 | # as second arg the vlan id (int) 10 | function createVlanConfig() { 11 | local mode=$1 12 | local vlan=$2 13 | 14 | read -r -d '\0' config <$@.tmp.$$ \ 94 | && mv -f $@.tmp.$$ $@ 95 | 96 | .PHONY: install 97 | install: $(NV_UNIT_FILES) 98 | install ${SELINUXOPT} -D -m0755 bin/netavark $(DESTDIR)$(LIBEXECPODMAN)/netavark 99 | $(MAKE) -C docs install 100 | install ${SELINUXOPT} -m 755 -d ${DESTDIR}${SYSTEMDDIR} 101 | install ${SELINUXOPT} -m 644 contrib/systemd/system/netavark-dhcp-proxy.socket ${DESTDIR}${SYSTEMDDIR}/netavark-dhcp-proxy.socket 102 | install ${SELINUXOPT} -m 644 contrib/systemd/system/netavark-dhcp-proxy.service ${DESTDIR}${SYSTEMDDIR}/netavark-dhcp-proxy.service 103 | install ${SELINUXOPT} -m 644 contrib/systemd/system/netavark-firewalld-reload.service ${DESTDIR}${SYSTEMDDIR}/netavark-firewalld-reload.service 104 | 105 | .PHONY: uninstall 106 | uninstall: 107 | rm -f $(DESTDIR)$(LIBEXECPODMAN)/netavark 108 | rm -f $(PREFIX)/share/man/man1/netavark*.1 109 | rm -f ${DESTDIR}${SYSTEMDDIR}/netavark-dhcp-proxy.service 110 | rm -f ${DESTDIR}${SYSTEMDDIR}/netavark-dhcp-proxy.socket 111 | 112 | .PHONY: test 113 | test: unit integration 114 | 115 | # Used by CI to compile the unit tests but not run them 116 | .PHONY: build_unit 117 | build_unit: $(CARGO_TARGET_DIR) 118 | $(CARGO) test --no-run 119 | 120 | .PHONY: unit 121 | unit: $(CARGO_TARGET_DIR) 122 | $(CARGO) test 123 | 124 | .PHONY: integration 125 | integration: $(CARGO_TARGET_DIR) examples 126 | # needs to be run as root or with podman unshare --rootless-netns 127 | bats test/ 128 | bats test-dhcp/ 129 | 130 | .PHONY: validate 131 | validate: $(CARGO_TARGET_DIR) 132 | $(CARGO) fmt --all -- --check 133 | $(CARGO) clippy -p netavark -- -D warnings 134 | $(MAKE) docs 135 | 136 | .PHONY: vendor-tarball 137 | vendor-tarball: build install.cargo-vendor-filterer 138 | VERSION=$(shell bin/netavark --version | cut -f2 -d" ") && \ 139 | $(CARGO) vendor-filterer --format=tar.gz --prefix vendor/ && \ 140 | mv vendor.tar.gz netavark-v$$VERSION-vendor.tar.gz && \ 141 | gzip -c bin/netavark > netavark.gz && \ 142 | sha256sum netavark.gz netavark-v$$VERSION-vendor.tar.gz > sha256sum 143 | 144 | .PHONY: install.cargo-vendor-filterer 145 | install.cargo-vendor-filterer: 146 | $(CARGO) install cargo-vendor-filterer 147 | 148 | .PHONY: mock-rpm 149 | mock-rpm: 150 | rpkg local 151 | 152 | .PHONY: help 153 | help: 154 | @echo "usage: make $(prog) [debug=1]" 155 | 156 | .PHONY: build_proxy_client 157 | build_proxy_client: bin $(CARGO_TARGET_DIR) 158 | $(CARGO) build --bin netavark-dhcp-proxy-client $(release) 159 | cp $(CARGO_TARGET_DIR)/$(profile)/netavark-dhcp-proxy-client bin/netavark-dhcp-proxy-client$(if $(debug),.debug,) 160 | -------------------------------------------------------------------------------- /src/commands/teardown.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::get_config_dir; 2 | use crate::dns::aardvark::{Aardvark, AardvarkEntry}; 3 | use crate::error::{NetavarkError, NetavarkErrorList, NetavarkResult}; 4 | use crate::network::constants::DRIVER_BRIDGE; 5 | use crate::network::core_utils; 6 | use crate::network::driver::{get_network_driver, DriverInfo}; 7 | 8 | use crate::{firewall, network}; 9 | use clap::builder::NonEmptyStringValueParser; 10 | use clap::Parser; 11 | use log::debug; 12 | use std::ffi::OsString; 13 | use std::os::fd::AsFd; 14 | use std::path::Path; 15 | 16 | #[derive(Parser, Debug)] 17 | pub struct Teardown { 18 | /// Network namespace path 19 | #[clap(required = true, value_parser = NonEmptyStringValueParser::new())] 20 | network_namespace_path: String, 21 | } 22 | 23 | impl Teardown { 24 | /// 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). 25 | pub fn new(network_namespace_path: String) -> Self { 26 | Self { 27 | network_namespace_path, 28 | } 29 | } 30 | 31 | pub fn exec( 32 | &self, 33 | input_file: Option, 34 | config_dir: Option, 35 | firewall_driver: Option, 36 | aardvark_bin: OsString, 37 | plugin_directories: Option>, 38 | rootless: bool, 39 | ) -> NetavarkResult<()> { 40 | debug!("Tearing down.."); 41 | let network_options = network::types::NetworkOptions::load(input_file)?; 42 | 43 | let mut error_list = NetavarkErrorList::new(); 44 | 45 | let dns_port = core_utils::get_netavark_dns_port()?; 46 | let config_dir = get_config_dir(config_dir, "teardown")?; 47 | 48 | let mut aardvark_entries = Vec::new(); 49 | for (key, network) in &network_options.network_info { 50 | if network.dns_enabled && network.driver == DRIVER_BRIDGE { 51 | aardvark_entries.push(AardvarkEntry { 52 | network_name: key, 53 | network_gateways: Vec::new(), 54 | network_dns_servers: &None, 55 | container_id: &network_options.container_id, 56 | container_ips_v4: Vec::new(), 57 | container_ips_v6: Vec::new(), 58 | container_names: Vec::new(), 59 | container_dns_servers: &None, 60 | is_internal: network.internal, 61 | }); 62 | } 63 | } 64 | 65 | if !aardvark_entries.is_empty() { 66 | // stop dns server first before netavark clears the interface 67 | let path = Path::new(&config_dir).join("aardvark-dns"); 68 | 69 | let aardvark_interface = Aardvark::new(path, rootless, aardvark_bin, dns_port); 70 | if let Err(err) = aardvark_interface.delete_from_netavark_entries(&aardvark_entries) { 71 | error_list.push(NetavarkError::wrap("remove aardvark entries", err)); 72 | } 73 | } 74 | 75 | let firewall_driver = match firewall::get_supported_firewall_driver(firewall_driver) { 76 | Ok(driver) => driver, 77 | Err(e) => return Err(e), 78 | }; 79 | 80 | let (mut hostns, mut netns) = 81 | core_utils::open_netlink_sockets(&self.network_namespace_path)?; 82 | 83 | for (net_name, network) in network_options.network_info.iter() { 84 | let per_network_opts = match network_options.networks.get(net_name) { 85 | Some(opts) => opts, 86 | None => { 87 | error_list.push(NetavarkError::Message(format!( 88 | "network options for network {net_name} not found" 89 | ))); 90 | continue; 91 | } 92 | }; 93 | 94 | let driver = match get_network_driver( 95 | DriverInfo { 96 | firewall: firewall_driver.as_ref(), 97 | container_id: &network_options.container_id, 98 | container_name: &network_options.container_name, 99 | container_hostname: &network_options.container_hostname, 100 | container_dns_servers: &network_options.dns_servers, 101 | netns_host: hostns.file.as_fd(), 102 | netns_container: netns.file.as_fd(), 103 | netns_path: &self.network_namespace_path, 104 | network, 105 | per_network_opts, 106 | port_mappings: &network_options.port_mappings, 107 | dns_port, 108 | config_dir: Path::new(&config_dir), 109 | rootless, 110 | }, 111 | &plugin_directories, 112 | ) { 113 | Ok(driver) => driver, 114 | Err(err) => { 115 | error_list.push(err); 116 | continue; 117 | } 118 | }; 119 | 120 | match driver.teardown((&mut hostns.netlink, &mut netns.netlink)) { 121 | Ok(_) => {} 122 | Err(err) => { 123 | error_list.push(err); 124 | continue; 125 | } 126 | }; 127 | } 128 | 129 | if !error_list.is_empty() { 130 | return Err(NetavarkError::List(error_list)); 131 | } 132 | 133 | debug!("Teardown complete"); 134 | Ok(()) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/network/dhcp.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{ErrorWrap, NetavarkError, NetavarkResult}; 2 | use crate::network::types::NetAddress; 3 | use ipnet::IpNet; 4 | use std::net::IpAddr; 5 | use std::str::FromStr; 6 | 7 | use crate::dhcp_proxy::lib::g_rpc::NetworkConfig; 8 | use crate::dhcp_proxy::proxy_conf::DEFAULT_UDS_PATH; 9 | 10 | use super::driver::DriverInfo; 11 | use super::{core_utils, netlink}; 12 | 13 | pub type DhcpLeaseInfo = (Vec, Option>, Option>); 14 | 15 | /// dhcp performs the connection to the nv-proxy over grpc where it 16 | /// requests it to perform a lease via the host's network interface 17 | /// but passes it the network interface from the container netns.:w 18 | /// 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `host_network_interface`: host interface name in &str 23 | /// * `container_network_interface`: container network interface (eth0) 24 | /// * `ns_path`: path to the container netns 25 | /// * `container_macvlan_mac`: mac address of the container network interface above. 26 | /// 27 | /// returns: Result, NetavarkError> 28 | /// 29 | /// # Examples 30 | /// 31 | /// ``` 32 | /// 33 | /// ``` 34 | pub fn get_dhcp_lease( 35 | host_network_interface: &str, 36 | container_network_interface: &str, 37 | ns_path: &str, 38 | container_macvlan_mac: &str, 39 | container_hostname: &str, 40 | container_id: &str, 41 | ) -> NetavarkResult { 42 | let nvp_config = NetworkConfig { 43 | host_iface: host_network_interface.to_string(), 44 | // TODO add in domain name support 45 | domain_name: "".to_string(), 46 | host_name: container_hostname.to_string(), 47 | version: 0, 48 | ns_path: ns_path.to_string(), 49 | container_iface: container_network_interface.to_string(), 50 | container_mac_addr: container_macvlan_mac.to_string(), 51 | container_id: container_id.to_string(), 52 | }; 53 | let lease = match tokio::task::LocalSet::new().block_on( 54 | match &tokio::runtime::Builder::new_current_thread() 55 | .enable_io() 56 | .build() 57 | { 58 | Ok(r) => r, 59 | Err(e) => { 60 | return Err(NetavarkError::msg(format!("unable to build thread: {e}"))); 61 | } 62 | }, 63 | nvp_config.get_lease(DEFAULT_UDS_PATH), 64 | ) { 65 | Ok(l) => l, 66 | Err(e) => { 67 | return Err(NetavarkError::msg(format!("unable to obtain lease: {e}"))); 68 | } 69 | }; 70 | 71 | // Note: technically DHCP can return multiple gateways but 72 | // we are just plucking the one. gw may also not exist. 73 | let gw = if !lease.gateways.is_empty() { 74 | match IpAddr::from_str(&lease.gateways[0]) { 75 | Ok(g) => Some(g), 76 | Err(e) => { 77 | return Err(NetavarkError::msg(format!("bad gateway address: {e}"))); 78 | } 79 | } 80 | } else { 81 | None 82 | }; 83 | 84 | let dns_servers = if !lease.dns_servers.is_empty() { 85 | let servers = lease 86 | .dns_servers 87 | .into_iter() 88 | .map(|d| match IpAddr::from_str(&d) { 89 | Ok(d) => Ok(d), 90 | Err(e) => Err(NetavarkError::msg(format!("bad dns address: {e}"))), 91 | }) 92 | .collect::, NetavarkError>>()?; 93 | Some(servers) 94 | } else { 95 | None 96 | }; 97 | let domain_name = if !lease.domain_name.is_empty() { 98 | Some(vec![lease.domain_name]) 99 | } else { 100 | None 101 | }; 102 | 103 | let ip_addr = match IpAddr::from_str(&lease.yiaddr) { 104 | Ok(i) => i, 105 | Err(e) => return Err(NetavarkError::Message(e.to_string())), 106 | }; 107 | let subnet_mask = match std::net::Ipv4Addr::from_str(&lease.subnet_mask) { 108 | Ok(s) => s, 109 | Err(e) => return Err(NetavarkError::Message(e.to_string())), 110 | }; 111 | 112 | let prefix_len = u32::from(subnet_mask).count_ones(); 113 | let ip = match IpNet::new(ip_addr, prefix_len as u8) { 114 | Ok(i) => i, 115 | Err(e) => return Err(NetavarkError::msg(e.to_string())), 116 | }; 117 | let ns = NetAddress { 118 | gateway: gw, 119 | ipnet: ip, 120 | }; 121 | 122 | Ok((vec![ns], dns_servers, domain_name)) 123 | } 124 | 125 | pub fn release_dhcp_lease( 126 | host_network_interface: &str, 127 | container_network_interface: &str, 128 | ns_path: &str, 129 | container_macvlan_mac: &str, 130 | ) -> NetavarkResult<()> { 131 | let nvp_config = NetworkConfig { 132 | host_iface: host_network_interface.to_string(), 133 | // TODO add in domain name support 134 | domain_name: "".to_string(), 135 | host_name: "".to_string(), 136 | version: 0, 137 | ns_path: ns_path.to_string(), 138 | container_iface: container_network_interface.to_string(), 139 | container_mac_addr: container_macvlan_mac.to_string(), 140 | container_id: "".to_string(), 141 | }; 142 | match tokio::task::LocalSet::new().block_on( 143 | match &tokio::runtime::Builder::new_current_thread() 144 | .enable_io() 145 | .build() 146 | { 147 | Ok(r) => r, 148 | Err(e) => { 149 | return Err(NetavarkError::msg(format!("unable to build thread: {e}"))); 150 | } 151 | }, 152 | nvp_config.drop_lease(DEFAULT_UDS_PATH), 153 | ) { 154 | Ok(_) => {} 155 | Err(e) => { 156 | return Err(NetavarkError::Message(e.to_string())); 157 | } 158 | }; 159 | Ok(()) 160 | } 161 | 162 | pub fn dhcp_teardown(info: &DriverInfo, sock: &mut netlink::Socket) -> NetavarkResult<()> { 163 | let ipam = core_utils::get_ipam_addresses(info.per_network_opts, info.network)?; 164 | let if_name = info.per_network_opts.interface_name.clone(); 165 | 166 | // If we are using DHCP, we need to at least call to the proxy so that 167 | // the proxy's cache can get updated and the current lease can be released. 168 | if ipam.dhcp_enabled { 169 | let dev = sock.get_link(netlink::LinkID::Name(if_name)).wrap(format!( 170 | "get container interface {}", 171 | &info.per_network_opts.interface_name 172 | ))?; 173 | 174 | let container_mac_address = core_utils::get_mac_address(dev.attributes)?; 175 | release_dhcp_lease( 176 | &info.network.network_interface.clone().unwrap_or_default(), 177 | &info.per_network_opts.interface_name, 178 | info.netns_path, 179 | &container_mac_address, 180 | )? 181 | } 182 | Ok(()) 183 | } 184 | -------------------------------------------------------------------------------- /src/dhcp_proxy/ip.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This file is intended to support netavark-dhcp-proxy configuring the IP information 3 | that it got from the dhcp server. 4 | 5 | Long term this file/function should move into netavark 6 | */ 7 | 8 | pub use crate::dhcp_proxy::lib::g_rpc::{Lease as NetavarkLease, Lease}; 9 | pub use crate::dhcp_proxy::types::{CustomErr, ProxyError}; 10 | use crate::network::core_utils; 11 | use crate::network::netlink; 12 | use crate::network::netlink::Socket; 13 | use ipnet::IpNet; 14 | use log::debug; 15 | use std::net::{IpAddr, Ipv4Addr}; 16 | use std::str::FromStr; 17 | 18 | /* 19 | Information that came back in the DHCP lease like name_servers, 20 | domain and host names, etc. will be implemented in podman; not here. 21 | */ 22 | 23 | #[derive(Clone, Debug)] 24 | struct MacVLAN { 25 | address: IpAddr, 26 | gateways: Vec, 27 | interface: String, 28 | // Unset right now 29 | // mtu: u32, 30 | prefix_length: u8, 31 | } 32 | 33 | trait Address { 34 | fn new(l: &Lease, interface: &str) -> Result 35 | where 36 | Self: Sized; 37 | fn add_ip(&self, nls: &mut Socket) -> Result<(), ProxyError>; 38 | fn add_gws(&self, nls: &mut Socket) -> Result<(), ProxyError>; 39 | } 40 | 41 | fn handle_gws(g: Vec, netmask: &str) -> Result, ProxyError> { 42 | // TODO Need unit test 43 | let mut gws = Vec::new(); 44 | for route in g { 45 | // TODO FIX for ipv6 46 | let sub_mask = match Ipv4Addr::from_str(netmask) { 47 | Ok(n) => n, 48 | Err(e) => return Err(ProxyError::new(e.to_string())), 49 | }; 50 | let prefix = u32::from(sub_mask).count_ones(); 51 | let ip = match Ipv4Addr::from_str(&route) { 52 | Ok(i) => i, 53 | Err(e) => return Err(ProxyError::new(e.to_string())), 54 | }; 55 | let gw = match IpNet::new(IpAddr::from(ip), prefix as u8) { 56 | Ok(r) => r, 57 | Err(e) => return Err(ProxyError::new(format!("{e}:'{route}'"))), 58 | }; 59 | gws.push(gw); 60 | } 61 | Ok(gws) 62 | } 63 | 64 | #[test] 65 | fn test_bad_gw_handle_gws() { 66 | let gws = vec!["192.168.1.1".to_string(), "10.10.10".into()]; 67 | let netmask = "255.255.255.0"; 68 | assert!(handle_gws(gws, netmask).is_err()) 69 | } 70 | 71 | #[test] 72 | fn test_bad_subnet_handle_gws() { 73 | let gws = vec!["192.168.1.1".to_string(), "10.10.10.1".into()]; 74 | let netmask = "255.255.255"; 75 | assert!(handle_gws(gws, netmask).is_err()) 76 | } 77 | 78 | #[test] 79 | fn test_handle_gws() { 80 | let gws = vec!["192.168.1.1".to_string(), "10.10.10.1".into()]; 81 | let netmask = "255.255.255.0"; 82 | assert!(handle_gws(gws, netmask).is_ok()) 83 | } 84 | // IPV4 implementation 85 | impl Address for MacVLAN { 86 | fn new(l: &NetavarkLease, interface: &str) -> Result { 87 | debug!("new ipv4 macvlan for {}", interface); 88 | let address = match IpAddr::from_str(&l.yiaddr) { 89 | Ok(a) => a, 90 | Err(e) => { 91 | return Err(ProxyError::new(format!("bad address: {e}"))); 92 | } 93 | }; 94 | let gateways = match handle_gws(l.gateways.clone(), &l.subnet_mask) { 95 | Ok(g) => g, 96 | Err(e) => { 97 | return Err(ProxyError::new(format!("bad gateways: {}", e))); 98 | } 99 | }; 100 | let prefix_length = match get_prefix_length_v4(&l.subnet_mask) { 101 | Ok(u) => u as u8, 102 | Err(e) => return Err(ProxyError::new(e.to_string())), 103 | }; 104 | Ok(MacVLAN { 105 | address, 106 | gateways, 107 | interface: interface.to_string(), 108 | // Disabled for now 109 | // mtu: l.mtu, 110 | prefix_length, 111 | }) 112 | } 113 | 114 | // add the ip address to the container namespace 115 | fn add_ip(&self, nls: &mut Socket) -> Result<(), ProxyError> { 116 | debug!("adding network information for {}", self.interface); 117 | let ip = IpNet::new(self.address, self.prefix_length)?; 118 | let dev = nls.get_link(netlink::LinkID::Name(self.interface.clone()))?; 119 | match nls.add_addr(dev.header.index, &ip) { 120 | Ok(_) => Ok(()), 121 | Err(e) => Err(ProxyError::new(e.to_string())), 122 | } 123 | } 124 | 125 | // add one or more routes to the container namespace 126 | fn add_gws(&self, nls: &mut Socket) -> Result<(), ProxyError> { 127 | debug!("adding gateways to {}", self.interface); 128 | match core_utils::add_default_routes(nls, &self.gateways, None) { 129 | Ok(_) => Ok(()), 130 | Err(e) => Err(ProxyError::new(e.to_string())), 131 | } 132 | } 133 | } 134 | 135 | // setup takes the DHCP lease and some additional information and 136 | // applies the TCP/IP information to the namespace. 137 | pub fn setup(lease: &NetavarkLease, interface: &str, ns_path: &str) -> Result<(), ProxyError> { 138 | debug!("setting up {}", interface); 139 | let vlan = match MacVLAN::new(lease, interface) { 140 | Ok(f) => f, 141 | Err(e) => return Err(e), 142 | }; 143 | let (_, mut netns) = core_utils::open_netlink_sockets(ns_path)?; 144 | vlan.add_ip(&mut netns.netlink)?; 145 | vlan.add_gws(&mut netns.netlink) 146 | } 147 | 148 | // teardown is likely unnecessary but holding place here 149 | pub fn teardown() -> Result<(), ProxyError> { 150 | todo!() 151 | } 152 | 153 | /// get_prefix_lengh takes a subnet mask in str form and 154 | /// returns its prefix length by counting ones. 155 | /// 156 | /// # Arguments 157 | /// 158 | /// * `netmask`: str form of subnet mask (i.e. 255.255.255.0) 159 | /// 160 | /// returns: Result 161 | /// 162 | /// # Examples 163 | /// 164 | /// ``` 165 | /// 166 | /// ``` 167 | fn get_prefix_length_v4(netmask: &str) -> Result { 168 | let sub_mask = match Ipv4Addr::from_str(netmask) { 169 | Ok(n) => n, 170 | Err(e) => return Err(ProxyError::new(e.to_string())), 171 | }; 172 | Ok(u32::from(sub_mask).count_ones()) 173 | } 174 | 175 | #[cfg(test)] 176 | mod tests { 177 | use super::*; 178 | 179 | #[test] 180 | fn test_24() { 181 | assert_eq!(get_prefix_length_v4("255.255.255.0").unwrap(), 24_u32) 182 | } 183 | 184 | #[test] 185 | fn test_16() { 186 | assert_eq!(get_prefix_length_v4("255.255.0.0").unwrap(), 16_u32) 187 | } 188 | 189 | #[test] 190 | fn test_25() { 191 | assert_eq!(get_prefix_length_v4("255.255.255.128").unwrap(), 25_u32) 192 | } 193 | 194 | #[test] 195 | fn test_bad_input() { 196 | assert!(get_prefix_length_v4("255.255.128").is_err()) 197 | } 198 | } 199 | --------------------------------------------------------------------------------