pax_global_header00006660000000000000000000000064147524042040014514gustar00rootroot0000000000000052 comment=2af90449f3758592b7c390fde85c83c32bc84bd1 containers-netavark-68510f4/000077500000000000000000000000001475240420400157515ustar00rootroot00000000000000containers-netavark-68510f4/.cirrus.yml000066400000000000000000000242051475240420400200640ustar00rootroot00000000000000--- # Format Ref: https://cirrus-ci.org/guide/writing-tasks/ # Main collection of env. vars to set for all tasks and scripts. env: # Actual|intended branch for this run DEST_BRANCH: "main" # The default is 'sh' if unspecified CIRRUS_SHELL: "/bin/bash" # Location where source repo. will be cloned CIRRUS_WORKING_DIR: "/var/tmp/netavark" # Rust package cache also lives here CARGO_HOME: "/var/cache/cargo" # Rust compiler output lives here (see Makefile) CARGO_TARGET_DIR: "$CIRRUS_WORKING_DIR/targets" # Save a little typing (path relative to $CIRRUS_WORKING_DIR) SCRIPT_BASE: "./contrib/cirrus" IMAGE_SUFFIX: "c20250131t121915z-f41f40d13" FEDORA_NETAVARK_IMAGE: "fedora-netavark-${IMAGE_SUFFIX}" AARDVARK_DNS_BRANCH: "main" AARDVARK_DNS_URL: "https://api.cirrus-ci.com/v1/artifact/github/containers/aardvark-dns/success/binary.zip?branch=${AARDVARK_DNS_BRANCH}" FEDORA_NETAVARK_AARCH64_AMI: "fedora-netavark-aws-arm64-${IMAGE_SUFFIX}" EC2_INST_TYPE: "t4g.xlarge" gcp_credentials: ENCRYPTED[d6efdb7d6d4c61e3831df2193ca6348bb02f26cd931695f69d41930b1965f7dab72a838ca0902f6ed8cde66c7deddae2] aws_credentials: ENCRYPTED[36b3e82f72ec2c909235b69d88b835a09e230aa289e2925d949b0dc4c813c1b468655aabb05edf3f7dcfed430c320b87] build_task: alias: "build" # Compiling is very CPU intensive, make it chooch quicker for this task only gce_instance: &standard_build_gce_x86_64 image_project: "libpod-218412" zone: "us-central1-c" disk: 200 # GB, do not set <200 per gcloud warning re: I/O performance cpu: 8 memory: "8Gb" image_name: "${FEDORA_NETAVARK_IMAGE}" cargo_cache: &cargo_cache # Populating this cache depends on execution of setup.sh, and runner.sh # to builds of all release, debug, plus unit-tests. folder: "$CARGO_HOME" # Cirrus-CI will automatically store separate caches for branches vs PRs. # We use the branch-name here mainly to distinguish PR-level caches in # order to properly support backport-PRs to release branches. Otherwise # all PRs & branches will share caches with other PRs and branches # for a given $DEST_BRANCH and vX value. Adjust vX if cache schema # changes. fingerprint_script: echo -e "cargo_v3_${DEST_BRANCH}_amd64\n---\n$(- netavark netavark.debug netavark.info netavark.aarch64-unknown-linux-gnu netavark.debug.aarch64-unknown-linux-gnu netavark.info.aarch64-unknown-linux-gnu clone_script: *noop bin_cache: *ro_bin_cache # The paths used for uploaded artifacts are relative here and in Cirrus script: - set -x - curl --fail --location -O --url ${API_URL_BASE}/build_aarch64/armbinary.zip - unzip armbinary.zip - rm -f armbinary.zip - mv bin/* ./ - rm -rf bin artifacts_test_script: # Other CI systems depend on all files being present - ls -la # If there's a missing file, show what it was in the output - for fn in $EXP_BINS; do [[ -r "$(echo $fn|tee /dev/stderr)" ]] || exit 1; done # Upload tested binary for consumption downstream # https://cirrus-ci.org/guide/writing-tasks/#artifacts-instruction binary_artifacts: path: ./*netavark* containers-netavark-68510f4/.github/000077500000000000000000000000001475240420400173115ustar00rootroot00000000000000containers-netavark-68510f4/.github/renovate.json5000066400000000000000000000042471475240420400221230ustar00rootroot00000000000000/* Renovate is a service similar to GitHub Dependabot, but with (fantastically) more configuration options. So many options in fact, if you're new I recommend glossing over this cheat-sheet prior to the official documentation: https://www.augmentedmind.de/2021/07/25/renovate-bot-cheat-sheet Configuration Update/Change Procedure: 1. Make changes 2. Manually validate changes (from repo-root): podman run -it \ -v ./.github/renovate.json5:/usr/src/app/renovate.json5:z \ ghcr.io/renovatebot/renovate:latest \ renovate-config-validator 3. Commit. Configuration Reference: https://docs.renovatebot.com/configuration-options/ Monitoring Dashboard: https://app.renovatebot.com/dashboard#github/containers Note: The Renovate bot will create/manage it's business on branches named 'renovate/*'. Otherwise, and by default, the only the copy of this file that matters is the one on the `main` branch. No other branches will be monitored or touched in any way. */ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", /************************************************* ****** Global/general configuration options ***** *************************************************/ // Re-use predefined sets of configuration options to DRY "extends": [ // https://github.com/containers/automation/blob/main/renovate/defaults.json5 "github>containers/automation//renovate/defaults.json5" ], /************************************************* *** Repository-specific configuration options *** *************************************************/ // Don't leave dep. update. PRs "hanging", assign them to people. "assignees": ["containers/netavark-maintainers"], /************************************************** ***** Manager-specific configuration options ***** **************************************************/ "dockerfile": { // Renovate has a hard-time managing base images for // Fedora and CentOS (see contrib/container_images/). Disable // all Dockerfile baes-image management. "enabled": false }, } containers-netavark-68510f4/.github/workflows/000077500000000000000000000000001475240420400213465ustar00rootroot00000000000000containers-netavark-68510f4/.github/workflows/check_cirrus_cron.yml000066400000000000000000000012431475240420400255560ustar00rootroot00000000000000--- # See also: # https://github.com/containers/podman/blob/main/.github/workflows/check_cirrus_cron.yml on: # Note: This only applies to the default branch. schedule: # N/B: This should correspond to a period slightly after # the last job finishes running. See job defs. at: # https://cirrus-ci.com/settings/repository/5138144844840960 - cron: '03 03 * * 1-5' # Debug: Allow triggering job manually in github-actions WebUI workflow_dispatch: {} jobs: # Ref: https://docs.github.com/en/actions/using-workflows/reusing-workflows call_cron_failures: uses: containers/podman/.github/workflows/check_cirrus_cron.yml@main secrets: inherit containers-netavark-68510f4/.github/workflows/rerun_cirrus_cron.yml000066400000000000000000000012361475240420400256360ustar00rootroot00000000000000--- # See also: https://github.com/containers/podman/blob/main/.github/workflows/rerun_cirrus_cron.yml on: # Note: This only applies to the default branch. schedule: # N/B: This should correspond to a period slightly after # the last job finishes running. See job defs. at: # https://cirrus-ci.com/settings/repository/5138144844840960 - cron: '01 01 * * 1-5' # Debug: Allow triggering job manually in github-actions WebUI workflow_dispatch: {} jobs: # Ref: https://docs.github.com/en/actions/using-workflows/reusing-workflows call_cron_rerun: uses: containers/podman/.github/workflows/rerun_cirrus_cron.yml@main secrets: inherit containers-netavark-68510f4/.gitignore000066400000000000000000000001621475240420400177400ustar00rootroot00000000000000/bin/ target/ targets/ *.swp netavark.1 netavark-firewalld.7 vendor/ .idea/* contrib/systemd/*/*.service .vscode* containers-netavark-68510f4/.packit.yaml000066400000000000000000000043621475240420400201730ustar00rootroot00000000000000--- # See the documentation for more information: # https://packit.dev/docs/configuration/ downstream_package_name: netavark upstream_tag_template: v{version} packages: netavark-fedora: pkg_tool: fedpkg specfile_path: rpm/netavark.spec netavark-centos: pkg_tool: centpkg specfile_path: rpm/netavark.spec srpm_build_deps: - cargo - make - openssl-devel jobs: - job: copr_build trigger: pull_request packages: [netavark-fedora] notifications: &copr_build_failure_notification failure_comment: message: "Ephemeral COPR build failed. @containers/packit-build please check." targets: fedora-all-x86_64: {} fedora-all-aarch64: {} fedora-eln-x86_64: additional_repos: - "https://kojipkgs.fedoraproject.org/repos/eln-build/latest/x86_64/" fedora-eln-aarch64: additional_repos: - "https://kojipkgs.fedoraproject.org/repos/eln-build/latest/aarch64/" enable_net: true - job: copr_build trigger: pull_request packages: [netavark-centos] notifications: *copr_build_failure_notification targets: - centos-stream-9-x86_64 - centos-stream-9-aarch64 - centos-stream-10-x86_64 - centos-stream-10-aarch64 enable_net: true # Run on commit to main branch - job: copr_build trigger: commit packages: [netavark-fedora] notifications: failure_comment: message: "podman-next COPR build failed. @containers/packit-build please check." branch: main owner: rhcontainerbot project: podman-next enable_net: true # Sync to Fedora - job: propose_downstream trigger: release packages: [netavark-fedora] update_release: false dist_git_branches: - fedora-all # Sync to CentOS Stream - job: propose_downstream trigger: release packages: [netavark-centos] update_release: false dist_git_branches: - c10s - job: koji_build trigger: commit packages: [netavark-fedora] sidetag_group: netavark-releases dist_git_branches: - fedora-all - job: bodhi_update trigger: koji_build packages: [netavark-fedora] sidetag_group: netavark-releases dependencies: - aardvark-dns dist_git_branches: - fedora-all containers-netavark-68510f4/CODE-OF-CONDUCT.md000066400000000000000000000002771475240420400204120ustar00rootroot00000000000000## The Netavark Project Community Code of Conduct The Netavark project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/main/CODE-OF-CONDUCT.md). containers-netavark-68510f4/Cargo.lock000066400000000000000000002235551475240420400176720ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", "windows-sys 0.59.0", ] [[package]] name = "anyhow" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "slab", ] [[package]] name = "async-fs" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock", "blocking", "futures-lite", ] [[package]] name = "async-io" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "tracing", "windows-sys 0.59.0", ] [[package]] name = "async-lock" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ "event-listener", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-process" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", "event-listener", "futures-lite", "rustix", "tracing", ] [[package]] name = "async-recursion" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "async-signal" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ "async-io", "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", "rustix", "signal-hook-registry", "slab", "windows-sys 0.59.0", ] [[package]] name = "async-stream" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", "pin-project-lite", ] [[package]] name = "async-stream-impl" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "async-task" version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", "bytes", "futures-util", "http", "http-body", "http-body-util", "itoa", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", "rustversion", "serde", "sync_wrapper", "tower 0.5.2", "tower-layer", "tower-service", ] [[package]] name = "axum-core" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", "futures-util", "http", "http-body", "http-body-util", "mime", "pin-project-lite", "rustversion", "sync_wrapper", "tower-layer", "tower-service", ] [[package]] name = "backtrace" version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "blocking" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ "async-channel", "async-task", "futures-io", "futures-lite", "piper", ] [[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "cc" version = "1.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", "windows-targets", ] [[package]] name = "clap" version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "data-encoding" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "dhcproto" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcee045385d5f7819022821f41209b9945d17550760b0b2349aaef4ecfa14bc3" dependencies = [ "dhcproto-macros", "hex", "ipnet", "rand 0.8.5", "thiserror 1.0.69", "trust-dns-proto", "url", ] [[package]] name = "dhcproto-macros" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7993efb860416547839c115490d4951c6d0f8ec04a3594d9dd99d50ed7ec170" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "endi" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enum-as-inner" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "enum-as-inner" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "enumflags2" version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", "env_filter", "humantime", "log", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "etherparse" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "827292ea592108849932ad8e30218f8b1f21c0dfd0696698a18b5d0aed62d990" dependencies = [ "arrayvec", ] [[package]] name = "ethtool" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d8e04a35517dc77748dc04bf38152799382d3d8f85cb07cb579bb7f4d8d3b5a" dependencies = [ "anyhow", "byteorder", "futures", "genetlink", "log", "netlink-packet-core", "netlink-packet-generic", "netlink-packet-utils", "netlink-proto", "netlink-sys", "thiserror 1.0.69", "tokio", ] [[package]] name = "event-listener" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fs2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" dependencies = [ "libc", "winapi", ] [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "genetlink" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f890076c1faa1298bf747ce3694a8d9e0d2cc4b06fe293f12dd95742bfd079f" dependencies = [ "futures", "log", "netlink-packet-core", "netlink-packet-generic", "netlink-packet-utils", "netlink-proto", "thiserror 1.0.69", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", "libc", "wasi 0.13.3+wasi-0.2.2", "windows-targets", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", "indexmap 2.7.1", "slab", "tokio", "tokio-util", "tracing", ] [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", "itoa", ] [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", ] [[package]] name = "http-body-util" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", "http", "http-body", "pin-project-lite", ] [[package]] name = "httparse" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", "futures-util", "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "smallvec", "tokio", "want", ] [[package]] name = "hyper-timeout" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ "hyper", "hyper-util", "pin-project-lite", "tokio", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", "socket2", "tokio", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icu_collections" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locid" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_locid_transform" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_locid_transform_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" [[package]] name = "icu_normalizer" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "utf16_iter", "utf8_iter", "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" [[package]] name = "icu_properties" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", "icu_locid_transform", "icu_properties_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" [[package]] name = "icu_provider" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_provider_macros" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "idna" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", ] [[package]] name = "indexmap" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", ] [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" dependencies = [ "serde", ] [[package]] name = "iptables" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b627935a2f5d654613bea2bcd677cc760b03ecf224ced0f1970c0d174813b9" dependencies = [ "lazy_static", "nix 0.29.0", "regex", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "log" version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "matches" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matchit" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "mozim" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "610e34113d007c3588631f879854c14fbf291f3ab7853b833220f7cbf6ece8ad" dependencies = [ "byteorder", "dhcproto", "etherparse", "futures", "libc", "log", "nispor", "nix 0.27.1", "rand 0.8.5", ] [[package]] name = "mptcp-pm" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eafa8fc63dce407b75e336f9a22f18cf5510a3a5c3a5d83262688eb5cca42d5" dependencies = [ "anyhow", "byteorder", "futures", "genetlink", "log", "netlink-packet-core", "netlink-packet-generic", "netlink-packet-utils", "netlink-proto", "netlink-sys", "thiserror 1.0.69", "tokio", ] [[package]] name = "multimap" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "netavark" version = "1.14.0" dependencies = [ "anyhow", "chrono", "clap", "env_logger", "fs2", "futures-channel", "futures-core", "futures-util", "hyper-util", "ipnet", "iptables", "libc", "log", "mozim", "netlink-packet-core", "netlink-packet-route 0.21.0", "netlink-packet-utils", "netlink-sys", "nftables", "nispor", "nix 0.29.0", "once_cell", "prost", "rand 0.9.0", "serde", "serde-value", "serde_json", "sha2", "sysctl", "tempfile", "tokio", "tokio-stream", "tonic", "tonic-build", "tower 0.5.2", "url", "zbus", ] [[package]] name = "netlink-packet-core" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" dependencies = [ "anyhow", "byteorder", "netlink-packet-utils", ] [[package]] name = "netlink-packet-generic" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd7eb8ad331c84c6b8cb7f685b448133e5ad82e1ffd5acafac374af4a5a308b" dependencies = [ "anyhow", "byteorder", "netlink-packet-core", "netlink-packet-utils", ] [[package]] name = "netlink-packet-route" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74c171cd77b4ee8c7708da746ce392440cb7bcf618d122ec9ecc607b12938bf4" dependencies = [ "anyhow", "byteorder", "libc", "log", "netlink-packet-core", "netlink-packet-utils", ] [[package]] name = "netlink-packet-route" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e" dependencies = [ "anyhow", "bitflags", "byteorder", "libc", "log", "netlink-packet-core", "netlink-packet-utils", ] [[package]] name = "netlink-packet-utils" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" dependencies = [ "anyhow", "byteorder", "paste", "thiserror 1.0.69", ] [[package]] name = "netlink-proto" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" dependencies = [ "bytes", "futures", "log", "netlink-packet-core", "netlink-sys", "thiserror 2.0.11", ] [[package]] name = "netlink-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" dependencies = [ "bytes", "futures", "libc", "log", "tokio", ] [[package]] name = "nftables" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c7288de9ee11086e792d89ee45adf490a5a778ff0fddc7e086ce1521989e657" dependencies = [ "serde", "serde_json", "serde_path_to_error", "strum", "strum_macros", "thiserror 1.0.69", ] [[package]] name = "nispor" version = "1.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310e947a3d74c87d73a98c080a07657fd63e96a76230cd88a778297533622b69" dependencies = [ "ethtool", "futures", "libc", "log", "mptcp-pm", "netlink-packet-route 0.19.0", "netlink-packet-utils", "netlink-sys", "rtnetlink", "serde", "serde_json", "tokio", "wl-nl80211", ] [[package]] name = "nix" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ "bitflags", "cfg-if", "libc", ] [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", "memoffset", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "ordered-float" version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" dependencies = [ "num-traits", ] [[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap 2.7.1", ] [[package]] name = "pin-project" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "polling" version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix", "tracing", "windows-sys 0.59.0", ] [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy 0.7.35", ] [[package]] name = "prettyplease" version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", "syn 2.0.98", ] [[package]] name = "proc-macro-crate" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "prost" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" dependencies = [ "bytes", "prost-derive", ] [[package]] name = "prost-build" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" dependencies = [ "heck 0.5.0", "itertools", "log", "multimap", "once_cell", "petgraph", "prettyplease", "prost", "prost-types", "regex", "syn 2.0.98", "tempfile", ] [[package]] name = "prost-derive" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", "itertools", "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "prost-types" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" dependencies = [ "prost", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", ] [[package]] name = "rand" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.0", "zerocopy 0.8.17", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core 0.6.4", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core 0.9.0", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.15", ] [[package]] name = "rand_core" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" dependencies = [ "getrandom 0.3.1", "zerocopy 0.8.17", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rtnetlink" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b684475344d8df1859ddb2d395dd3dac4f8f3422a1aa0725993cb375fc5caba5" dependencies = [ "futures", "log", "netlink-packet-core", "netlink-packet-route 0.19.0", "netlink-packet-utils", "netlink-proto", "netlink-sys", "nix 0.27.1", "thiserror 1.0.69", "tokio", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.59.0", ] [[package]] name = "rustversion" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "serde" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde-value" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ "ordered-float", "serde", ] [[package]] name = "serde_derive" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "serde_json" version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_path_to_error" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ "itoa", "serde", ] [[package]] name = "serde_repr" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" [[package]] name = "strum_macros" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "rustversion", "syn 2.0.98", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "synstructure" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "sysctl" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ "bitflags", "byteorder", "enum-as-inner 0.6.1", "libc", "thiserror 1.0.69", "walkdir", ] [[package]] name = "tempfile" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "thiserror-impl" version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinyvec" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "tokio-stream" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-util" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap 2.7.1", "toml_datetime", "winnow", ] [[package]] name = "tonic" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-stream", "async-trait", "axum", "base64", "bytes", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-timeout", "hyper-util", "percent-encoding", "pin-project", "prost", "socket2", "tokio", "tokio-stream", "tower 0.4.13", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tonic-build" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" dependencies = [ "prettyplease", "proc-macro2", "prost-build", "prost-types", "quote", "syn 2.0.98", ] [[package]] name = "tower" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", "indexmap 1.9.3", "pin-project", "pin-project-lite", "rand 0.8.5", "slab", "tokio", "tokio-util", "tower-layer", "tower-service", "tracing", ] [[package]] name = "tower" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tower-layer", "tower-service", ] [[package]] name = "tower-layer" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] [[package]] name = "trust-dns-proto" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" dependencies = [ "async-trait", "cfg-if", "data-encoding", "enum-as-inner 0.5.1", "futures-channel", "futures-io", "futures-util", "idna 0.2.3", "ipnet", "lazy_static", "rand 0.8.5", "smallvec", "thiserror 1.0.69", "tinyvec", "tracing", "url", ] [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uds_windows" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ "memoffset", "tempfile", "winapi", ] [[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-normalization" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna 1.0.3", "percent-encoding", "serde", ] [[package]] name = "utf16_iter" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ "try-lock", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn 2.0.98", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ "bitflags", ] [[package]] name = "wl-nl80211" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cffcf1e1dca38467779e22768bfc7f294f1b7b3bd99727edf13280eb2429789" dependencies = [ "anyhow", "bitflags", "byteorder", "futures", "genetlink", "log", "netlink-packet-core", "netlink-packet-generic", "netlink-packet-utils", "netlink-proto", "netlink-sys", "thiserror 1.0.69", "tokio", ] [[package]] name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "xdg-home" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "yoke" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", "synstructure", ] [[package]] name = "zbus" version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" dependencies = [ "async-broadcast", "async-executor", "async-fs", "async-io", "async-lock", "async-process", "async-recursion", "async-task", "async-trait", "blocking", "enumflags2", "event-listener", "futures-core", "futures-sink", "futures-util", "hex", "nix 0.29.0", "ordered-stream", "rand 0.8.5", "serde", "serde_repr", "sha1", "static_assertions", "tracing", "uds_windows", "windows-sys 0.52.0", "xdg-home", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.98", "zvariant_utils", ] [[package]] name = "zbus_names" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", "zvariant", ] [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive 0.7.35", ] [[package]] name = "zerocopy" version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" dependencies = [ "zerocopy-derive 0.8.17", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "zerocopy-derive" version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "zerofrom" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", "synstructure", ] [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "zvariant" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.98", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] containers-netavark-68510f4/Cargo.toml000066400000000000000000000037461475240420400177130ustar00rootroot00000000000000[package] name = "netavark" version = "1.14.0" edition = "2021" authors = ["github.com/containers"] license = "Apache-2.0" readme = "README.md" description = "A container network stack" homepage = "https://github.com/containers/netavark" repository = "https://github.com/containers/netavark" categories = ["virtualization"] exclude = ["/.cirrus.yml", "/.github/*", "/hack/*"] build = "build.rs" rust-version = "1.76" [package.metadata.vendor-filter] platforms = ["*-unknown-linux-*"] tier = "2" [[bin]] name = "netavark" path = "src/main.rs" [[bin]] name = "netavark-dhcp-proxy-client" path = "src/dhcp_proxy_client/client.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] default = ["serde", "deps-serde"] deps-serde = ["chrono/serde", "url/serde"] [dependencies] anyhow = "1.0.93" clap = { version = "~4.5.23", features = ["derive", "env"] } env_logger = "0.11.6" ipnet = { version = "2.11.0", features = ["serde"] } iptables = "0.5.2" libc = "0.2.157" log = "0.4.24" serde = { version = "1.0.213", features = ["derive"], optional = true } serde-value = "0.7.0" serde_json = "1.0.136" sysctl = "0.6.0" url = "2.5.3" zbus = { version = "4.3.1" } nix = { version = "0.29.0", features = ["sched", "signal", "user"] } rand = "0.9.0" sha2 = "0.10.8" netlink-packet-utils = "0.5.2" netlink-packet-route = "0.21.0" netlink-packet-core = "0.7.0" nftables = "0.5.0" fs2 = "0.4.3" netlink-sys = "0.8.7" tokio = { version = "1.43.0", features = ["rt", "rt-multi-thread", "signal", "fs"] } tokio-stream = { version = "0.1.17", features = ["net"] } tonic = "0.12.3" mozim = "0.2.5" prost = "0.13.4" futures-channel = "0.3.31" futures-core = "0.3.31" futures-util = "0.3.31" nispor = "1.2.22" tower = { version = "0.5.2", features = ["util"] } hyper-util = "0.1.10" [build-dependencies] chrono = { version = "0.4.39", default-features = false, features = ["clock"] } tonic-build = "0.12.3" [dev-dependencies] once_cell = "1.20.3" rand = "0.9.0" tempfile = "3.16.0" containers-netavark-68510f4/DISTRO_PACKAGE.md000066400000000000000000000065151475240420400204210ustar00rootroot00000000000000# Netavark: A container network stack This document is currently written with Fedora as a reference. As Netavark gets shipped in other distros, this should become a distro-agnostic document. ## Fedora Users Netavark is available as an officlal Fedora package on Fedora 35 and newer versions and is only meant to be used with Podman v4 and newer releases. ```console $ sudo dnf install netavark ``` **NOTE:** Fedora 35 users will not be able to install Podman v4 using the default yum repositories. Please consult the Podman packaging docs for instructions on how to fetch Podman v4.0 on Fedora 35. After installation, if you would like to migrate all your containers to use Netavark, you will need to set `network_backend = "netavark"` under the `[network]` section in your containers.conf (typically located at: `/usr/share/containers/containers.conf` If you would like to test the latest unreleased upstream code, try the podman-next COPR ```console $ sudo dnf copr enable rhcontainerbot/podman-next $ sudo dnf install netavark ``` **CAUTION:** The podman-next COPR provides the latest unreleased sources of Podman, Netavark and Aardvark-dns as rpms which would override the versions provided by the official packages. ## Distro Packagers The vendored sources for netavark will be attached to each netavark release as a tarball. You can download them with the following: `https://github.com/containers/netavark/releases/download/v{version}/netavark-v{version}-vendor.tar.gz` And then create a cargo config file to point it to the vendor dir. ``` tar xvf %{SOURCE} mkdir -p .cargo cat >.cargo/config << EOF [source.crates-io] replace-with = "vendored-sources" [source.vendored-sources] directory = "vendor" EOF ``` The Fedora packaging sources for Netavark are available at the [Netavark dist-git](https://src.fedoraproject.org/rpms/netavark). The Fedora packaged versions of the rust crates that Netavark depends on are frequently out of date, for example, rtnetlink, sha2, zbus and zvariant at the time of initial package creation. So, the Fedora package builds Netavark using the dependencies vendored upstream, found in the `vendor` subdirectory. The `netavark` binary is installed to `/usr/libexec/podman/netavark`. ## Dependency on aardvark-dns The netavark package has a `Recommends` on the `aardvark-dns` package. The aardvark-dns package will be installed by default with netavark, but netavark will be functional without it. ## Relationship with the CNI Plugins package While Netavark is a replacement for CNI Plugins (available as `containernetworking-plugins` on Fedora), the `netavark` package should be recommended for new installations but will not conflict with `containernetworking-plugins`. To avoid that conflict, we have made the following changes to the Fedora packages. 1. netavark package includes: ``` Provides: container-network-stack = 2 ``` 2. containernetworking-plugins package includes: ``` Provides: container-network-stack = 1 ``` 3. containers-common package includes: ``` Requires: container-network-stack Recommends: netavark ``` ## Listing bundled dependencies If you need to list the bundled dependencies in your packaging sources, you can run the `cargo tree` command in the upstream source. For example, Fedora's packaging source uses: ``` $ cargo tree --prefix none | awk '{print "Provides: bundled(crate("$1")) = "$2}' | sort | uniq ``` containers-netavark-68510f4/LICENSE000066400000000000000000000261351475240420400167650ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. containers-netavark-68510f4/Makefile000066400000000000000000000114761475240420400174220ustar00rootroot00000000000000# This Makefile is intended for developer convenience. For the most part # all the targets here simply wrap calls to the `cargo` tool. Therefore, # most targets must be marked 'PHONY' to prevent `make` getting in the way # #prog :=xnixperms DESTDIR ?= PREFIX ?= /usr/local LIBEXECDIR ?= ${PREFIX}/libexec LIBEXECPODMAN ?= ${LIBEXECDIR}/podman SYSTEMDDIR ?= ${PREFIX}/lib/systemd/system SELINUXOPT ?= $(shell test -x /usr/sbin/selinuxenabled && selinuxenabled && echo -Z) # Get crate version by parsing the line that starts with version. CRATE_VERSION ?= $(shell grep ^version Cargo.toml | awk '{print $$3}') GIT_TAG ?= $(shell git describe --tags) # Set this to any non-empty string to enable unoptimized # build w/ debugging features. debug ?= # Set path to cargo executable CARGO ?= cargo # All complication artifacts, including dependencies and intermediates # will be stored here, for all architectures. Use a non-default name # since the (default) 'target' is used/referenced ambiguously in many # places in the tool-chain (including 'make' itself). CARGO_TARGET_DIR ?= targets export CARGO_TARGET_DIR # 'cargo' is sensitive to this env. var. value. ifdef debug $(info debug is $(debug)) # These affect both $(CARGO_TARGET_DIR) layout and contents # Ref: https://doc.rust-lang.org/cargo/guide/build-cache.html release := profile :=debug else release :=--release profile :=release endif .PHONY: all all: build bin: mkdir -p $@ $(CARGO_TARGET_DIR): mkdir -p $@ .PHONY: build build: build_netavark build_proxy_client .PHONY: build_netavark build_netavark: bin $(CARGO_TARGET_DIR) $(CARGO) build $(release) cp $(CARGO_TARGET_DIR)/$(profile)/netavark bin/netavark$(if $(debug),.debug,) .PHONY: examples examples: bin $(CARGO_TARGET_DIR) cargo build --examples $(release) .PHONY: crate-publish crate-publish: @if [ "v$(CRATE_VERSION)" != "$(GIT_TAG)" ]; then\ echo "Git tag is not equivalent to the version set in Cargo.toml. Please checkout the correct tag";\ exit 1;\ fi @echo "It is expected that you have already done 'cargo login' before running this command. If not command may fail later" $(CARGO) publish --dry-run $(CARGO) publish .PHONY: clean clean: rm -rf bin if [ "$(CARGO_TARGET_DIR)" = "targets" ]; then rm -rf targets; fi $(MAKE) -C docs clean .PHONY: client client: bin $(CARGO_TARGET_DIR) $(CARGO) build --bin netavark-dhcp-proxy-client $(release) .PHONY: docs docs: ## build the docs on the host $(MAKE) -C docs NV_UNIT_FILES = contrib/systemd/system/netavark-dhcp-proxy.service \ contrib/systemd/system/netavark-firewalld-reload.service %.service: %.service.in sed -e 's;@@NETAVARK@@;$(LIBEXECPODMAN)/netavark;g' $< >$@.tmp.$$ \ && mv -f $@.tmp.$$ $@ .PHONY: install install: $(NV_UNIT_FILES) install ${SELINUXOPT} -D -m0755 bin/netavark $(DESTDIR)$(LIBEXECPODMAN)/netavark $(MAKE) -C docs install install ${SELINUXOPT} -m 755 -d ${DESTDIR}${SYSTEMDDIR} install ${SELINUXOPT} -m 644 contrib/systemd/system/netavark-dhcp-proxy.socket ${DESTDIR}${SYSTEMDDIR}/netavark-dhcp-proxy.socket install ${SELINUXOPT} -m 644 contrib/systemd/system/netavark-dhcp-proxy.service ${DESTDIR}${SYSTEMDDIR}/netavark-dhcp-proxy.service install ${SELINUXOPT} -m 644 contrib/systemd/system/netavark-firewalld-reload.service ${DESTDIR}${SYSTEMDDIR}/netavark-firewalld-reload.service .PHONY: uninstall uninstall: rm -f $(DESTDIR)$(LIBEXECPODMAN)/netavark rm -f $(PREFIX)/share/man/man1/netavark*.1 rm -f ${DESTDIR}${SYSTEMDDIR}/netavark-dhcp-proxy.service rm -f ${DESTDIR}${SYSTEMDDIR}/netavark-dhcp-proxy.socket .PHONY: test test: unit integration # Used by CI to compile the unit tests but not run them .PHONY: build_unit build_unit: $(CARGO_TARGET_DIR) $(CARGO) test --no-run .PHONY: unit unit: $(CARGO_TARGET_DIR) $(CARGO) test .PHONY: integration integration: $(CARGO_TARGET_DIR) examples # needs to be run as root or with podman unshare --rootless-netns bats test/ bats test-dhcp/ .PHONY: validate validate: $(CARGO_TARGET_DIR) $(CARGO) fmt --all -- --check $(CARGO) clippy -p netavark -- -D warnings $(MAKE) docs .PHONY: vendor-tarball vendor-tarball: build install.cargo-vendor-filterer VERSION=$(shell bin/netavark --version | cut -f2 -d" ") && \ $(CARGO) vendor-filterer --format=tar.gz --prefix vendor/ && \ mv vendor.tar.gz netavark-v$$VERSION-vendor.tar.gz && \ gzip -c bin/netavark > netavark.gz && \ sha256sum netavark.gz netavark-v$$VERSION-vendor.tar.gz > sha256sum .PHONY: install.cargo-vendor-filterer install.cargo-vendor-filterer: $(CARGO) install cargo-vendor-filterer .PHONY: mock-rpm mock-rpm: rpkg local .PHONY: help help: @echo "usage: make $(prog) [debug=1]" .PHONY: build_proxy_client build_proxy_client: bin $(CARGO_TARGET_DIR) $(CARGO) build --bin netavark-dhcp-proxy-client $(release) cp $(CARGO_TARGET_DIR)/$(profile)/netavark-dhcp-proxy-client bin/netavark-dhcp-proxy-client$(if $(debug),.debug,) containers-netavark-68510f4/OWNERS000066400000000000000000000001131475240420400167040ustar00rootroot00000000000000approvers: - baude - lsm5 - Luap99 - mheon reviewers: - flouthoc containers-netavark-68510f4/README.md000066400000000000000000000035701475240420400172350ustar00rootroot00000000000000# netavark: A container network stack Netavark is a rust based network stack for containers. It is being designed to work with [Podman](https://github.com/containers/podman) but is also applicable for other OCI container management applications. ## Overview and scope Netavark is a tool for configuring networking for Linux containers. Its features include: * Configuration of container networks via JSON configuration file * Creation and management of required network interfaces, including MACVLAN networks * All required firewall configuration to perform NAT and port forwarding as required for containers * Support for iptables, firewalld and nftables * Support for rootless containers * Support for IPv4 and IPv6 * Support for container DNS resolution via the [aardvark-dns](https://github.com/containers/aardvark-dns) project ## Requires - [go-md2man](https://github.com/cpuguy83/go-md2man) - [Rust](https://www.rust-lang.org/tools/install) - [Podman](https://podman.io/docs) 4.0+ - [protoc](https://grpc.io/docs/protoc-installation/) ## MSRV (Minimum Supported Rust Version) v1.76 We test that Netavark can be build on this Rust version and on some newer versions. All newer versions should also build, and if they do not, the issue should be reported and will be fixed. Older versions are not guaranteed to build and issues will not be fixed. ## Build ```console $ make ``` ## Test ```console $ make test ``` Also see [./test](./test/README.md) for more information. ## Communications For general questions and discussion, please use Podman's [channels](https://podman.io/community/). For discussions around issues/bugs and features, you can use the GitHub [issues](https://github.com/containers/netavark/issues) and [PRs](https://github.com/containers/netavark/pulls) tracking system. ## Plugins Netavark also supports executing external plugins, see [./plugin-API.md](./plugin-API.md). containers-netavark-68510f4/RELEASE_NOTES.md000066400000000000000000000131771475240420400203340ustar00rootroot00000000000000# Release Notes ## v1.14.0 * bridge: Add support for a new option called `mode`. When set to `unmanaged` only the veth pair and ip addresses are setup. The bridge must exist and no firewall or sysctl setting will be configured in this mode. ([#1090](https://github.com/containers/netavark/issues/1090)) * bridge: Add support for DHCP when using unmanaged mode. ([#868](https://github.com/containers/netavark/issues/868)) * bridge: Add support for the `vlan` option. ([#1028](https://github.com/containers/netavark/issues/1028)) * When using DHCP netavark will now send the container hostname in the DHCP request and use the container id as client id. ([#676](https://github.com/containers/netavark/issues/676)) * The firewalld driver was improved and major outstanding bugs were addressed but is still considered experimental. A new man page `netavark-firewalld(7)` has been added to document some of the firewalld interactions. * Dependency updates. ## v1.13.1 * Fixed a bug where port forwarding rules might not be removed correctly on nftables when different host ips are used for the same port. ([#1129](https://github.com/containers/netavark/issues/1129)) * On aardvark-dns setup errors properly cleanup interfaces and firewall rules again. ([#1121](https://github.com/containers/netavark/issues/1121)) ## v1.13.0 * Fixed bug where port forwarding rules might not be removed correctly on nftables * Add DNS DNAT rules first with nftables ## v1.12.2 * Ensure DNS rules cover TCP for iptables and nftables * On ardvark-dns start, delete entries again on failure ## v1.12.1 * Fixed problem with categories in Cargo.toml that prevented us from publishing v1.12.0 ## v1.12.0 * Dependency updates * Netavark-DHCP proxy: use dns servers from dhcp lease * Improved handling and visibility of errors from aardvark-dns * Use nftables as default driver for Fedora 41 ## v1.11.0 * Do not perform namespace detection for aardvark-dns updates as it is not needed * Fixed condition where ignored errors were being returned as real * With nftables, only dump netavark table rules * Fix port forward with strict RPF and multi-networks * updated dependencies ## v1.10.1 * updated nftables to 0.3 ## v1.10.0 * added an nftables backend that allows its use on systems without iptables installed * added command line option to change firewall driver * show error if process is in wrong netns * removed unessesary unlock lockfile calls * updated dependencies ## v1.9.0 * add firewalld-reload subcommand * bridge: force static mac on bridge interface * dependency updates * numerous fixes to test suite ## v1.8.0 * iptables: improve error when ip6?tables commands are missing * docs: Convert markdown with go-md2man instead of mandown * iptables: drop invalid packages * bump rust edition to 2021 * Add ACCEPT rules in firewall for bridge network with internal dns * Add vrf support for bridges ## v1.7.0 * Fix misleading dns disabled log * Dependency updates * --config is now required when dns is used * netavark dhcp-proxy correctly renews the lease after dhcp time-out * bridge: isolate=strict option has been added * macvlan: bclim option has been added * "no_default_route" option has been added * static routes can now be configured ## v1.6.0 * Now supports a driver plugin module for user defined network drivers * Initial MACVLAN DHCP support (additional unit file required for packagers) * Dependency updates ## v1.5.0 * Removed crossbeam-utils * Dependency updates * Preliminary macvlan dhcp support (not fully supported yet) * Addition of ipvlan support ## v1.4.0 * Added network update command * Corrected issue #491 to only teardown network forwarding when on complete teardown only * Fixed some rust documentation ## v1.3.0 * Housekeep and code cleanup * macvlan: remove tmp interface when name already used in netns * Add support for route metrics * netlink: return better error if ipv6 is disabled * macvlan: fix name collision on hostns * Ignore dns-enabled for macvlan (BZ2137320) * better errors on teardown * allow customer dns servers for containers * do not set route for internal-only networks * do not use ipv6 autoconf ## v1.2.0 * Reworked how netavark calls aardvark * Implemented locking when committing * Remove bridge only when no containers are attached * Updated versions of libraries where possible ## v1.1.0 * Netavark is now capable of starting Aardvark on a port other than 53 (controlled by `dns_bind_port` in `containers.conf`). Firewall rules are added to ensure DNS still functions properly despite the port change. * Added the ability to isolate networks. Networks with the isolate option set cannot communicate with other networks with the isolate option set. * Improved the way Aardvark is launched to avoid potential race conditions where DNS would not be ready when containers were started. * Fixed a bug where Aardvark could not be run in environments with a read-only `/proc` (e.g. inside a container). ## v1.0.3 * Updated dependenciess * Simplified option parsing for bridge/macvlan * Added support for an ipam `none` driver ## v1.0.2 * Fix issue [#13533](https://github.com/containers/podman/issues/13533) - only use systemd when present * Dropped vergen dependency * Updated several dependency libraries * Allow macvlans to not require a default gateway ## v1.0.1 * core,macvlan: add gateway as default route to macvlan interface * Add host_ip and container_ip version matching to iptables portforwardinhg * Remove vendor directory from upstream github repo ## v1.0.0 * First official release of netavark ## v1.0.0-RC2 * RC2 containers several bug fixes and code cleanup ## v1.0.0-RC1 * This is the first release candidate of Netavark. All functionality should be working. containers-netavark-68510f4/SECURITY.md000066400000000000000000000003571475240420400175470ustar00rootroot00000000000000## Security and Disclosure Information Policy for the Netavark Project The Netavark Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects. containers-netavark-68510f4/build.rs000066400000000000000000000063421475240420400174230ustar00rootroot00000000000000use chrono::{DateTime, Utc}; use std::env; use std::path::Path; use std::process::Command; fn main() { let builder = tonic_build::configure() .type_attribute("netavark_proxy.Lease", "#[derive(serde::Serialize)]") .type_attribute("netavark_proxy.DhcpV4Lease", "#[derive(serde::Serialize)]") .type_attribute("netavark_proxy.DhcpV6Lease", "#[derive(serde::Serialize)]") .type_attribute("netavark_proxy.IPResponse", "#[derive(serde::Serialize)]") .type_attribute("netavark_proxy.MacAddress", "#[derive(serde::Serialize)]") .type_attribute("netavark_proxy.NvIpv4Addr", "#[derive(serde::Serialize)]") .type_attribute("netavark_proxy.Lease", "#[derive(serde::Deserialize)]") .type_attribute( "netavark_proxy.DhcpV4Lease", "#[derive(serde::Deserialize)]", ) .type_attribute( "netavark_proxy.DhcpV6Lease", "#[derive(serde::Deserialize)]", ) .type_attribute("netavark_proxy.IPResponse", "#[derive(serde::Deserialize)]") .type_attribute("netavark_proxy.MacAddress", "#[derive(serde::Deserialize)]") .type_attribute("netavark_proxy.NvIpv4Addr", "#[derive(serde::Deserialize)]") .type_attribute("netavark_proxy.MacAddress", "#[derive(Eq)]") .type_attribute("netavark_proxy.MacAddress", "#[derive(Hash)]") .type_attribute( "netavark_proxy.NetworkConfig", "#[derive(serde::Deserialize)]", ) .type_attribute( "netavark_proxy.NetworkConfig", "#[derive(serde::Serialize)]", ); builder .compile_protos(&[Path::new("src/proto/proxy.proto")], &[Path::new("proto")]) .unwrap_or_else(|e| panic!("Failed at builder: {:?}", e.to_string())); // Generate the default 'cargo:' instruction output println!("cargo:rerun-if-changed=build.rs"); // get timestamp let now = match env::var("SOURCE_DATE_EPOCH") { Ok(val) => DateTime::from_timestamp(val.parse::().unwrap(), 0).unwrap(), Err(_) => Utc::now(), }; println!("cargo:rustc-env=BUILD_TIMESTAMP={}", now.to_rfc3339()); // get rust target triple from TARGET env println!( "cargo:rustc-env=BUILD_TARGET={}", std::env::var("TARGET").unwrap() ); // get git commit let command = Command::new("git").args(["rev-parse", "HEAD"]).output(); let commit = match command { Ok(output) => String::from_utf8(output.stdout).unwrap(), // if error, e.g. build from source without git repo, just show empty string Err(_) => "".to_string(), }; println!("cargo:rustc-env=GIT_COMMIT={commit}"); // Handle default firewall driver. // Allowed values "nftables" and "iptables". let fwdriver = match env::var("NETAVARK_DEFAULT_FW") .unwrap_or("iptables".to_string()) .as_str() { "nftables" => "nftables", "iptables" => "iptables", "none" => "none", inv => panic!("Invalid default firewall driver {}", inv), }; println!("cargo:rustc-check-cfg=cfg(default_fw, values(\"nftables\", \"iptables\", \"none\"))"); println!("cargo:rustc-cfg=default_fw=\"{}\"", fwdriver); println!("cargo:rustc-env=DEFAULT_FW={fwdriver}"); } containers-netavark-68510f4/contrib/000077500000000000000000000000001475240420400174115ustar00rootroot00000000000000containers-netavark-68510f4/contrib/cirrus/000077500000000000000000000000001475240420400207205ustar00rootroot00000000000000containers-netavark-68510f4/contrib/cirrus/cache_groom.sh000066400000000000000000000070051475240420400235240ustar00rootroot00000000000000#!/bin/bash # # This script is intended to be run from Cirrus-CI to prepare the # rust targets cache for re-use during subsequent runs. This mainly # involves removing files and directories which change frequently # but are cheap/quick to regenerate - i.e. prevent "cache-flapping". # Any other use of this script is not supported and may cause harm. # # WARNING: This script is re-used from $DEST_BRANCH by other # repositories. Namely aardvark-dns and possibly others. Check # before removing / changing / updating. set -eo pipefail source $(dirname ${BASH_SOURCE[0]})/lib.sh if [[ "$CIRRUS_CI" != true ]]; then die "Script is not intended for use outside of Cirrus-CI" fi req_env_vars CARGO_HOME CARGO_TARGET_DIR CIRRUS_BUILD_ID # Giant-meat-cleaver HACK: It's possible (with a long-running cache key) for # the targets and/or cargo cache to grow without-bound (gigabytes). Ref: # https://github.com/rust-lang/cargo/issues/5026 # There isn't a good way to deal with this or account for outdated content # in some intelligent way w/o trolling through config and code files. So, # Any time the Cirrus-CI build ID is evenly divisible by some number (chosen # arbitrarily) clobber the whole thing and make the next run entirely # re-populate cache. This is ugly, but maybe the best option available :( if [[ "$CIRRUS_BRANCH" == "$DEST_BRANCH" ]] && ((CIRRUS_BUILD_ID%15==0)); then msg "It's a cache-clobber build, yay! This build has been randomly selected for" msg "a forced cache-wipe! Congratulations! This means the next build will be" msg "slow, and nobody will know who to to blame!. Lucky you! Hurray!" msg "(This is necessary to prevent branch-level cache from infinitely growing)" cd $CARGO_TARGET_DIR # Could use `cargo clean` for this, but it's easier to just clobber everything. rm -rf ./* ./.??* # In case somebody goes poking around, leave a calling-card hopefully leading # them back to this script. I don't know of a better way to handle this :S touch CACHE_WAS_CLOBBERED cd $CARGO_HOME rm -rf ./* ./.??* touch CACHE_WAS_CLOBBERED exit 0 fi # The following applies to both PRs and branch-level cache. It attempts to remove # things which are non-essential and/or may change frequently. It stops short of # trolling through config & code files to determine what is relevant or not. # Ref: https://doc.rust-lang.org/nightly/cargo/guide/build-cache.html # https://github.com/Swatinem/rust-cache/tree/master/src cd $CARGO_TARGET_DIR for targetname in $(find ./ -type d -maxdepth 1 -mindepth 1); do msg "Grooming $CARGO_TARGET_DIR/$targetname..." cd $CARGO_TARGET_DIR/$targetname # Any top-level hidden files or directories showrun rm -rf ./.??* # Example targets showrun rm -rf ./target/debug/examples # Documentation showrun rm -rf ./target/doc # Internal to rust build process showrun rm -rf ./target/debug/deps ./target/debug/incremental ./target/debug/build done # The following only applies to dependent packages (crates). It follows recommendations # Ref: https://doc.rust-lang.org/nightly/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci # and probably shouldn't be extended beyond what's documented. This cache plays a major # role in built-time reduction, but must also be prevented from causing "cache-flapping". cd $CARGO_HOME for dirname in $(find ./ -type d -maxdepth 2 -mindepth 1); do case "$dirname" in ./bin) ;& # same steps as next item ./registry/index) ;& ./registry/cache) ;& ./git/db) continue ;; # Keep *) rm -rf $dirname ;; # Remove esac done containers-netavark-68510f4/contrib/cirrus/lib.sh000066400000000000000000000044011475240420400220210ustar00rootroot00000000000000 # Library of common, shared utility functions. This file is intended # to be sourced by other scripts, not called directly. # BEGIN Global export of all variables set -a # Automation library installed at image-build time, # defining $AUTOMATION_LIB_PATH in this file. if [[ -r "/etc/automation_environment" ]]; then source /etc/automation_environment fi if [[ -n "$AUTOMATION_LIB_PATH" ]]; then source $AUTOMATION_LIB_PATH/common_lib.sh else ( echo "WARNING: It does not appear that containers/automation was installed." echo " Functionality of most of this library will be negatively impacted" echo " This ${BASH_SOURCE[0]} was loaded by ${BASH_SOURCE[1]}" ) > /dev/stderr fi # Unsafe env. vars for display SECRET_ENV_RE='(ACCOUNT)|(GC[EP]..+)|(SSH)|(PASSWORD)|(TOKEN)' # setup.sh calls make_cienv() to cache these values for the life of the VM if [[ -r "/etc/ci_environment" ]]; then source /etc/ci_environment else # set default values - see make_cienv() below # VM Images are built with this setup CARGO_HOME="${CARGO_HOME:-/var/cache/cargo}" source $CARGO_HOME/env # Make caching more effective - disable incremental compilation, # so that the Rust compiler doesn't waste time creating the # additional artifacts required for incremental builds. # Ref: https://github.com/marketplace/actions/rust-cache#cache-details CARGO_INCREMENTAL=0 fi # END Global export of all variables set -a # Shortcut to automation library timeout/retry function retry() { err_retry 8 1000 "" "$@"; } # just over 4 minutes max # Helper to ensure a consistent environment across multiple CI scripts # containers, and shell environments (e.g. hack/get_ci_vm.sh) make_cienv(){ local envname local envval local SETUP_ENVIRONMENT=1 for envname in CARGO_HOME PATH CIRRUS_WORKING_DIR SETUP_ENVIRONMENT; do envval="${!envname}" # Properly escape values to prevent injection printf -- "$envname=%q\n" "$envval" done } complete_setup(){ set +x msg "************************************************************" msg "Completing environment setup, writing vars:" msg "************************************************************" make_cienv | tee -a /etc/ci_environment } containers-netavark-68510f4/contrib/cirrus/runner.sh000077500000000000000000000040751475240420400225760ustar00rootroot00000000000000#!/bin/bash set -eo pipefail # This script runs in the Cirrus CI environment, invoked from .cirrus.yml . # It can also be invoked manually in a `hack/get_ci_cm.sh` environment, # documentation of said usage is TBI. # # The principal deciding factor is the first argument. For any # given value 'xyz' there must be a function '_run_xyz' to handle that # argument. source $(dirname ${BASH_SOURCE[0]})/lib.sh _run_noarg() { die "runner.sh must be called with a single argument" } _run_build() { # Assume we're on a fast VM, compile everything needed by the # rest of CI since subsequent tasks may have limited resources. make all debug=1 make build_unit # reuses some debug binaries make all # optimized/non-debug binaries # This will get scooped up and become part of the artifact archive. # Identify where the binary came from to benefit downstream consumers. cat | tee bin/netavark.info << EOF repo: $CIRRUS_REPO_CLONE_URL branch: $CIRRUS_BASE_BRANCH title: $CIRRUS_CHANGE_TITLE commit: $CIRRUS_CHANGE_IN_REPO build: https://cirrus-ci.com/build/$CIRRUS_BUILD_ID task: https://cirrus-ci.com/task/$CIRRUS_TASK_ID EOF } _run_build_aarch64() { _run_build } _run_validate() { make validate } _run_validate_aarch64() { _run_validate } _run_unit() { make unit } _run_unit_aarch64() { _run_unit } _run_integration() { make integration } _run_integration_aarch64() { _run_integration } show_env_vars msg "************************************************************" msg "Toolchain details" msg "************************************************************" rustc --version cargo --version msg "************************************************************" msg "Runner executing '$1' on $OS_REL_VER" msg "************************************************************" ((${SETUP_ENVIRONMENT:-0})) || \ die "Expecting setup.sh to have completed successfully" cd "${CIRRUS_WORKING_DIR}/" handler="_run_${1:-noarg}" if [ "$(type -t $handler)" != "function" ]; then die "Unknown/Unsupported runner.sh argument '$1'" fi $handler containers-netavark-68510f4/contrib/cirrus/setup.sh000077500000000000000000000027421475240420400224240ustar00rootroot00000000000000#!/bin/bash # This script configures the CI runtime environment. It's intended # to be used by Cirrus-CI, not humans. set -e source $(dirname $0)/lib.sh # Only do this once if [[ -r "/etc/ci_environment" ]]; then msg "It appears ${BASH_SOURCE[0]} already ran, exiting." exit 0 fi trap "complete_setup" EXIT msg "************************************************************" msg "Setting up runtime environment" msg "************************************************************" show_env_vars req_env_vars AARDVARK_DNS_URL AARDVARK_DNS_BRANCH cd /usr/libexec/podman rm -vf aardvark-dns* if showrun curl --fail --location -o /tmp/aardvark-dns.zip "$AARDVARK_DNS_URL" && \ unzip -o /tmp/aardvark-dns.zip; then if [[ $(uname -m) != "x86_64" ]]; then showrun mv aardvark-dns.$(uname -m)-unknown-linux-gnu aardvark-dns fi showrun chmod a+x /usr/libexec/podman/aardvark-dns else warn "Error downloading/extracting the latest pre-compiled aardvark binary from CI" showrun cargo install \ --root /usr/libexec/podman \ --git https://github.com/containers/aardvark-dns \ --branch "$AARDVARK_DNS_BRANCH" mv -v /usr/libexec/podman/bin/aardvark-dns /usr/libexec/podman fi # show aardvark commit in CI logs showrun /usr/libexec/podman/aardvark-dns version # Warning, this isn't the end. An exit-handler is installed to finalize # setup of env. vars. This is required for runner.sh to operate properly. # See complete_setup() in lib.sh for details. containers-netavark-68510f4/contrib/container_images/000077500000000000000000000000001475240420400227205ustar00rootroot00000000000000containers-netavark-68510f4/contrib/container_images/Dockerfile.Rust000066400000000000000000000004221475240420400256440ustar00rootroot00000000000000# Source for quay.io/libpod/nv-rust # This version should always match the MSRV, when you update this also update # the version in the root README.md and Cargo.toml. FROM docker.io/library/rust:1.76 RUN apt-get update && apt-get -y install protobuf-compiler libprotobuf-dev containers-netavark-68510f4/contrib/container_images/README.md000066400000000000000000000006721475240420400242040ustar00rootroot00000000000000# Rust image 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. If this is acceptable then update the version in the Dockerfile.Rust FROM line then build the new image see below. # Build and publish rust image Make sure you have valid `quay.io/libpod` credentials in order to push the image there. Then run the script `build_and_publish_rust_image.sh` to build and push it. containers-netavark-68510f4/contrib/container_images/build_and_publish_rust_image.sh000077500000000000000000000007761475240420400311570ustar00rootroot00000000000000#!/bin/bash set -e PODMAN=${PODMAN:-podman} SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}") DOCKERFILE="$(dirname "${BASH_SOURCE[0]}")/Dockerfile.Rust" # get the tag from the Dockerfile so we do not duplicate it TAG=$(awk -F':' '/FROM/{print $NF}' $DOCKERFILE) if [[ -z "$TAG" ]]; then echo "Empty tag in $DOCKERFILE; the tag must specify the rust version to use" >&2 exit 1 fi FULL_IMAGE_NAME="quay.io/libpod/nv-rust:$TAG" $PODMAN build -t $FULL_IMAGE_NAME -f $DOCKERFILE $PODMAN push $FULL_IMAGE_NAME containers-netavark-68510f4/contrib/systemd/000077500000000000000000000000001475240420400211015ustar00rootroot00000000000000containers-netavark-68510f4/contrib/systemd/system/000077500000000000000000000000001475240420400224255ustar00rootroot00000000000000containers-netavark-68510f4/contrib/systemd/system/netavark-dhcp-proxy.service.in000066400000000000000000000003541475240420400303240ustar00rootroot00000000000000[Unit] Description=Netavark DHCP proxy service Requires=netavark-dhcp-proxy.socket After=netavark-dhcp-proxy.socket StartLimitIntervalSec=0 [Service] Type=exec ExecStart=@@NETAVARK@@ dhcp-proxy -a 30 [Install] WantedBy=default.target containers-netavark-68510f4/contrib/systemd/system/netavark-dhcp-proxy.socket000066400000000000000000000002201475240420400275370ustar00rootroot00000000000000[Unit] Description=Netavark DHCP proxy socket [Socket] ListenStream=%t/podman/nv-proxy.sock SocketMode=0660 [Install] WantedBy=sockets.target containers-netavark-68510f4/contrib/systemd/system/netavark-firewalld-reload.service.in000066400000000000000000000006221475240420400314420ustar00rootroot00000000000000[Unit] Description=Listen for the firewalld reload event and reapply all netavark firewall rules. # This causes systemd to stop this unit when firewalld is stopped. PartOf=firewalld.service After=firewalld.service [Service] ExecStart=@@NETAVARK@@ firewalld-reload [Install] # If the unit is enabled add a wants to firewalld so it is only started when firewalld is started. WantedBy=firewalld.service containers-netavark-68510f4/docs/000077500000000000000000000000001475240420400167015ustar00rootroot00000000000000containers-netavark-68510f4/docs/Makefile000066400000000000000000000010611475240420400203370ustar00rootroot00000000000000PREFIX ?= /usr/local DATADIR ?= ${PREFIX}/share MANDIR ?= $(DATADIR)/man GO ?= go GOMD2MAN ?= go-md2man docs: $(patsubst %.md,%,$(wildcard *.[0-9].md)) %.1: %.1.md $(GOMD2MAN) -in $^ -out $@ %.7: %.7.md $(GOMD2MAN) -in $^ -out $@ .PHONY: .install.md2man .install.md2man: $(GO) install github.com/cpuguy83/go-md2man/v2@latest .PHONY: install install: install -d ${DESTDIR}/${MANDIR}/man1 install -m 0644 *.1 ${DESTDIR}/${MANDIR}/man1 install -d ${DESTDIR}/${MANDIR}/man7 install -m 0644 *.7 ${DESTDIR}/${MANDIR}/man7 .PHONY: clean clean: $(RM) *.1 containers-netavark-68510f4/docs/generate-code-coverage-report.md000066400000000000000000000021771475240420400250360ustar00rootroot00000000000000# Generating a code coverage report ## Get set up - Enable `nightly` Rust: ```bash rustup override set nightly ``` - Build the demangler: ```bash cargo install rustfilt ``` - Install Grcov ```bash cargo install grcov ``` ## Generate the report - Build the program with coverage profiling enabled in the Rust compiler: ```bash RUSTFLAGS="-C instrument-coverage" make ``` - Run all of the tests: ```bash make test #or cargo test sudo bats test/ sudo bats test-dhcp/ ``` - Use `grcov` to generate a report: ```bash grcov . -s . --binary-path ./targets/release/ -t html --branch --ignore-not-existing --ignore '../*' --ignore '/*' -o target/coverage/ ``` ## Access the report - To view the code coverage report, you can open the html file generated by `grcov` in your browser: ```bash firefox target/coverage/index.html ``` - You will now have a large collection of raw profiling data in the form of default_*.profraw files. Now that the report has been created, you can delete these files. ### Additional Resources - **Instrumentation-based Code Coverage**: https://doc.rust-lang.org/rustc/instrument-coverage.html#-c-instrument-coverageoptions containers-netavark-68510f4/docs/netavark-dhcp-proxy.md000066400000000000000000000023561475240420400231370ustar00rootroot00000000000000% netavark-dhcp-proxy(1) ## NAME netavark-dhcp-proxy - a proxy for DHCP interactions with containers ## SYNOPSIS **netavark-dhcp-proxy** [*options*] *command* ## DESCRIPTION When using DHCP with MacVLAN and containers, you need the container to either have an init system with DHCP clients or some sort of proxy server that can act on behalf of the container. The netavark-dhcp-proxy is the latter and should be used in combination with Podman and Netavark when setting up containers that wish to use DHCP and MacVLAN networking. **netavark-dhcp-proxy [GLOBAL OPTIONS]** ## GLOBAL OPTIONS #### **--activity-timeout, -a** Time in seconds when the proxy should exit if it has no leases. The default time is *300* seconds. A value of *0* disables the activity timeout. #### **--dir**=*path* The directory option is a path to store the lease backup files. The default is */run/podman/*. The lease name is *nv-proxy.leases*. #### **--uds** Set the unix domain socket directory instead of using the default. The default is */run/podman*. The socket name is *nv-proxy.sock*. #### **--help**, **-h** Print usage statement #### **--version**, **-v** Print the version ## HISTORY Sep 2022, Originally compiled by Brent Baude containers-netavark-68510f4/docs/netavark-firewalld.7.md000066400000000000000000000116431475240420400231570ustar00rootroot00000000000000% NETAVARK-FIREWALLD 7 Netavark Firewalld Interactions Man Page % Matthew Heon % January 2025 ## Name netavark-firewalld - description of the interaction of Netavark and firewalld ## Description Netavark can be used on systems with firewalld enabled without issue. When using the default `nftables` or `iptables` firewall drivers, on systems where firewalld is running, firewalld will automatically be configured to allow connectivity to Podman containers. All subnets of Podman-managed networks will be automatically added to the `trusted` zone to allow this access. ### Firewalld Driver There is also a dedicated firewalld driver in Netavark. This driver uses the firewalld DBus API to natively interact with firewalld. It can be enabled by setting `firewall_driver` to `firewalld` in `containers.conf`. The native firewalld driver offers better integration with firewalld, but presently suffers from several limitations. It does not support isolation (the `--opt isolate=` option to `podman network create`. Connections to ports forwarded by a container on the same host can only be made through the IPv4 localhost IP (`127.0.0.1`). Using other IPs on the host will not work, unless the connection comes from a separate host. ### Strict Port Forwarding Since firewalld version 2.3.0, a new setting, `StrictForwardPorts`, has been added. The setting is located in `/etc/firewalld/firewalld.conf` and defaults to `no` (disabled). When disabled, port forwarding with Podman works as normal. When it is enabled (set to `yes`), port forwarding with root Podman will become nonfunctional. Attempting to start a container or pod with the `-p` or `-P` options will return errors. When StrictForwardPorts is enabled, all port forwarding must be done through firewalld using the firewall-cmd tool. This ensures that containers cannot allow traffic through the firewall without administrator intervention. Please note that rootless Podman is unaffected by this setting and will function as it always has. Instead, containers should be started without forwarded ports specified and preferably with static IPs. To forward a port externally, the following command should be run, substituting the desired host and container port numbers, protocol, and the container's IP. ``` # firewall-cmd --permanent --zone {ZONE} --add-forward-port=port={HOST_PORT_NUMBER}:proto={PROTOCOL}:toport={CONTAINER_PORT_NUMBER}:toaddr={CONTAINER_IP} ``` If you are not sure which zone to use, the `public` zone should always work. If the container does not have a static IP, it can be found with `podman inspect`: ``` # podman inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' {CONTAINER_NAME_OR_ID} ``` Once the container is stopped or removed, the rule must be manually removed with the following command: ``` # firewall-cmd --permanent --zone {ZONE} --remove-forward-port=port={HOST_PORT_NUMBER}:proto={PROTOCOL}:toport={CONTAINER_PORT_NUMBER}:toaddr={CONTAINER_IP} ``` To also allow forwarding via IPv4 localhost (`127.0.0.1`), a firewalld policy must be added, as well as a rich rule for each port requiring localhost forwarding. Forwarding via IPv6 localhost is not possible due to kernel limitations. To add the policies required for IPv4 localhost forwarding, the following commands must be run. This is only necessary once per system. ``` # firewall-cmd --permanent --new-policy localhostForward # firewall-cmd --permanent --policy localhostForward --add-ingress-zone HOST # firewall-cmd --permanent --policy localhostForward --add-egress-zone ANY ``` A further rich rule for each container is required: ``` # firewall-cmd --permanent --policy localhostForward --add-rich-rule='rule family=ipv4 destination address=127.0.0.0/8 forward-port port={HOST_PORT_NUMBER} protocol={PROTOCOL} to-port={CONTAINER_PORT_NUMBER} to-addr={CONTAINER_IP}' ``` These rules must be manually removed when the container is stopped or removed with the following command: ``` # firewall-cmd --permanent --policy localhostForward --remove-rich-rule='rule family=ipv4 destination address=127.0.0.0/8 forward-port port={HOST_PORT_NUMBER} protocol={PROTOCOL} to-port={CONTAINER_PORT_NUMBER} to-addr={CONTAINER_IP}' ``` The associated `localhostForward` policy does not need to be removed. Please also note that, at present, it is only possible to access forwarded ports of a container on the same host via the IPv4 localhost IP (`127.0.0.1`), and only when the rich rule above has been applied. Accessing via an IP that is not `127.0.0.1` from the same host is presently not possible, but we hope to address this with a future firewalld release. Please note that the firewalld driver presently bypasses this protection, and will still allow traffic through the firewall when `StrictForwardPorts` is enabled without manual forwarding through `firewall-cmd`. This may be changed in a future release. ## SEE ALSO firewalld(1), firewall-cmd(1), firewalld.conf(5), podman(1), containers.conf(5) ## Authors Matthew Heon containers-netavark-68510f4/docs/netavark.1.md000066400000000000000000000031731475240420400212010ustar00rootroot00000000000000% netavark(1) ## NAME netavark - Configure a given network namespace for use by a container ## SYNOPSIS **netavark** [*options*] *command* *network namespace path* ## DESCRIPTION Netavark configures a network namespace according to a configuration read from STDIN. The configuration is JSON formatted. ## GLOBAL OPTIONS #### **--file**, **-f** 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. ## COMMANDS ### netavark setup The setup command configures the given network namespace with the given configuration, creating any interfaces and firewall rules necessary. ### netavark teardown The teardown command is the inverse of the setup command, undoing any configuration applied. Some interfaces may not be deleted (bridge interfaces, for example, will not be removed). ### CONFIGURATION FORMAT The configuration accepted is the same for both setup and teardown. It is JSON formatted. 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) TODO: Transcribe configuration into here in a nice tabular format ## EXAMPLE netavark setup /run/user/1000/podman/netns/d11d1f9c499d netavark -f /run/podman/828b0508ae64.conf teardown /run/podman/netns/828b0508ae64 ## SEE ALSO podman(1) ## HISTORY September 2021, Originally compiled by Matt Heon containers-netavark-68510f4/docs/publish-crate.md000066400000000000000000000004771475240420400217750ustar00rootroot00000000000000# Publishing netavark crate to crates.io ### Steps * Make sure you have already done `cargo login` on your current session with a valid token. * `cd netavark` * Git checkout the version which you want to publish. * `make crate-publish` * New version should be reflected here: https://crates.io/crates/netavark/versions containers-netavark-68510f4/examples/000077500000000000000000000000001475240420400175675ustar00rootroot00000000000000containers-netavark-68510f4/examples/error-plugin.rs000066400000000000000000000016401475240420400225630ustar00rootroot00000000000000//! This is just an example plugin, do not use it in production! use netavark::{ network::types, new_error, plugin::{Info, Plugin, PluginExec, API_VERSION}, }; fn main() { let info = Info::new("0.1.0-dev".to_owned(), API_VERSION.to_owned(), None); PluginExec::new(Exec {}, info).exec(); } struct Exec {} impl Plugin for Exec { fn create( &self, _network: types::Network, ) -> Result> { Err(new_error!("create error")) } fn setup( &self, _netns: String, _opts: types::NetworkPluginExec, ) -> Result> { Err(new_error!("setup error")) } fn teardown( &self, _netns: String, _opts: types::NetworkPluginExec, ) -> Result<(), Box> { Err(new_error!("teardown error")) } } containers-netavark-68510f4/examples/host-device-plugin.rs000066400000000000000000000063171475240420400236520ustar00rootroot00000000000000//! This is just an example plugin, do not use it in production! use std::{collections::HashMap, os::fd::AsFd}; use netavark::{ network::{ core_utils::{open_netlink_sockets, CoreUtils}, netlink, types, }, new_error, plugin::{Info, Plugin, PluginExec, API_VERSION}, }; use netlink_packet_route::{address::AddressAttribute, link::LinkAttribute}; fn main() { let info = Info::new("0.1.0-dev".to_owned(), API_VERSION.to_owned(), None); PluginExec::new(Exec {}, info).exec(); } struct Exec {} impl Plugin for Exec { fn create( &self, network: types::Network, ) -> Result> { if network.network_interface.as_deref().unwrap_or_default() == "" { return Err(new_error!("no network interface is specified")); } Ok(network) } fn setup( &self, netns: String, opts: types::NetworkPluginExec, ) -> Result> { let (mut host, netns) = open_netlink_sockets(&netns)?; let name = opts.network.network_interface.unwrap_or_default(); let link = host.netlink.get_link(netlink::LinkID::Name(name.clone()))?; let mut mac_address = String::from(""); for nla in link.attributes { if let LinkAttribute::Address(ref addr) = nla { mac_address = CoreUtils::encode_address_to_hex(addr); } } let addresses = host.netlink.dump_addresses()?; let mut subnets = Vec::new(); for address in addresses { if address.header.index == link.header.index { for nla in address.attributes { if let AddressAttribute::Address(ip) = &nla { let net = ipnet::IpNet::new(*ip, address.header.prefix_len)?; subnets.push(types::NetAddress { gateway: None, ipnet: net, }) } } } } host.netlink .set_link_ns(link.header.index, netns.file.as_fd())?; // interfaces map, but we only ever expect one, for response let mut interfaces: HashMap = HashMap::new(); let interface = types::NetInterface { mac_address, subnets: Option::from(subnets), }; interfaces.insert(name, interface); // StatusBlock response let response = types::StatusBlock { dns_server_ips: None, dns_search_domains: None, interfaces: Some(interfaces), }; Ok(response) } fn teardown( &self, netns: String, opts: types::NetworkPluginExec, ) -> Result<(), Box> { // on teardown revert what was done in setup let (host, mut netns) = open_netlink_sockets(&netns)?; let name = opts.network.network_interface.unwrap_or_default(); let link = netns.netlink.get_link(netlink::LinkID::Name(name))?; netns .netlink .set_link_ns(link.header.index, host.file.as_fd())?; Ok(()) } } containers-netavark-68510f4/examples/stderr-plugin.rs000066400000000000000000000021741475240420400227400ustar00rootroot00000000000000//! This is just an example plugin, do not use it in production! use netavark::{ network::types, plugin::{Info, Plugin, PluginExec, API_VERSION}, }; fn main() { let info = Info::new("0.1.0-dev".to_owned(), API_VERSION.to_owned(), None); PluginExec::new(Exec {}, info).exec(); } struct Exec {} impl Plugin for Exec { fn create( &self, network: types::Network, ) -> Result> { eprintln!("stderr create"); Ok(network) } fn setup( &self, _netns: String, _opts: types::NetworkPluginExec, ) -> Result> { eprintln!("stderr setup"); // StatusBlock response let response = types::StatusBlock { dns_server_ips: None, dns_search_domains: None, interfaces: None, }; Ok(response) } fn teardown( &self, _netns: String, _opts: types::NetworkPluginExec, ) -> Result<(), Box> { eprintln!("stderr teardown"); Ok(()) } } containers-netavark-68510f4/hack/000077500000000000000000000000001475240420400166575ustar00rootroot00000000000000containers-netavark-68510f4/hack/get_ci_vm.sh000077500000000000000000000046371475240420400211640ustar00rootroot00000000000000#!/usr/bin/env bash # # For help and usage information, simply execute the script w/o any arguments. # # This script is intended to be run by Red Hat podman developers who need # to debug problems specifically related to Cirrus-CI automated testing. # It requires that you have been granted prior access to create VMs in # google-cloud. For non-Red Hat contributors, VMs are available as-needed, # with supervision upon request. set -e SCRIPT_FILEPATH=$(realpath "${BASH_SOURCE[0]}") SCRIPT_DIRPATH=$(dirname "$SCRIPT_FILEPATH") REPO_DIRPATH=$(realpath "$SCRIPT_DIRPATH/../") # Help detect what get_ci_vm container called this script GET_CI_VM="${GET_CI_VM:-0}" in_get_ci_vm() { if ((GET_CI_VM==0)); then echo "Error: $1 is not intended for use in this context" exit 2 fi } # get_ci_vm APIv1 container entrypoint calls into this script # to obtain required repo. specific configuration options. if [[ "$1" == "--config" ]]; then in_get_ci_vm "$1" # handles GET_CI_VM==0 case case "$GET_CI_VM" in 1) cat < /dev/stderr ./contrib/cirrus/setup.sh else # Pass this repo and CLI args into container for VM creation/management mkdir -p $HOME/.config/gcloud/ssh mkdir -p $HOME/.aws podman run -it --rm \ --tz=local \ -e NAME="$USER" \ -e SRCDIR=/src \ -e GCLOUD_ZONE="$GCLOUD_ZONE" \ -e A_DEBUG="${A_DEBUG:-0}" \ -v $REPO_DIRPATH:/src:O \ -v $HOME/.config/gcloud:/root/.config/gcloud:z \ -v $HOME/.config/gcloud/ssh:/root/.ssh:z \ -v $HOME/.aws:/root/.aws:z \ quay.io/libpod/get_ci_vm:latest "$@" fi containers-netavark-68510f4/perf-netavark.sh000077500000000000000000000006421475240420400210570ustar00rootroot00000000000000#!/bin/bash # Netavark binary NETAVARK=${NETAVARK:-./bin/netavark} trap cleanup EXIT function cleanup() { kill -9 $netnspid rm -rf $TMP_CONFIG } TMP_CONFIG=$(mktemp -d) unshare -n sleep 100 & netnspid=$! # first arg is the fw driver if [ -n "$1" ]; then export NETAVARK_FW="$1" fi unshare -n perf stat $NETAVARK -f ./test/testfiles/simplebridge.json --config $TMP_CONFIG setup /proc/$netnspid/ns/net containers-netavark-68510f4/plugin-API.md000066400000000000000000000136071475240420400202070ustar00rootroot00000000000000# Description of the netavark plugin API A netavark plugin is a external binary which must implement a specific set of subcommands that will be called by podman and netavark. - `create`: creates a network config - `setup`: setup the network configuration - `teardown`: tear down the network configuration - `info`: show info about this plugin ## Create subcommand The create subcommand creates a new network config for podman. The subcommand will receive the JSON network config via STDIN. Podman will populate the network name and ID before calling the plugin. The name and ID cannot be changed by the plugin. The driver name must also not be changed. All other config fields can be changed in the plugin. Other fields such as subnet and options will also be populated by podman when these options are set on the podman network create command, i.e. `--subnet` and `--option`. The plugin validates the given values and errors out for invalid values. On success the plugin should print the generated config as JSON to STDOUT. Example JSON input and output format: ``` { "name": "example1", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "mydriver", "network_interface": "enp1", "subnets": [ { "subnet": "10.0.0.0/16", "gateway": "10.0.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" }, "options": { "custom": "opt" } } ``` ## Setup subcommand The setup subcommand sets-up the network configuration. This command is called when a container is started or network connect is used, assuming the container uses a network which was created by the plugin, see the create command above. On STDIN it receives a JSON config which contains the network config and container options. ON STDOUT the plugin must return a JSON status block. This contains information about the created interface, the assigned ip and mac address. This information will be visible in the podman inspect output. Also this command accepts one argument which is the path to the container network namespace. Example JSON input: ``` { "container_id": "752947ff91f961eb3cb47ffe9315016979f3ffbec09e4d96a4fae3fb03391697", "container_name": "testctr", "port_mappings": [ { "container_port": 80, "host_ip": "127.0.0.1", "host_port": 8080, "protocol": "tcp", "range": 1 } ], "network": { "dns_enabled": false, "driver": "bridge", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "options": null, "ipam_options": { "driver": "host-local" }, "subnets": [ { "gateway": "10.88.0.1", "lease_range": null, "subnet": "10.88.0.0/16" } ], "network_dns_servers": null }, "network_options": { "aliases": [ "752947ff91f9" ], "interface_name": "eth0", "static_ips": [ "10.88.0.50" ], "static_mac": "aa:bb:cc:dd:aa:00" } } ``` Example JSON output: ``` { "dns_search_domains": [], "dns_server_ips": [], "interfaces": { "eth0": { "mac_address": "aa:bb:cc:dd:aa:00", "subnets": [ { "gateway": "10.88.0.1", "ipnet": "10.88.0.50/16" } ] } } } ``` ## Teardown subcommand The teardown command is basically the reverse of the setup command. It should revert what the plugin did in setup. It accepts the same input as setup but it should not return anything on success. ## Info subcommand Used to output information about this plugin. It must contain the version of your plugin and the API version. Extra fields can be added. The API version must be set to `1.0.0` at the moment, it is not used the moment but could be used in the future to allow for backwards compatibility in case the plugin types change. ``` { "version": "0.1.0", "api_version": "1.0.0" } ``` ## Error handling If the plugin encounters an error it should return a special json message with the following format: ``` {"error": "message"} ``` where message should be replace with your actual error message. This message will be returned by netavark and will be visible to podman users. ## Rust types Rust types can be found in [./src/network/types.rs](./src/network/types.rs), see the documentation [here](https://docs.rs/netavark/latest/netavark/network/types). Fields that are wrapped by an `Option` can be omitted from the json, otherwise they must be set to allow proper deserialization. ## Rust plugin interface There is a simple ready to use interface for writing your plugin in rust, see [./src/plugin.rs](./src/plugin.rs) ```rust use netavark::{ network::types, plugin::{Info, Plugin, PluginExec, API_VERSION}, }; fn main() { // change the version to the version of your plugin let info = Info::new("0.1.0".to_owned(), API_VERSION.to_owned(), None); PluginExec::new(Exec {}, info).exec(); } struct Exec {} impl Plugin for Exec { fn create( &self, network: types::Network, ) -> Result> { // your logic here } fn setup( &self, netns: String, opts: types::NetworkPluginExec, ) -> Result> { // your logic here } fn teardown( &self, netns: String, opts: types::NetworkPluginExec, ) -> Result<(), Box> { // your logic here } } ``` Also see the examples in [./examples](./examples/). containers-netavark-68510f4/rpm/000077500000000000000000000000001475240420400165475ustar00rootroot00000000000000containers-netavark-68510f4/rpm/netavark.spec000066400000000000000000000100441475240420400212350ustar00rootroot00000000000000# Building from fedora dependencies not possible # Latest upstream rtnetlink frequently required # sha2, zbus, zvariant are currently out of date %global with_debug 1 %if 0%{?with_debug} %global _find_debuginfo_dwz_opts %{nil} %global _dwz_low_mem_die_limit 0 %else %global debug_package %{nil} %endif # Minimum X.Y dep for aardvark-dns %define major_minor %((v=%{version}; echo ${v%.*})) # Set default firewall to nftables on CentOS Stream 10+, RHEL 10+, Fedora 41+ # and default to iptables on all other environments # The `rhel` macro is defined on CentOS Stream, RHEL as well as Fedora ELN. %if (%{defined rhel} && 0%{?rhel} >= 10) || (%{defined fedora} && 0%{?fedora} >= 41) %define default_fw nftables %else %define default_fw iptables %endif Name: netavark # Set a different Epoch for copr builds %if %{defined copr_username} Epoch: 102 %else Epoch: 2 %endif Version: 0 Release: %autorelease # The `AND` needs to be uppercase in the License for SPDX compatibility License: Apache-2.0 AND BSD-3-Clause AND MIT %if %{defined golang_arches_future} ExclusiveArch: %{golang_arches_future} %else ExclusiveArch: aarch64 ppc64le s390x x86_64 %endif Summary: OCI network stack URL: https://github.com/containers/%{name} # Tarballs fetched from upstream's release page Source0: %{url}/archive/v%{version}.tar.gz Source1: %{url}/releases/download/v%{version}/%{name}-v%{version}-vendor.tar.gz BuildRequires: cargo BuildRequires: %{_bindir}/go-md2man # aardvark-dns and %%{name} are usually released in sync Requires: aardvark-dns >= %{epoch}:%{major_minor} Provides: container-network-stack = 2 %if "%{default_fw}" == "nftables" Requires: nftables %else Requires: iptables %endif BuildRequires: make BuildRequires: protobuf-c BuildRequires: protobuf-compiler %if %{defined rhel} # rust-toolset requires the `local` repo enabled on non-koji ELN build environments BuildRequires: rust-toolset %else BuildRequires: rust-packaging BuildRequires: rust-srpm-macros %endif BuildRequires: git-core BuildRequires: systemd BuildRequires: systemd-devel %description %{summary} Netavark is a rust based network stack for containers. It is being designed to work with Podman but is also applicable for other OCI container management applications. Netavark is a tool for configuring networking for Linux containers. Its features include: * Configuration of container networks via JSON configuration file * Creation and management of required network interfaces, including MACVLAN networks * All required firewall configuration to perform NAT and port forwarding as required for containers * Support for iptables and firewalld at present, with support for nftables planned in a future release * Support for rootless containers * Support for IPv4 and IPv6 * Support for container DNS resolution via aardvark-dns. %prep %autosetup -Sgit %{name}-%{version} # Following steps are only required on environments like koji which have no # network access and thus depend on the vendored tarball. Copr pulls # dependencies directly from the network. %if !%{defined copr_username} tar fx %{SOURCE1} %if 0%{?fedora} || 0%{?rhel} >= 10 %cargo_prep -v vendor %else %cargo_prep -V 1 %endif %endif %build NETAVARK_DEFAULT_FW=%{default_fw} %{__make} CARGO="%{__cargo}" build %if (0%{?fedora} || 0%{?rhel} >= 10) && !%{defined copr_username} %cargo_license_summary %{cargo_license} > LICENSE.dependencies %cargo_vendor_manifest %endif cd docs %{__make} %install %{__make} DESTDIR=%{buildroot} PREFIX=%{_prefix} install %preun %systemd_preun %{name}-dhcp-proxy.service %systemd_preun %{name}-firewalld-reload.service %postun %systemd_postun %{name}-dhcp-proxy.service %systemd_postun %{name}-firewalld-reload.service %files %license LICENSE %if (0%{?fedora} || 0%{?rhel} >= 10) && !%{defined copr_username} %license LICENSE.dependencies %license cargo-vendor.txt %endif %dir %{_libexecdir}/podman %{_libexecdir}/podman/%{name}* %{_mandir}/man1/%{name}.1* %{_mandir}/man7/%{name}-firewalld.7* %{_unitdir}/%{name}-dhcp-proxy.service %{_unitdir}/%{name}-dhcp-proxy.socket %{_unitdir}/%{name}-firewalld-reload.service %changelog %autochangelog containers-netavark-68510f4/src/000077500000000000000000000000001475240420400165405ustar00rootroot00000000000000containers-netavark-68510f4/src/commands/000077500000000000000000000000001475240420400203415ustar00rootroot00000000000000containers-netavark-68510f4/src/commands/dhcp_proxy.rs000066400000000000000000000361171475240420400230760ustar00rootroot00000000000000#![cfg_attr(not(unix), allow(unused_imports))] use crate::dhcp_proxy::cache::{Clear, LeaseCache}; use crate::dhcp_proxy::dhcp_service::{process_client_stream, DhcpV4Service}; use crate::dhcp_proxy::ip; use crate::dhcp_proxy::lib::g_rpc::netavark_proxy_server::{NetavarkProxy, NetavarkProxyServer}; use crate::dhcp_proxy::lib::g_rpc::{ Empty, Lease as NetavarkLease, NetworkConfig, OperationResponse, }; use crate::dhcp_proxy::proxy_conf::{ get_cache_fqname, get_proxy_sock_fqname, DEFAULT_INACTIVITY_TIMEOUT, DEFAULT_TIMEOUT, }; use crate::error::{NetavarkError, NetavarkResult}; use crate::network::core_utils; use clap::Parser; use log::{debug, error, warn}; use tokio::task::AbortHandle; use std::collections::HashMap; use std::fs::File; use std::io::Write; use std::os::unix::io::FromRawFd; use std::os::unix::net::UnixListener as stdUnixListener; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::{env, fs}; #[cfg(unix)] use tokio::net::UnixListener; #[cfg(unix)] use tokio::signal::unix::{signal, SignalKind}; use tokio::sync::mpsc::Sender; use tokio::sync::oneshot::error::TryRecvError; use tokio::sync::{mpsc, oneshot}; use tokio::time::{timeout, Duration}; #[cfg(unix)] use tokio_stream::wrappers::UnixListenerStream; use tonic::{ transport::Server, Code, Code::Internal, Code::InvalidArgument, Request, Response, Status, }; #[derive(Debug)] /// This is the tonic netavark proxy service that is required to impl the Netavark Proxy trait which /// includes the gRPC methods defined in proto/proxy.proto. We can store a atomically referenced counted /// mutex cache in the structure tuple. /// /// The cache needs to be **safely mutable across multiple threads**. We need to share the lease cache /// across multiple threads for 2 reasons /// 1. Each tonic request is spawned in its own new thread. /// 2. A new thread must be spawned in any request that uses mozim, such as get_lease. This is because /// tonic creates its own runtime for each request and mozim trys to make its own runtime inside of /// a runtime. /// struct NetavarkProxyService { // cache is the lease hashmap cache: Arc>>, // the timeout for the dora operation dora_timeout: u32, // channel send-side for resetting the inactivity timeout timeout_sender: Arc>>, // All dhcp poll will be spawned on a new task, keep track of it so // we can remove it on teardown. The key is the container mac. task_map: Arc>>, } impl NetavarkProxyService { fn reset_inactivity_timeout(&self) { let sender = self.timeout_sender.clone(); let locked_sender = match sender.lock() { Ok(v) => v, Err(e) => { log::error!("{}", e.to_string()); return; } }; match locked_sender.try_send(1) { Ok(..) => {} Err(e) => log::error!("{}", e.to_string()), } } } // gRPC request and response methods #[tonic::async_trait] impl NetavarkProxy for NetavarkProxyService { /// gRPC connection to get a lease async fn setup( &self, request: Request, ) -> Result, Status> { debug!("Request from client {:?}", request.remote_addr()); // notify server of activity self.reset_inactivity_timeout(); let cache = self.cache.clone(); let timeout = self.dora_timeout; let task_map = self.task_map.clone(); // setup client side streaming let network_config = request.into_inner(); // _tx will be dropped when the request is dropped, this will trigger rx, which means the // client disconnected let (_tx, mut rx) = oneshot::channel::<()>(); let lease = tokio::task::spawn(async move { // Check if the connection has been dropped before attempting to get a lease if rx.try_recv() == Err(TryRecvError::Closed) { log::debug!("Request dropped, aborting DORA"); return Err(Status::new(Code::Aborted, "client disconnected")); } let get_lease = process_setup(network_config, timeout, cache, task_map); // watch the client and the lease, which ever finishes first return let get_lease: NetavarkLease = tokio::select! { _ = &mut rx => { // we never send to tx, so this completing means that the other end, tx, was dropped! log::debug!("Request dropped, aborting DORA"); return Err(Status::new(Code::Aborted, "client disconnected")) } lease = get_lease => { Ok::(lease?) } }?; // check after lease was found that the client is still there if rx.try_recv() == Err(TryRecvError::Closed) { log::debug!("Request dropped, aborting DORA"); return Err(Status::new(Code::Aborted, "client disconnected")); } Ok(get_lease) }) .await; return match lease { Ok(Ok(lease)) => Ok(Response::new(lease)), Ok(Err(status)) => Err(status), Err(e) => Err(Status::new(Code::Unknown, e.to_string())), }; } /// When a container is shut down this method should be called. It will clear the lease information /// from the caching system. async fn teardown( &self, request: Request, ) -> Result, Status> { // notify server of activity self.reset_inactivity_timeout(); let nc = request.into_inner(); let cache = self.cache.clone(); let tasks = self.task_map.clone(); let task = tasks .lock() .expect("lock tasks") .remove(&nc.container_mac_addr); if let Some(handle) = task { handle.abort(); } // Remove the client from the cache dir let lease = cache .lock() .expect("Could not unlock cache. A thread was poisoned") .remove_lease(&nc.container_mac_addr) .map_err(|e| Status::internal(e.to_string()))?; Ok(Response::new(lease)) } /// On teardown of the proxy the cache will be cleared gracefully. async fn clean(&self, request: Request) -> Result, Status> { debug!("Request from client: {:?}", request.remote_addr()); self.cache .clone() .lock() .expect("Could not unlock cache. A thread was poisoned") .teardown()?; Ok(Response::new(OperationResponse { success: true })) } } #[derive(Parser, Debug)] #[clap(version = env ! ("CARGO_PKG_VERSION"))] pub struct Opts { /// location to store backup files #[clap(short, long)] dir: Option, /// alternative uds location #[clap(short, long)] uds: Option, /// optional time in seconds to time out after looking for a lease #[clap(short, long)] timeout: Option, /// activity timeout #[clap(short, long)] activity_timeout: Option, } /// Handle SIGINT signal. /// /// Will wait until process receives a SIGINT/ ctrl+c signal and then clean up and shut down async fn handle_signal(uds_path: PathBuf) { tokio::spawn(async move { // Handle signal hooks with expect, it is important these are setup so data is not corrupted let mut sigterm = signal(SignalKind::terminate()).expect("Could not set up SIGTERM hook"); let mut sigint = signal(SignalKind::interrupt()).expect("Could not set up SIGINT hook"); // Wait for either a SIGINT or a SIGTERM to clean up tokio::select! { _ = sigterm.recv() => { warn!("Received SIGTERM, cleaning up and exiting"); } _ = sigint.recv() => { warn!("Received SIGINT, cleaning up and exiting"); } } if let Err(e) = fs::remove_file(uds_path) { error!("Could not close uds socket: {}", e); } std::process::exit(0x0100); }); } #[tokio::main] pub async fn serve(opts: Opts) -> NetavarkResult<()> { let optional_run_dir = opts.dir.as_deref(); let dora_timeout = opts.timeout.unwrap_or(DEFAULT_TIMEOUT); let inactivity_timeout = Duration::from_secs(opts.activity_timeout.unwrap_or(DEFAULT_INACTIVITY_TIMEOUT)); let uds_path = get_proxy_sock_fqname(optional_run_dir); debug!("socket path: {}", &uds_path.display()); let mut is_systemd_activated = false; // check if the UDS is a systemd socket activated service. if it is, // then systemd hands this over to us on FD 3. let uds: UnixListener = match env::var("LISTEN_FDS") { Ok(effds) => { if effds != "1" { return Err(NetavarkError::msg("Received more than one FD from systemd")); } is_systemd_activated = true; let systemd_socket = unsafe { stdUnixListener::from_raw_fd(3) }; systemd_socket.set_nonblocking(true)?; UnixListener::from_std(systemd_socket)? } // Use the standard socket approach Err(..) => { // Create a new uds socket path match Path::new(&uds_path).parent() { None => { return Err(NetavarkError::msg("Could not get parent from uds path")); } Some(f) => tokio::fs::create_dir_all(f).await?, } // Watch for signals after the uds path has been created, so that the socket can be closed. handle_signal(uds_path.clone()).await; UnixListener::bind(&uds_path)? } }; let uds_stream = UnixListenerStream::new(uds); // Create the cache file let fq_cache_path = get_cache_fqname(optional_run_dir); let file = match File::create(&fq_cache_path) { Ok(file) => { debug!("Successfully created leases file: {:?}", fq_cache_path); file } Err(e) => { return Err(NetavarkError::msg(format!( "Exiting. Could not create lease cache file: {e}", ))); } }; let cache = match LeaseCache::new(file) { Ok(c) => Arc::new(Mutex::new(c)), Err(e) => { return Err(NetavarkError::msg(format!( "Could not setup the cache: {e}" ))); } }; // Create send and receive channels for activity timeout. If anything is // sent by the tx side, the inactivity timeout is reset let (activity_timeout_tx, activity_timeout_rx) = mpsc::channel(5); let netavark_proxy_service = NetavarkProxyService { cache: cache.clone(), dora_timeout, timeout_sender: Arc::new(Mutex::new(activity_timeout_tx.clone())), task_map: Arc::new(Mutex::new(HashMap::new())), }; let server = Server::builder() .add_service(NetavarkProxyServer::new(netavark_proxy_service)) .serve_with_incoming(uds_stream); tokio::pin!(server); tokio::select! { // a timeout duration of 0 means NEVER _ = handle_wakeup(activity_timeout_rx, inactivity_timeout, cache.clone()), if inactivity_timeout.as_secs() > 0 => {}, _ = &mut server => {}, }; // Make sure to only remove the socket path when we do not run socket activated, // otherwise we delete the socket systemd is using which causes all new connections to fail. if !is_systemd_activated { fs::remove_file(uds_path)?; } Ok(()) } /// manages the timeout lifecycle for the proxy server based on a defined timeout. /// /// # Arguments /// /// * `rx`: receive side of channel /// * `timeout_duration`: time duration in seconds /// /// returns: () /// /// # Examples /// /// ``` /// /// ``` async fn handle_wakeup( mut rx: mpsc::Receiver, timeout_duration: Duration, current_cache: Arc>>, ) { loop { match timeout(timeout_duration, rx.recv()).await { Ok(Some(_)) => { debug!("timeout timer reset") } Ok(None) => { println!("timeout channel closed"); break; } Err(_) => { // only 'exit' if the timeout is met AND there are no leases // if we do not exit, the activity_timeout is reset if is_catch_empty(current_cache.clone()) { println!( "timeout met: exiting after {} secs of inactivity", timeout_duration.as_secs() ); break; } } } } } /// get_cache_len returns the number of leases in the hashmap in memory /// /// # Arguments /// /// * `current_cache`: /// /// returns: usize /// /// # Examples /// /// ``` /// /// ``` fn is_catch_empty(current_cache: Arc>>) -> bool { match current_cache.lock() { Ok(v) => { debug!("cache_len is {}", v.len().to_string()); v.is_empty() } Err(e) => { log::error!("{}", e.to_string()); false } } } /// Process network config into a lease and setup the ip /// /// # Arguments /// /// * `network_config`: Network config /// * `timeout`: dora timeout /// * `cache`: lease cache /// /// returns: Result async fn process_setup( network_config: NetworkConfig, timeout: u32, cache: Arc>>, tasks: Arc>>, ) -> Result { let container_network_interface = network_config.container_iface.clone(); let ns_path = network_config.ns_path.clone(); // test if mac is valid core_utils::CoreUtils::decode_address_from_hex(&network_config.container_mac_addr) .map_err(|e| Status::new(InvalidArgument, format!("{e}")))?; let mac = &network_config.container_mac_addr.clone(); let nv_lease = match network_config.version { //V4 0 => { let mut service = DhcpV4Service::new(network_config, timeout)?; let lease = service.get_lease().await?; let task = tokio::spawn(process_client_stream(service)); tasks .lock() .expect("lock tasks") .insert(mac.to_string(), task.abort_handle()); lease } //V6 TODO implement DHCPv6 1 => { return Err(Status::new(InvalidArgument, "ipv6 not yet supported")); } _ => { return Err(Status::new(InvalidArgument, "invalid protocol version")); } }; if let Err(e) = cache .lock() .expect("Could not unlock cache. A thread was poisoned") .add_lease(mac, &nv_lease) { return Err(Status::new( Internal, format!("Error caching the lease: {e}"), )); } ip::setup(&nv_lease, &container_network_interface, &ns_path)?; Ok(nv_lease) } containers-netavark-68510f4/src/commands/firewalld_reload.rs000066400000000000000000000045411475240420400242120ustar00rootroot00000000000000use std::{ ffi::{OsStr, OsString}, path::Path, }; use zbus::{blocking::Connection, proxy, CacheProperties}; use crate::{ error::{ErrorWrap, NetavarkResult}, firewall::{get_supported_firewall_driver, state::read_fw_config}, network::constants, }; #[proxy( interface = "org.fedoraproject.FirewallD1", default_service = "org.fedoraproject.FirewallD1", default_path = "/org/fedoraproject/FirewallD1" )] trait FirewallDDbus {} const SIGNAL_NAME: &str = "Reloaded"; pub fn listen(config_dir: Option) -> NetavarkResult<()> { let config_dir = Path::new( config_dir .as_deref() .unwrap_or(OsStr::new(constants::DEFAULT_CONFIG_DIR)), ); log::debug!("looking for firewall configs in {:?}", config_dir); let conn = Connection::system()?; let proxy = FirewallDDbusProxyBlocking::builder(&conn) .cache_properties(CacheProperties::No) .build()?; // Setup fw rules on start because we are started after firewalld // this means at the time firewalld stated the fw rules were flushed // and we need to add them back. // It is important to keep things like "systemctl restart firewalld" working. reload_rules(config_dir); // This loops forever until the process is killed or there is some dbus error. for _ in proxy.0.receive_signal(SIGNAL_NAME)? { log::debug!("got firewalld {} signal", SIGNAL_NAME); reload_rules(config_dir); } Ok(()) } fn reload_rules(config_dir: &Path) { if let Err(e) = reload_rules_inner(config_dir) { log::error!("failed to reload firewall rules: {e}"); } } fn reload_rules_inner(config_dir: &Path) -> NetavarkResult<()> { let conf = read_fw_config(config_dir).wrap("read firewall config")?; // If we got no conf there are no containers so nothing to do. if let Some(conf) = conf { let fw_driver = get_supported_firewall_driver(Some(conf.driver))?; let dbus_conn = match zbus::blocking::Connection::system() { Ok(c) => Some(c), Err(_) => None, }; for net in conf.net_confs { fw_driver.setup_network(net, &dbus_conn)?; } for port in &conf.port_confs { fw_driver.setup_port_forward(port.into(), &dbus_conn)?; } log::info!("Successfully reloaded firewall rules"); } Ok(()) } containers-netavark-68510f4/src/commands/mod.rs000066400000000000000000000006631475240420400214730ustar00rootroot00000000000000use std::ffi::OsString; use crate::error::{NetavarkError, NetavarkResult}; pub mod dhcp_proxy; pub mod firewalld_reload; pub mod setup; pub mod teardown; pub mod update; pub mod version; fn get_config_dir(dir: Option, cmd: &str) -> NetavarkResult { dir.ok_or_else(|| { NetavarkError::msg(format!( "--config not specified but required for netavark {}", cmd )) }) } containers-netavark-68510f4/src/commands/setup.rs000066400000000000000000000151021475240420400220460ustar00rootroot00000000000000//! Configures the given network namespace with provided specs use crate::commands::get_config_dir; use crate::dns::aardvark::Aardvark; use crate::error::{NetavarkError, NetavarkResult}; use crate::firewall; use crate::network::driver::{get_network_driver, DriverInfo, NetworkDriver}; use crate::network::netlink::{self, LinkID}; use crate::network::{self}; use crate::network::{core_utils, types}; use clap::builder::NonEmptyStringValueParser; use clap::Parser; use log::{debug, error, info}; use std::collections::HashMap; use std::ffi::OsString; use std::fs::{self}; use std::os::fd::AsFd; use std::path::Path; #[derive(Parser, Debug)] pub struct Setup { /// Network namespace path #[clap(required = true, value_parser = NonEmptyStringValueParser::new())] network_namespace_path: String, } impl Setup { /// The setup command configures the given network namespace with the given configuration, creating any interfaces and firewall rules necessary. pub fn new(network_namespace_path: String) -> Self { Self { network_namespace_path, } } pub fn exec( &self, input_file: Option, config_dir: Option, firewall_driver: Option, aardvark_bin: OsString, plugin_directories: Option>, rootless: bool, ) -> NetavarkResult<()> { match network::validation::ns_checks(&self.network_namespace_path) { Ok(_) => (), Err(e) => { return Err(NetavarkError::wrap("invalid namespace path", e)); } } debug!("Setting up..."); let network_options = network::types::NetworkOptions::load(input_file)?; let firewall_driver = firewall::get_supported_firewall_driver(firewall_driver)?; let mut response: HashMap = HashMap::new(); let dns_port = core_utils::get_netavark_dns_port()?; let (mut hostns, mut netns) = core_utils::open_netlink_sockets(&self.network_namespace_path)?; // setup loopback, it should be safe to assume that 1 is the loopback index netns.netlink.set_up(LinkID::ID(1))?; let config_dir = get_config_dir(config_dir, "setup")?; let mut drivers = Vec::with_capacity(network_options.network_info.len()); // Perform per-network setup for (net_name, network) in network_options.network_info.iter() { let per_network_opts = network_options.networks.get(net_name).ok_or_else(|| { NetavarkError::Message(format!("network options for network {net_name} not found")) })?; let mut driver = get_network_driver( DriverInfo { firewall: firewall_driver.as_ref(), container_id: &network_options.container_id, container_name: &network_options.container_name, container_hostname: &network_options.container_hostname, container_dns_servers: &network_options.dns_servers, netns_host: hostns.file.as_fd(), netns_container: netns.file.as_fd(), netns_path: &self.network_namespace_path, network, per_network_opts, port_mappings: &network_options.port_mappings, dns_port, config_dir: Path::new(&config_dir), rootless, }, &plugin_directories, )?; // validate before we do anything driver.validate()?; drivers.push(driver); } let mut aardvark_entries = Vec::new(); // Only now after we validated all drivers we setup each. // If there is an error we have to tear down all previous drivers. for (i, driver) in drivers.iter().enumerate() { let (status, aardvark_entry) = match driver.setup((&mut hostns.netlink, &mut netns.netlink)) { Ok((s, a)) => (s, a), Err(e) => { // now teardown the already setup drivers teardown_drivers( drivers.iter().take(i), &mut hostns.netlink, &mut netns.netlink, ); return Err(e); } }; let _ = response.insert(driver.network_name(), status); if let Some(a) = aardvark_entry { aardvark_entries.push(a); } } if !aardvark_entries.is_empty() { if Path::new(&aardvark_bin).exists() { let path = Path::new(&config_dir).join("aardvark-dns"); match fs::create_dir(path.as_path()) { Ok(_) => {} // ignore error when path already exists Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => {} Err(e) => { teardown_drivers(drivers.iter(), &mut hostns.netlink, &mut netns.netlink); return Err(NetavarkError::wrap( format!("failed to create aardvark-dns directory {}", path.display()), NetavarkError::Io(e), )); } } let aardvark_interface = Aardvark::new(path, rootless, aardvark_bin, dns_port); if let Err(er) = aardvark_interface.commit_netavark_entries(aardvark_entries) { teardown_drivers(drivers.iter(), &mut hostns.netlink, &mut netns.netlink); return Err(NetavarkError::wrap("error while applying dns entries", er)); } } else { info!( "dns disabled because aardvark-dns path {:?} does not exists", &aardvark_bin ); } } debug!("{:#?}", response); let response_json = serde_json::to_string(&response)?; println!("{response_json}"); debug!("Setup complete"); Ok(()) } } fn teardown_drivers<'a, I>(drivers: I, host: &mut netlink::Socket, netns: &mut netlink::Socket) where I: Iterator>, { for driver in drivers { if let Err(e) = driver.teardown((host, netns)) { error!( "failed to cleanup network {} after setup failed: {}", driver.network_name(), e ); }; } } containers-netavark-68510f4/src/commands/teardown.rs000066400000000000000000000115561475240420400225420ustar00rootroot00000000000000use crate::commands::get_config_dir; use crate::dns::aardvark::{Aardvark, AardvarkEntry}; use crate::error::{NetavarkError, NetavarkErrorList, NetavarkResult}; use crate::network::constants::DRIVER_BRIDGE; use crate::network::core_utils; use crate::network::driver::{get_network_driver, DriverInfo}; use crate::{firewall, network}; use clap::builder::NonEmptyStringValueParser; use clap::Parser; use log::debug; use std::ffi::OsString; use std::os::fd::AsFd; use std::path::Path; #[derive(Parser, Debug)] pub struct Teardown { /// Network namespace path #[clap(required = true, value_parser = NonEmptyStringValueParser::new())] network_namespace_path: String, } impl Teardown { /// 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). pub fn new(network_namespace_path: String) -> Self { Self { network_namespace_path, } } pub fn exec( &self, input_file: Option, config_dir: Option, firewall_driver: Option, aardvark_bin: OsString, plugin_directories: Option>, rootless: bool, ) -> NetavarkResult<()> { debug!("Tearing down.."); let network_options = network::types::NetworkOptions::load(input_file)?; let mut error_list = NetavarkErrorList::new(); let dns_port = core_utils::get_netavark_dns_port()?; let config_dir = get_config_dir(config_dir, "teardown")?; let mut aardvark_entries = Vec::new(); for (key, network) in &network_options.network_info { if network.dns_enabled && network.driver == DRIVER_BRIDGE { aardvark_entries.push(AardvarkEntry { network_name: key, network_gateways: Vec::new(), network_dns_servers: &None, container_id: &network_options.container_id, container_ips_v4: Vec::new(), container_ips_v6: Vec::new(), container_names: Vec::new(), container_dns_servers: &None, is_internal: network.internal, }); } } if !aardvark_entries.is_empty() { // stop dns server first before netavark clears the interface let path = Path::new(&config_dir).join("aardvark-dns"); let aardvark_interface = Aardvark::new(path, rootless, aardvark_bin, dns_port); if let Err(err) = aardvark_interface.delete_from_netavark_entries(&aardvark_entries) { error_list.push(NetavarkError::wrap("remove aardvark entries", err)); } } let firewall_driver = firewall::get_supported_firewall_driver(firewall_driver)?; let (mut hostns, mut netns) = core_utils::open_netlink_sockets(&self.network_namespace_path)?; for (net_name, network) in network_options.network_info.iter() { let per_network_opts = match network_options.networks.get(net_name) { Some(opts) => opts, None => { error_list.push(NetavarkError::Message(format!( "network options for network {net_name} not found" ))); continue; } }; let driver = match get_network_driver( DriverInfo { firewall: firewall_driver.as_ref(), container_id: &network_options.container_id, container_name: &network_options.container_name, container_hostname: &network_options.container_hostname, container_dns_servers: &network_options.dns_servers, netns_host: hostns.file.as_fd(), netns_container: netns.file.as_fd(), netns_path: &self.network_namespace_path, network, per_network_opts, port_mappings: &network_options.port_mappings, dns_port, config_dir: Path::new(&config_dir), rootless, }, &plugin_directories, ) { Ok(driver) => driver, Err(err) => { error_list.push(err); continue; } }; match driver.teardown((&mut hostns.netlink, &mut netns.netlink)) { Ok(_) => {} Err(err) => { error_list.push(err); continue; } }; } if !error_list.is_empty() { return Err(NetavarkError::List(error_list)); } debug!("Teardown complete"); Ok(()) } } containers-netavark-68510f4/src/commands/update.rs000066400000000000000000000037031475240420400221740ustar00rootroot00000000000000use crate::commands::get_config_dir; use crate::dns::aardvark::Aardvark; use crate::error::{NetavarkError, NetavarkResult}; use crate::network::core_utils; use clap::builder::NonEmptyStringValueParser; use clap::Parser; use log::debug; use std::ffi::OsString; use std::path::Path; #[derive(Parser, Debug)] pub struct Update { /// Network name to update #[clap(required = true, value_parser = NonEmptyStringValueParser::new())] network_name: String, /// DNS Servers to update for the network #[clap(long, required = true)] network_dns_servers: Vec, } impl Update { /// Updates network dns servers for an already configured network pub fn new(network_name: String, network_dns_servers: Vec) -> Self { Self { network_name, network_dns_servers, } } pub fn exec( &mut self, config_dir: Option, aardvark_bin: OsString, rootless: bool, ) -> NetavarkResult<()> { let dns_port = core_utils::get_netavark_dns_port()?; let config_dir = get_config_dir(config_dir, "update")?; if Path::new(&aardvark_bin).exists() { let path = Path::new(&config_dir).join("aardvark-dns"); let aardvark_interface = Aardvark::new(path, rootless, aardvark_bin, dns_port); // if empty network_dns_servers are passed, pass empty array instead of `[""]` if self.network_dns_servers.len() == 1 && self.network_dns_servers[0].is_empty() { self.network_dns_servers = Vec::new(); } if let Err(err) = aardvark_interface .modify_network_dns_servers(&self.network_name, &self.network_dns_servers) { return Err(NetavarkError::wrap( "unable to modify network dns servers", err, )); } } debug!("Network update complete"); Ok(()) } } containers-netavark-68510f4/src/commands/version.rs000066400000000000000000000013571475240420400224020ustar00rootroot00000000000000use crate::error::NetavarkResult; use clap::Parser; use serde::Serialize; #[derive(Parser, Debug)] pub struct Version {} #[derive(Debug, Serialize)] struct Info { version: &'static str, commit: &'static str, build_time: &'static str, target: &'static str, default_fw_driver: &'static str, } impl Version { pub fn exec(&self) -> NetavarkResult<()> { let info = Info { version: env!("CARGO_PKG_VERSION"), commit: env!("GIT_COMMIT"), build_time: env!("BUILD_TIMESTAMP"), target: env!("BUILD_TARGET"), default_fw_driver: env!("DEFAULT_FW"), }; let out = serde_json::to_string_pretty(&info)?; println!("{out}"); Ok(()) } } containers-netavark-68510f4/src/dhcp_proxy/000077500000000000000000000000001475240420400207175ustar00rootroot00000000000000containers-netavark-68510f4/src/dhcp_proxy/cache.rs000066400000000000000000000377721475240420400223500ustar00rootroot00000000000000use crate::dhcp_proxy::lib::g_rpc::{Lease as NetavarkLease, Lease}; use log::{debug, error}; use std::collections::HashMap; use std::fs::File; use std::io; use std::io::{Cursor, Write}; #[derive(Debug)] #[allow(dead_code)] pub struct ClearError { msg: String, } /// The Writer on the cache must be clearable so that any new changes can be overwritten pub trait Clear { fn clear(&mut self) -> Result<(), ClearError>; } impl Clear for Cursor> { fn clear(&mut self) -> Result<(), ClearError> { self.set_position(0); self.get_mut().clear(); Ok(()) } } impl Clear for File { fn clear(&mut self) -> Result<(), ClearError> { match self.set_len(0) { Ok(_) => Ok(()), Err(e) => Err(ClearError { msg: e.to_string() }), } } } /// The leasing cache holds a in memory record of the leases, and a on file version #[derive(Debug)] pub struct LeaseCache { mem: HashMap>, writer: W, } impl LeaseCache { /// /// /// # Arguments /// /// * `writer`: any type that can has the Write and Clear trait implemented. In production this /// is a file. In development/testing this is a Cursor of bytes /// /// returns: Result, Error> /// pub fn new(writer: W) -> Result, io::Error> { Ok(LeaseCache { mem: HashMap::new(), writer, }) } /// Add a new lease to a memory and file system cache /// /// # Arguments /// /// * `mac_addr`: Mac address of the container /// * `lease`: New lease that should be saved in the cache /// /// returns: Result<(), Error> /// pub fn add_lease(&mut self, mac_addr: &str, lease: &NetavarkLease) -> Result<(), io::Error> { debug!("add lease: {:?}", mac_addr); // Update cache memory with new lease let cache = &mut self.mem; cache.insert(mac_addr.to_string(), vec![lease.clone()]); // write updated memory cache to the file system self.save_memory_to_fs() } /// When a lease changes, update the lease in memory and on the writer. /// /// # Arguments /// /// * `mac_addr`: Mac address of the container /// * `lease`: Newest lease information /// /// returns: Result<(), Error> /// pub fn update_lease(&mut self, mac_addr: &str, lease: NetavarkLease) -> Result<(), io::Error> { let cache = &mut self.mem; // write to the memory cache cache.insert(mac_addr.to_string(), vec![lease]); // write updated memory cache to the file system self.save_memory_to_fs() } /// When a singular container is taken down. Remove that lease from the cache memory and fs /// /// # Arguments /// /// * `mac_addr`: Mac address of the container pub fn remove_lease(&mut self, mac_addr: &str) -> Result { debug!("remove lease: {:?}", mac_addr); let mem = &mut self.mem; // Check and see if the lease exists, if not create an empty one let lease = match mem.get(mac_addr) { None => Lease { t1: 0, t2: 0, lease_time: 0, mtu: 0, domain_name: "".to_string(), mac_address: "".to_string(), is_v6: false, siaddr: "".to_string(), yiaddr: "".to_string(), srv_id: "".to_string(), subnet_mask: "".to_string(), broadcast_addr: "".to_string(), dns_servers: vec![], gateways: vec![], ntp_servers: vec![], host_name: "".to_string(), }, Some(l) => l[0].clone(), }; // Try and remove the lease. If it doesnt exist, exit with the blank lease if mem.remove(mac_addr).is_none() { return Ok(lease); } // write updated memory cache to the file system match self.save_memory_to_fs() { Ok(_) => Ok(lease), Err(e) => Err(e), } } /// Clean up the memory and file system on tear down of the proxy server pub fn teardown(&mut self) -> Result<(), io::Error> { self.mem.clear(); self.save_memory_to_fs() } /// Save the memory contents to the file system. This will remove the contents in the file, /// then write the memory map to the file. This method will be called any the lease memory cache /// changes (new lease, remove lease, update lease) fn save_memory_to_fs(&mut self) -> io::Result<()> { let mem = &self.mem; let writer = &mut self.writer; // Clear the writer so we can add the old leases match writer.clear() { Ok(_) => { serde_json::to_writer(writer.by_ref(), &mem)?; writer.flush() } Err(e) => { error!( "Could not clear the writer. Not updating lease information: {:?}", e ); Ok(()) } } } // rust validators require both len and is_empty if you define one // of them pub fn len(&self) -> usize { self.mem.len() } pub fn is_empty(&self) -> bool { if self.len() < 1 { return true; } false } } #[cfg(test)] mod cache_tests { use super::super::cache::LeaseCache; use super::super::lib::g_rpc::{Lease as NetavarkLease, Lease}; use crate::network::core_utils; use rand::{rng, Rng}; use std::collections::HashMap; use std::io::Cursor; // Create a single random ipv4 addr fn random_ipv4() -> String { let mut rng = rng(); format!( "{:?}.{:?}.{:?}.{:?}.", rng.random_range(0..255), rng.random_range(0..255), rng.random_range(0..255), rng.random_range(0..255) ) } // Create a single random mac address fn random_macaddr() -> String { let mut rng = rng(); let bytes = vec![ rng.random::(), rng.random::(), rng.random::(), rng.random::(), rng.random::(), rng.random::(), ]; core_utils::CoreUtils::encode_address_to_hex(&bytes) } // Create a single random lease fn random_lease(mac_address: &String) -> Lease { Lease { t1: 0, t2: 3600, lease_time: 0, mtu: 0, domain_name: "example.domain".to_string(), mac_address: String::from(mac_address), siaddr: random_ipv4(), yiaddr: random_ipv4(), srv_id: random_ipv4(), subnet_mask: "".to_string(), broadcast_addr: "".to_string(), dns_servers: vec![], gateways: vec![], ntp_servers: vec![], host_name: "example.host_name".to_string(), is_v6: false, } } // Shared information for all tests struct CacheTestSetup { cache: LeaseCache>>, macaddrs: Vec, range: u8, } impl CacheTestSetup { fn new() -> Self { // Use byte Cursor instead of file for testing let buff = Cursor::new(Vec::new()); let cache = match LeaseCache::new(buff) { Ok(cache) => cache, Err(e) => panic!("Could not create leases cache: {e:?}"), }; // Create a random amount of randomized leases let macaddrs = Vec::new(); let mut rng = rng(); // Make a random amount of leases let range: u8 = rng.random_range(0..10); CacheTestSetup { cache, macaddrs, range, } } } #[test] fn add_leases() { let setup = CacheTestSetup::new(); let mut cache = setup.cache; let mut macaddrs = setup.macaddrs; let range = setup.range; for i in 0..range { // Create a random mac address to create a random lease of that mac address let mac_address = random_macaddr(); macaddrs.push(mac_address.clone()); let lease = random_lease(&mac_address); // Add the lease to the cache cache .add_lease(&mac_address, &lease) .expect("could not add lease to cache"); // Deserialize the written bytes to compare let lease_bytes = cache.writer.get_ref().as_slice(); let s: HashMap> = match serde_json::from_slice(lease_bytes) { Ok(s) => s, Err(e) => panic!("Error: {e:?}"), }; // Get the mac address of the lease let macaddr = macaddrs .get(i as usize) .expect("Could not get the mac address of the lease added"); // Find the lease in the set of deserialized leases let deserialized_lease = s .get(macaddr) .expect("Could not get the mac address from the map") .first() .expect("Could not get lease from set of mac addresses") .clone(); // Assure that the amount of leases added is correct amount assert_eq!(s.len(), (i + 1) as usize); // Assure that the lease added was correct assert_eq!(lease, deserialized_lease); } } #[test] fn remove_leases() { let setup = CacheTestSetup::new(); let mut cache = setup.cache; let mut macaddrs = setup.macaddrs; let range = setup.range; for i in 0..range { // Create a random mac address to create a random lease of that mac address let mac_address = random_macaddr(); macaddrs.push(mac_address.clone()); let lease = random_lease(&mac_address); // Add the lease to the cache cache .add_lease(&mac_address, &lease) .expect("could not add lease to cache"); // Deserialize the written bytes to compare let lease_bytes = cache.writer.get_ref().as_slice(); let s: HashMap> = match serde_json::from_slice(lease_bytes) { Ok(s) => s, Err(e) => panic!("Error: {e:?}"), }; // Get the mac address of the lease let macaddr = macaddrs .get(i as usize) .expect("Could not get the mac address of the lease added"); // Find the lease in the set of deserialized leases let deserialized_lease = s .get(macaddr) .expect("Could not get the mac address from the map") .first() .expect("Could not get lease from set of mac addresses") .clone(); // Assure that the amount of leases added is correct amount assert_eq!(s.len(), (i + 1) as usize); // Assure that the lease added was correct assert_eq!(lease, deserialized_lease); } for i in 0..range { // Deserialize the written bytes to compare let lease_bytes = cache.writer.get_ref().as_slice(); let s: HashMap> = match serde_json::from_slice(lease_bytes) { Ok(s) => s, Err(e) => panic!("Error: {e:?}"), }; let macaddr = macaddrs .get(i as usize) .expect("Could not get the mac address of the lease added"); let deserialized_lease = s .get(macaddr) .expect("Could not get the mac address from the map") .first() .expect("Could not get lease from set of mac addresses") .clone(); let removed_lease = cache .remove_lease(macaddr) .unwrap_or_else(|_| panic!("Could not remove {macaddr:?} from leases")); // Assure the lease is no longer in memory assert_eq!(deserialized_lease, removed_lease); assert_eq!(s.len(), (range - i) as usize); // Deserialize the cache again to assure the lease is not in the writer let lease_bytes = cache.writer.get_ref().as_slice(); let s: HashMap> = match serde_json::from_slice(lease_bytes) { Ok(s) => s, Err(e) => panic!("Error: {e:?}"), }; // There should be no lease under that mac address if the lease was removed let no_lease = s.get(macaddr); assert_eq!(no_lease, None); // Remove a lease that does not exist let removed_lease = cache .remove_lease(macaddr) .expect("Could not remove the lease successfully"); // The returned lease should be a blank one assert_eq!(removed_lease.mac_address, "".to_string()); } } #[test] fn update_leases() { let setup = CacheTestSetup::new(); let mut cache = setup.cache; let mut macaddrs = setup.macaddrs; let range = setup.range; for i in 0..range { // Create a random mac address to create a random lease of that mac address let mac_address = random_macaddr(); macaddrs.push(mac_address.clone()); let lease = random_lease(&mac_address); // Add the lease to the cache cache .add_lease(&mac_address, &lease) .expect("could not add lease to cache"); // Deserialize the written bytes to compare let lease_bytes = cache.writer.get_ref().as_slice(); let s: HashMap> = match serde_json::from_slice(lease_bytes) { Ok(s) => s, Err(e) => panic!("Error: {e:?}"), }; // Get the mac address of the lease let macaddr = macaddrs .get(i as usize) .expect("Could not get the mac address of the lease added"); // Find the lease in the set of deserialized leases let deserialized_lease = s .get(macaddr) .expect("Could not get the mac address from the map") .first() .expect("Could not get lease from set of mac addresses") .clone(); // Assure that the amount of leases added is correct amount assert_eq!(s.len(), (i + 1) as usize); // Assure that the lease added was correct assert_eq!(lease, deserialized_lease); } // Update all of the leases for i in 0..range { // Deserialize the written bytes to compare let macaddr = macaddrs .get(i as usize) .expect("Could not get the mac address of the lease added"); // Create a new random lease with the same mac address let new_lease = random_lease(macaddr); cache .update_lease(macaddr, new_lease.clone()) .expect("Could not update the lease"); // Deserialize the cache again to assure the lease is not in the writer let lease_bytes = cache.writer.get_ref().as_slice(); let s: HashMap> = match serde_json::from_slice(lease_bytes) { Ok(s) => s, Err(e) => panic!("Error: {e:?}"), }; // There should be no lease under that mac address if the lease was removed let deserialized_updated_lease = s .get(macaddr) .expect("Could not get lease from deserialized map") .first() .expect("Could not find lease in set of multi-homing leases"); assert_eq!(deserialized_updated_lease, &new_lease); } } } containers-netavark-68510f4/src/dhcp_proxy/dhcp_service.rs000066400000000000000000000233031475240420400237240ustar00rootroot00000000000000use std::net::Ipv4Addr; use crate::dhcp_proxy::dhcp_service::DhcpServiceErrorKind::{ Bug, InvalidArgument, NoLease, Timeout, }; use crate::dhcp_proxy::lib::g_rpc::{Lease as NetavarkLease, NetworkConfig}; use crate::error::{ErrorWrap, NetavarkError, NetavarkResult}; use crate::network::core_utils; use crate::network::netlink::Route; use crate::wrap; use log::debug; use mozim::{DhcpV4ClientAsync, DhcpV4Config, DhcpV4Lease as MozimV4Lease}; use tokio_stream::StreamExt; use tonic::{Code, Status}; /// The kind of DhcpServiceError that can be caused when finding a dhcp lease pub enum DhcpServiceErrorKind { Timeout, InvalidArgument, InvalidDhcpServerReply, NoLease, Bug, LeaseExpired, Unimplemented, } /// A DhcpServiceError is an error caused in the process of finding a dhcp lease pub struct DhcpServiceError { kind: DhcpServiceErrorKind, msg: String, } impl DhcpServiceError { pub fn new(kind: DhcpServiceErrorKind, msg: String) -> Self { DhcpServiceError { kind, msg } } } /// DHCP service is responsible for creating, handling, and managing the dhcp lease process. pub struct DhcpV4Service { client: DhcpV4ClientAsync, network_config: NetworkConfig, previous_lease: Option, } impl DhcpV4Service { pub fn new(nc: NetworkConfig, timeout: u32) -> Result { let mut config = DhcpV4Config::new_proxy(&nc.host_iface, &nc.container_mac_addr); config.set_timeout(timeout); // Sending the hostname to the DHCP server is optional but it can be useful // in environments where DDNS is used to create or update DNS records. if !nc.host_name.is_empty() { config.set_host_name(&nc.host_name); }; // DHCP servers use the "client id", which is usually the MAC address, // to keep track of leases but each time the container starts, it gets // a new, random, MAC address so there's a good chance that the container // won't get the same IP address if it restarts. This can be an issue if // a container provides a service and needs to be restarted because, even // if DDNS is in use and the container has a DNS A record, a client may // still have the old IP address cached until the DNS TTL expires. // // Since the container id remains constant for life of the container // and it should be globally unique, we can use it as the client id to // ensure the container gets the same IP address each time it starts. // The client id is a byte array so we need to convert the container id // to a byte array. The client_id_type of "0" means the client id // is not a hardware address. config.set_client_id(0, nc.container_id.as_bytes()); let client = match DhcpV4ClientAsync::init(config, None) { Ok(client) => Ok(client), Err(err) => Err(DhcpServiceError::new(InvalidArgument, err.to_string())), }?; Ok(Self { client, network_config: nc, previous_lease: None, }) } /// Performs a DHCP DORA on a ipv4 network configuration. /// # Arguments /// /// * `client`: a IPv4 mozim dhcp client. When this method is called, it takes ownership of client. /// /// returns: Result. Either finds a lease successfully, finds no lease, or fails /// pub async fn get_lease(&mut self) -> Result { if let Some(lease_result) = self.client.next().await { match lease_result { Ok(lease) => { let mut netavark_lease = >::from(lease.clone()); netavark_lease.add_domain_name(&self.network_config.domain_name); netavark_lease.add_mac_address(&self.network_config.container_mac_addr); debug!( "found a lease for {:?}, {:?}", &self.network_config.container_mac_addr, &netavark_lease ); self.previous_lease = Some(lease); return Ok(netavark_lease); } Err(err) => { return Err(match err.kind() { mozim::ErrorKind::Timeout => { DhcpServiceError::new(Timeout, err.to_string()) } mozim::ErrorKind::InvalidArgument => { DhcpServiceError::new(InvalidArgument, err.to_string()) } mozim::ErrorKind::NoLease => { DhcpServiceError::new(NoLease, err.to_string()) } mozim::ErrorKind::Bug => DhcpServiceError::new(Bug, err.to_string()), _ => DhcpServiceError::new(Bug, err.to_string()), }) } } } Err(DhcpServiceError::new( Timeout, "Could not find a lease within the timeout limit".to_string(), )) } } impl std::fmt::Display for DhcpServiceError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.msg) } } impl From for Status { fn from(err: DhcpServiceError) -> Self { match err.kind { Timeout => Status::new(Code::Aborted, err.msg), InvalidArgument => Status::new(Code::InvalidArgument, err.msg), NoLease => Status::new(Code::NotFound, err.msg), Bug => Status::new(Code::Internal, err.msg), _ => Status::new(Code::Internal, err.msg), } } } pub async fn process_client_stream(mut client: DhcpV4Service) { while let Some(lease) = client.client.next().await { match lease { Ok(lease) => { log::info!( "got new lease for mac {}: {:?}", &client.network_config.container_mac_addr, &lease ); // get previous lease and check if ip addr changed, if not we do not have to do anything if let Some(old_lease) = &client.previous_lease { if old_lease.yiaddr != lease.yiaddr || old_lease.subnet_mask != lease.subnet_mask || old_lease.gateways != lease.gateways { // ips do not match, remove old ones and assign new ones. log::info!( "ip or gateway for mac {} changed, update address", &client.network_config.container_mac_addr ); match update_lease_ip( &client.network_config.ns_path, &client.network_config.container_iface, old_lease, &lease, ) { Ok(_) => {} Err(err) => { log::error!("{err}"); continue; } } } } client.previous_lease = Some(lease) } Err(err) => log::error!( "Failed to renew lease for {}: {err}", &client.network_config.container_mac_addr ), } } } fn update_lease_ip( netns: &str, interface: &str, old_lease: &MozimV4Lease, new_lease: &MozimV4Lease, ) -> NetavarkResult<()> { let (_, netns) = core_utils::open_netlink_sockets(netns).wrap("failed to open netlink socket in netns")?; let mut sock = netns.netlink; let old_net = wrap!( ipnet::Ipv4Net::with_netmask(old_lease.yiaddr, old_lease.subnet_mask), "create ipnet from old lease" )?; let new_net = wrap!( ipnet::Ipv4Net::with_netmask(new_lease.yiaddr, new_lease.subnet_mask), "create ipnet from new lease" )?; if new_net != old_net { let link = sock .get_link(crate::network::netlink::LinkID::Name(interface.to_string())) .wrap("get interface in netns")?; sock.add_addr(link.header.index, &ipnet::IpNet::V4(new_net)) .wrap("add new addr")?; sock.del_addr(link.header.index, &ipnet::IpNet::V4(old_net)) .wrap("remove old addrs")?; } if new_lease.gateways != old_lease.gateways { if let Some(gws) = &old_lease.gateways { let old_gw = gws.first(); if let Some(gw) = old_gw { let route = Route::Ipv4 { dest: ipnet::Ipv4Net::new(Ipv4Addr::new(0, 0, 0, 0), 0)?, gw: *gw, metric: None, }; match sock.del_route(&route) { Ok(_) => {} Err(err) => match err.unwrap() { // special case do not error if route does not exists NetavarkError::Netlink(e) if -e.raw_code() == libc::ESRCH => {} _ => return Err(err).wrap("delete old default route"), }, }; } } if let Some(gws) = &new_lease.gateways { let new_gw = gws.first(); if let Some(gw) = new_gw { let route = Route::Ipv4 { dest: ipnet::Ipv4Net::new(Ipv4Addr::new(0, 0, 0, 0), 0)?, gw: *gw, metric: None, }; sock.add_route(&route)?; } } } Ok(()) } containers-netavark-68510f4/src/dhcp_proxy/ip.rs000066400000000000000000000134061475240420400217010ustar00rootroot00000000000000/* This file is intended to support netavark-dhcp-proxy configuring the IP information that it got from the dhcp server. Long term this file/function should move into netavark */ pub use crate::dhcp_proxy::lib::g_rpc::{Lease as NetavarkLease, Lease}; pub use crate::dhcp_proxy::types::{CustomErr, ProxyError}; use crate::network::core_utils; use crate::network::netlink; use crate::network::netlink::Socket; use ipnet::IpNet; use log::debug; use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; /* Information that came back in the DHCP lease like name_servers, domain and host names, etc. will be implemented in podman; not here. */ #[derive(Clone, Debug)] struct MacVLAN { address: IpAddr, gateways: Vec, interface: String, // Unset right now // mtu: u32, prefix_length: u8, } trait Address { fn new(l: &Lease, interface: &str) -> Result where Self: Sized; fn add_ip(&self, nls: &mut Socket) -> Result<(), ProxyError>; fn add_gws(&self, nls: &mut Socket) -> Result<(), ProxyError>; } fn handle_gws(g: Vec, netmask: &str) -> Result, ProxyError> { // TODO Need unit test let mut gws = Vec::new(); for route in g { // TODO FIX for ipv6 let sub_mask = match Ipv4Addr::from_str(netmask) { Ok(n) => n, Err(e) => return Err(ProxyError::new(e.to_string())), }; let prefix = u32::from(sub_mask).count_ones(); let ip = match Ipv4Addr::from_str(&route) { Ok(i) => i, Err(e) => return Err(ProxyError::new(e.to_string())), }; let gw = match IpNet::new(IpAddr::from(ip), prefix as u8) { Ok(r) => r, Err(e) => return Err(ProxyError::new(format!("{e}:'{route}'"))), }; gws.push(gw); } Ok(gws) } #[test] fn test_bad_gw_handle_gws() { let gws = vec!["192.168.1.1".to_string(), "10.10.10".into()]; let netmask = "255.255.255.0"; assert!(handle_gws(gws, netmask).is_err()) } #[test] fn test_bad_subnet_handle_gws() { let gws = vec!["192.168.1.1".to_string(), "10.10.10.1".into()]; let netmask = "255.255.255"; assert!(handle_gws(gws, netmask).is_err()) } #[test] fn test_handle_gws() { let gws = vec!["192.168.1.1".to_string(), "10.10.10.1".into()]; let netmask = "255.255.255.0"; assert!(handle_gws(gws, netmask).is_ok()) } // IPV4 implementation impl Address for MacVLAN { fn new(l: &NetavarkLease, interface: &str) -> Result { debug!("new ipv4 macvlan for {}", interface); let address = match IpAddr::from_str(&l.yiaddr) { Ok(a) => a, Err(e) => { return Err(ProxyError::new(format!("bad address: {e}"))); } }; let gateways = match handle_gws(l.gateways.clone(), &l.subnet_mask) { Ok(g) => g, Err(e) => { return Err(ProxyError::new(format!("bad gateways: {}", e))); } }; let prefix_length = match get_prefix_length_v4(&l.subnet_mask) { Ok(u) => u as u8, Err(e) => return Err(ProxyError::new(e.to_string())), }; Ok(MacVLAN { address, gateways, interface: interface.to_string(), // Disabled for now // mtu: l.mtu, prefix_length, }) } // add the ip address to the container namespace fn add_ip(&self, nls: &mut Socket) -> Result<(), ProxyError> { debug!("adding network information for {}", self.interface); let ip = IpNet::new(self.address, self.prefix_length)?; let dev = nls.get_link(netlink::LinkID::Name(self.interface.clone()))?; match nls.add_addr(dev.header.index, &ip) { Ok(_) => Ok(()), Err(e) => Err(ProxyError::new(e.to_string())), } } // add one or more routes to the container namespace fn add_gws(&self, nls: &mut Socket) -> Result<(), ProxyError> { debug!("adding gateways to {}", self.interface); match core_utils::add_default_routes(nls, &self.gateways, None) { Ok(_) => Ok(()), Err(e) => Err(ProxyError::new(e.to_string())), } } } // setup takes the DHCP lease and some additional information and // applies the TCP/IP information to the namespace. pub fn setup(lease: &NetavarkLease, interface: &str, ns_path: &str) -> Result<(), ProxyError> { debug!("setting up {}", interface); let vlan = MacVLAN::new(lease, interface)?; let (_, mut netns) = core_utils::open_netlink_sockets(ns_path)?; vlan.add_ip(&mut netns.netlink)?; vlan.add_gws(&mut netns.netlink) } // teardown is likely unnecessary but holding place here pub fn teardown() -> Result<(), ProxyError> { todo!() } /// get_prefix_lengh takes a subnet mask in str form and /// returns its prefix length by counting ones. /// /// # Arguments /// /// * `netmask`: str form of subnet mask (i.e. 255.255.255.0) /// /// returns: Result /// /// # Examples /// /// ``` /// /// ``` fn get_prefix_length_v4(netmask: &str) -> Result { let sub_mask = match Ipv4Addr::from_str(netmask) { Ok(n) => n, Err(e) => return Err(ProxyError::new(e.to_string())), }; Ok(u32::from(sub_mask).count_ones()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_24() { assert_eq!(get_prefix_length_v4("255.255.255.0").unwrap(), 24_u32) } #[test] fn test_16() { assert_eq!(get_prefix_length_v4("255.255.0.0").unwrap(), 16_u32) } #[test] fn test_25() { assert_eq!(get_prefix_length_v4("255.255.255.128").unwrap(), 25_u32) } #[test] fn test_bad_input() { assert!(get_prefix_length_v4("255.255.128").is_err()) } } containers-netavark-68510f4/src/dhcp_proxy/lib.rs000066400000000000000000000227571475240420400220500ustar00rootroot00000000000000extern crate core; use crate::dhcp_proxy::lib::g_rpc::{Lease, NetworkConfig}; use crate::error::NetavarkError; use std::convert::TryFrom; use std::error::Error; use g_rpc::netavark_proxy_client::NetavarkProxyClient; use hyper_util::rt::TokioIo; use log::debug; use std::fs::File; use std::net::AddrParseError; use std::net::Ipv4Addr; use std::str::FromStr; use tokio::net::UnixStream; use tonic::transport::{Channel, Endpoint}; use tonic::Request; use tower::service_fn; #[allow(clippy::unwrap_used)] pub mod g_rpc { include!(concat!(env!("OUT_DIR"), "/netavark_proxy.rs")); use crate::dhcp_proxy::lib::VectorConv; use crate::dhcp_proxy::types::{CustomErr, ProxyError}; use mozim::DhcpV4Lease; use std::convert::TryFrom; use std::net::Ipv4Addr; use std::str::FromStr; impl Lease { /// Add mac address to a lease pub fn add_mac_address(&mut self, mac_addr: &String) { self.mac_address = mac_addr.to_string() } /// Update the domain name of the lease pub fn add_domain_name(&mut self, domain_name: &String) { self.domain_name = domain_name.to_string(); } } impl From for Lease { fn from(l: DhcpV4Lease) -> Lease { // Since these fields are optional as per mozim. Match them first and then set them let domain_name = l.domain_name.unwrap_or_default(); let mtu = l.mtu.unwrap_or(0) as u32; Lease { t1: l.t1, t2: l.t2, lease_time: l.lease_time, mtu, domain_name, mac_address: "".to_string(), siaddr: l.siaddr.to_string(), yiaddr: l.yiaddr.to_string(), srv_id: l.srv_id.to_string(), subnet_mask: l.subnet_mask.to_string(), // TODO something is jacked with8 broadcast, moving on broadcast_addr: "".to_string(), dns_servers: handle_ip_vectors(l.dns_srvs), gateways: handle_ip_vectors(l.gateways), ntp_servers: handle_ip_vectors(l.ntp_srvs), host_name: l.host_name.unwrap_or_else(|| String::from("")), is_v6: false, } } } impl TryFrom for DhcpV4Lease { type Error = ProxyError; fn try_from(l: Lease) -> Result { let host_name = if !l.host_name.is_empty() { Some(l.host_name) } else { None }; let domain_name = if !l.domain_name.is_empty() { Some(l.domain_name) } else { None }; let broadcast_addr = if !l.broadcast_addr.is_empty() { Some(Ipv4Addr::from_str(&l.broadcast_addr)?) } else { None }; let mtu = match u16::try_from(l.mtu) { Ok(m) => Some(m), Err(e) => return Err(ProxyError::new(e.to_string())), }; // Have to do it the hard way because the struct in mozim has a private // called srv_id which is a vector of 6 u8s representing the DHCP server's // mac address let mut lease = DhcpV4Lease::default(); lease.siaddr = Ipv4Addr::from_str(&l.siaddr)?; lease.yiaddr = Ipv4Addr::from_str(&l.yiaddr)?; lease.t1 = l.t1; lease.t2 = l.t2; lease.lease_time = l.lease_time; lease.srv_id = Ipv4Addr::from_str(&l.srv_id)?; lease.subnet_mask = Ipv4Addr::from_str(&l.subnet_mask)?; lease.broadcast_addr = broadcast_addr; lease.dns_srvs = l.dns_servers.to_v4_addrs()?; lease.gateways = l.gateways.to_v4_addrs()?; lease.ntp_srvs = l.ntp_servers.to_v4_addrs()?; lease.mtu = mtu; lease.host_name = host_name; lease.domain_name = domain_name; Ok(lease) } } fn handle_ip_vectors(ip: Option>) -> Vec { let mut ips: Vec = Vec::new(); if let Some(j) = ip { for ip in j { ips.push(ip.to_string()); } } ips } impl From for NvIpv4Addr { fn from(ip: std::net::Ipv4Addr) -> NvIpv4Addr { NvIpv4Addr { octets: Vec::from(ip.octets()), } } } impl From> for NvIpv4Addr { fn from(ip: Option) -> Self { if let Some(addr) = ip { return NvIpv4Addr { octets: Vec::from(addr.octets()), }; } NvIpv4Addr { octets: Vec::from([0, 0, 0, 0]), } } } #[test] fn test_handle_gw() { use std::str::FromStr; let mut ips: Vec = Vec::new(); for i in 0..5 { let ip = format!("10.1.{i}.1"); let ipv4 = std::net::Ipv4Addr::from_str(&ip).expect("failed hard"); ips.push(ipv4); } let response = handle_ip_vectors(Some(ips)); // Len of response should be same as ips assert_eq!(response.len(), 5); assert_eq!(response[0].to_string(), "10.1.0.1"); } } // A collection of functions for client side connections to the proxy server impl NetworkConfig { pub fn load(path: &str) -> Result> { let file = std::io::BufReader::new(File::open(path)?); Ok(serde_json::from_reader(file)?) } /// get_client is an internal function to obtain the uds endpoint /// /// # Arguments /// /// * `p`: path to uds /// /// returns: Result, NetavarkError> /// /// # Examples /// /// ``` /// /// ``` async fn get_client(p: String) -> Result, NetavarkError> { // We do not know why the uds connections need to be done like this. The // maintainer suggested it is part of the their API. // We know this is safe and if it ever fails test will catch it let endpoint = Endpoint::try_from("http://[::1]").unwrap(); debug!("using uds path: {}", &p); let path = p.clone(); let channel = endpoint .connect_with_connector(service_fn(move |_| { let path = p.clone(); async{Ok::<_, std::io::Error>(TokioIo::new(UnixStream::connect(path).await?))} })) .await .map_err(|e| { let msg = match e.source() { Some(err) => { // this is a bit ugly but we check if the socket was not found to provide a proper error message // and hint at the systemd socket unit match err .source() .and_then(|e| e.downcast_ref::()) .and_then(|e| { if e.kind() == std::io::ErrorKind::NotFound || e.kind() == std::io::ErrorKind::ConnectionRefused { Some(format!("socket \"{}\": {e}, is the netavark-dhcp-proxy.socket unit enabled?", &path)) } else { None } }) { Some(msg) => msg, None => err.to_string(), } } None => e.to_string(), }; NetavarkError::msg(msg) })?; Ok(NetavarkProxyClient::new(channel)) } /// get_lease is a wrapper function for obtaining a lease /// over grpc from the nvproxy-server /// /// # Arguments /// /// * `p`: path to uds /// /// returns: Result /// /// # Examples /// /// ``` /// /// ``` pub async fn get_lease(self, p: &str) -> Result { let mut client = NetworkConfig::get_client(p.to_string()).await?; let lease = match client.setup(Request::new(self)).await { Ok(l) => l.into_inner(), Err(s) => return Err(s.into()), }; Ok(lease) } /// drop_lease is a wrapper function to release the current /// DHCP lease via the nvproxy /// /// /// # Arguments /// /// * `p`: path to uds /// /// returns: Result /// /// # Examples /// /// ``` /// /// ``` pub async fn drop_lease(self, p: &str) -> Result { let mut client = NetworkConfig::get_client(p.to_string()).await?; let lease = match client.teardown(Request::new(self)).await { Ok(l) => l.into_inner(), Err(e) => return Err(e.into()), }; Ok(lease) } } trait VectorConv { fn to_v4_addrs(&self) -> Result>, AddrParseError>; } impl VectorConv for Vec { fn to_v4_addrs(&self) -> Result>, AddrParseError> { if self.is_empty() { return Ok(None); } let mut out_addrs = Vec::new(); for ip in self { match Ipv4Addr::from_str(ip) { Ok(i) => out_addrs.push(i), Err(e) => return Err(e), }; } Ok(Some(out_addrs)) } } containers-netavark-68510f4/src/dhcp_proxy/mod.rs000066400000000000000000000001411475240420400220400ustar00rootroot00000000000000pub mod cache; pub mod dhcp_service; pub mod ip; pub mod lib; pub mod proxy_conf; pub mod types; containers-netavark-68510f4/src/dhcp_proxy/proxy_conf.rs000066400000000000000000000210771475240420400234620ustar00rootroot00000000000000// TODO these constant destinations are not final. use std::env; use std::path::{Path, PathBuf}; // Where the cache and socket are stored by default pub const NETAVARK_PROXY_RUN_DIR: &str = "/run/podman"; pub const NETAVARK_PROXY_RUN_DIR_ENV: &str = "NETAVARK_PROXY_RUN_DIR_ENV"; // Default UDS path for gRPC to communicate on. pub const DEFAULT_UDS_PATH: &str = "/run/podman/nv-proxy.sock"; // Default configuration directory. pub const DEFAULT_CONFIG_DIR: &str = ""; // Default Network configuration path pub const DEFAULT_NETWORK_CONFIG: &str = "/dev/stdin"; // Default epoll wait time before dhcp socket times out pub const DEFAULT_TIMEOUT: u32 = 8; // Proxy server gRPC socket file name pub const PROXY_SOCK_NAME: &str = "nv-proxy.sock"; // Where leases are stored on the filesystem pub const CACHE_FILE_NAME: &str = "nv-proxy.lease"; // Seconds until the service should exit pub const DEFAULT_INACTIVITY_TIMEOUT: u64 = 300; /// Get the RUN_DIR where the proxy cache and socket /// are stored /// /// /// # Arguments /// /// * `run_cli`: /// /// returns: String /// /// # Examples /// /// ``` /// /// ``` pub fn get_run_dir(run_cli: Option<&str>) -> String { // if environment, return it // if opt, return it // return default match env::var(NETAVARK_PROXY_RUN_DIR_ENV) { // env::var returns an error if the key doesnt exist Ok(v) => return v, Err(_) => { if let Some(val) = run_cli { return val.to_string(); } } } NETAVARK_PROXY_RUN_DIR.to_string() } /// Returns the fully qualified path of the proxy socket file including /// the socket file name /// /// # Arguments /// /// * `run_dir_opt`: /// /// returns: PathBuf /// /// # Examples /// /// ``` /// /// ``` pub fn get_proxy_sock_fqname(run_dir_opt: Option<&str>) -> PathBuf { let run_dir = get_run_dir(run_dir_opt); Path::new(&run_dir).join(PROXY_SOCK_NAME) } /// Returns the fully qualified path of the cache file including the cache /// file name /// /// /// # Arguments /// /// * `run_dir`: /// /// returns: PathBuf /// /// # Examples /// /// ``` /// /// ``` pub fn get_cache_fqname(run_dir: Option<&str>) -> PathBuf { let run_dir = get_run_dir(run_dir); Path::new(&run_dir).join(CACHE_FILE_NAME) } #[cfg(test)] mod conf_tests { use crate::dhcp_proxy::proxy_conf::{ get_cache_fqname, get_proxy_sock_fqname, get_run_dir, CACHE_FILE_NAME, NETAVARK_PROXY_RUN_DIR, NETAVARK_PROXY_RUN_DIR_ENV, PROXY_SOCK_NAME, }; use std::path::Path; use std::collections::HashMap; use std::env; use std::ffi::OsStr; use std::hash::Hash; use std::panic::{self, RefUnwindSafe, UnwindSafe}; use std::sync::Mutex; use once_cell::sync::Lazy; use rand::distr::Alphanumeric; use rand::{rng, Rng}; /// Make sure that the environment isn't modified concurrently. static SERIAL_TEST: Lazy> = Lazy::new(Default::default); /// /// The following stanzas of code should be attributed to https://github.com/vmx/temp-env /// /// The previous value is restored when the closure completes or panics, before unwinding the /// panic. /// /// If `value` is set to `None`, then the environment variable is unset. pub fn with_var(key: K, value: Option, closure: F) -> R where K: AsRef + Clone + Eq + Hash, V: AsRef + Clone, F: Fn() -> R + UnwindSafe + RefUnwindSafe, { with_vars(vec![(key, value)], closure) } /// Unsets a single environment variable for the duration of the closure. /// /// The previous value is restored when the closure completes or panics, before unwinding the /// panic. /// /// This is a shorthand and identical to the following: /// ```rust /// temp_env::with_var("MY_ENV_VAR", None::<&str>, || { /// // Run some code where `MY_ENV_VAR` is unset. /// }); /// ``` pub fn with_var_unset(key: K, closure: F) -> R where K: AsRef + Clone + Eq + Hash, F: Fn() -> R + UnwindSafe + RefUnwindSafe, { with_var(key, None::<&str>, closure) } /// Sets environment variables for the duration of the closure. /// /// The previous values are restored when the closure completes or panics, before unwinding the /// panic. /// /// If a `value` is set to `None`, then the environment variable is unset. /// /// If the variable with the same name is set multiple times, the last one wins. pub fn with_vars(kvs: Vec<(K, Option)>, closure: F) -> R where K: AsRef + Clone + Eq + Hash, V: AsRef + Clone, F: Fn() -> R + UnwindSafe + RefUnwindSafe, { let guard = SERIAL_TEST.lock().unwrap(); let mut old_kvs: HashMap> = HashMap::new(); for (key, value) in kvs { // If the same key is given several times, the original/old value is only correct before // the environment was updated. if !old_kvs.contains_key(&key) { let old_value = env::var(&key).ok(); old_kvs.insert(key.clone(), old_value); } update_env(&key, value); } match panic::catch_unwind(closure) { Ok(result) => { for (key, value) in old_kvs { update_env(key, value); } result } Err(err) => { for (key, value) in old_kvs { update_env(key, value); } drop(guard); panic::resume_unwind(err); } } } fn update_env(key: K, value: Option) where K: AsRef, V: AsRef, { match value { Some(v) => env::set_var(key, v), None => env::remove_var(key), } } fn random_string(len: usize) -> String { let rand_string: String = rng() .sample_iter(&Alphanumeric) .take(len) .map(char::from) .collect(); format!("/{rand_string}") } // The following tests seem to be susceptible to the environment variables poisoning // each other when run in parallel (default rust behavior). If we set --test-threads=1, // this will not happen. For now, I wrap the tests in `with_var_unset`. #[test] fn test_run_dir_env() { let r = random_string(25); with_var(NETAVARK_PROXY_RUN_DIR_ENV, Some(&r), || { assert_eq!(get_run_dir(None), r) }); } #[test] fn test_run_dir_with_opt() { let r = random_string(25); with_var_unset(NETAVARK_PROXY_RUN_DIR_ENV, || { assert_eq!(get_run_dir(Some(&r)), r) }); } #[test] fn test_run_dir_as_none() { with_var_unset(NETAVARK_PROXY_RUN_DIR_ENV, || { assert_eq!(get_run_dir(None), NETAVARK_PROXY_RUN_DIR) }); } #[test] fn test_get_cache_env() { let r = random_string(25); with_var(NETAVARK_PROXY_RUN_DIR_ENV, Some(&r), || { assert_eq!(get_cache_fqname(None), Path::new(&r).join(CACHE_FILE_NAME)); }); } #[test] fn test_get_cache_with_opt() { let r = random_string(25); with_var_unset(NETAVARK_PROXY_RUN_DIR_ENV, || { assert_eq!( get_cache_fqname(Some(&r)), Path::new(&r).join(CACHE_FILE_NAME) ) }) } #[test] fn test_get_cache_as_none() { with_var_unset(NETAVARK_PROXY_RUN_DIR_ENV, || { assert_eq!( get_cache_fqname(None), Path::new(NETAVARK_PROXY_RUN_DIR).join(CACHE_FILE_NAME) ) }); } #[test] fn test_get_proxy_sock_env() { let r = random_string(25); with_var(NETAVARK_PROXY_RUN_DIR_ENV, Some(&r), || { assert_eq!( get_proxy_sock_fqname(None), Path::new(&r).join(PROXY_SOCK_NAME) ); }); } #[test] fn test_get_proxy_sock_with_opt() { let r = random_string(25); with_var_unset(NETAVARK_PROXY_RUN_DIR_ENV, || { assert_eq!( get_proxy_sock_fqname(Some(&r)), Path::new(&r).join(PROXY_SOCK_NAME) ) }) } #[test] fn test_get_proxy_sock_as_none() { with_var_unset(NETAVARK_PROXY_RUN_DIR_ENV, || { assert_eq!( get_proxy_sock_fqname(None), Path::new(NETAVARK_PROXY_RUN_DIR).join(PROXY_SOCK_NAME) ) }); } } containers-netavark-68510f4/src/dhcp_proxy/types.rs000066400000000000000000000037321475240420400224360ustar00rootroot00000000000000use crate::dhcp_proxy::lib::g_rpc::NetworkConfig; use crate::error::NetavarkError; use ipnet::PrefixLenError; use mozim::DhcpError; use mozim::ErrorKind::InvalidArgument; use std::net::AddrParseError; use std::num::ParseIntError; use std::str::FromStr; use std::string::ToString; use tonic::{Code, Status}; impl FromStr for NetworkConfig { type Err = ParseIntError; fn from_str(_s: &str) -> Result { // s is actually a string so if we intend to generate // a `NetworkConfig` object from `s` parse `s` and populate // instead of default empty values Ok(NetworkConfig { host_iface: "".to_string(), container_mac_addr: "".to_string(), domain_name: "".to_string(), host_name: "".to_string(), version: 0, ns_path: "".to_string(), container_iface: "".to_string(), container_id: "".to_string(), }) } } #[derive(Debug, Clone)] pub struct ProxyError(String); pub trait CustomErr { fn new(msg: String) -> Self; } impl CustomErr for ProxyError { fn new(msg: String) -> Self { ProxyError(msg) } } impl std::fmt::Display for ProxyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl From for Status { fn from(pe: ProxyError) -> Self { Status::new(Code::Unknown, pe.to_string()) } } impl From for ProxyError { fn from(cause: NetavarkError) -> Self { ProxyError::new(cause.to_string()) } } impl From for ProxyError { fn from(cause: PrefixLenError) -> Self { ProxyError::new(cause.to_string()) } } impl From for ProxyError { fn from(e: AddrParseError) -> Self { ProxyError::new(e.to_string()) } } impl From for DhcpError { fn from(e: ProxyError) -> Self { DhcpError::new(InvalidArgument, e.to_string()) } } containers-netavark-68510f4/src/dhcp_proxy_client/000077500000000000000000000000001475240420400222555ustar00rootroot00000000000000containers-netavark-68510f4/src/dhcp_proxy_client/client.rs000066400000000000000000000052021475240420400241000ustar00rootroot00000000000000use clap::{Parser, Subcommand}; use commands::{setup, teardown}; use std::process; use tonic::{Code, Status}; use netavark::dhcp_proxy::lib::g_rpc::{Lease, NetworkConfig}; use netavark::dhcp_proxy::proxy_conf::{DEFAULT_NETWORK_CONFIG, DEFAULT_UDS_PATH}; use netavark::error::NetavarkError; pub mod commands; #[derive(Parser, Debug)] #[clap(version = env!("CARGO_PKG_VERSION"))] struct Opts { /// Use specific uds path #[clap(short, long)] uds: Option, /// Instead of reading from STDIN, read the configuration to be applied from the given file. #[clap(short, long)] file: Option, /// Netavark trig command #[clap(subcommand)] subcmd: SubCommand, } #[derive(Subcommand, Debug)] enum SubCommand { /// Configures the given network namespace with the given configuration. Setup(setup::Setup), /// Undo any configuration applied via setup command. Teardown(teardown::Teardown), // Display info about netavark. // Version(version::Version), } #[cfg(unix)] #[tokio::main] // This client assumes you use the default lease directory async fn main() -> Result<(), Box> { // This should be moved to somewhere central. We also need to add override logic. env_logger::builder().format_timestamp(None).init(); let opts = Opts::parse(); let file = opts .file .unwrap_or_else(|| DEFAULT_NETWORK_CONFIG.to_string()); let uds_path = opts.uds.unwrap_or_else(|| DEFAULT_UDS_PATH.to_string()); let input_config = NetworkConfig::load(&file)?; let result = match opts.subcmd { SubCommand::Setup(s) => s.exec(&uds_path, input_config).await, SubCommand::Teardown(t) => t.exec(&uds_path, input_config).await, }; let r = match result { Ok(r) => r, Err(e) => { eprintln!("Error: {e}"); match e { NetavarkError::DHCPProxy(status) => process_failure(status), _ => process::exit(1), } } }; let pp = ::serde_json::to_string_pretty(&r); // TODO this should probably return an empty lease so consumers // don't soil themselves println!("{}", pp.unwrap_or_else(|_| "".to_string())); Ok(()) } // // process_failure makes the client exit with a specific // error code // fn process_failure(status: Status) -> Lease { let mut rc: i32 = 1; match status.code() { Code::Unknown => { rc = 155; } Code::InvalidArgument => { rc = 156; } Code::DeadlineExceeded => {} Code::NotFound => { rc = 6; } _ => {} } process::exit(rc) } containers-netavark-68510f4/src/dhcp_proxy_client/commands/000077500000000000000000000000001475240420400240565ustar00rootroot00000000000000containers-netavark-68510f4/src/dhcp_proxy_client/commands/mod.rs000066400000000000000000000000651475240420400252040ustar00rootroot00000000000000pub mod setup; pub mod teardown; // pub mod version; containers-netavark-68510f4/src/dhcp_proxy_client/commands/setup.rs000066400000000000000000000006711475240420400255700ustar00rootroot00000000000000use clap::Parser; use log::debug; use netavark::{ dhcp_proxy::lib::g_rpc::{Lease, NetworkConfig}, error::NetavarkError, }; #[derive(Parser, Debug)] pub struct Setup {} impl Setup { pub async fn exec(&self, p: &str, config: NetworkConfig) -> Result { debug!("Setting up..."); debug!("input: {:#?}", serde_json::to_string_pretty(&config)); config.clone().get_lease(p).await } } containers-netavark-68510f4/src/dhcp_proxy_client/commands/teardown.rs000066400000000000000000000005741475240420400262550ustar00rootroot00000000000000use clap::Parser; use log::debug; use netavark::{ dhcp_proxy::lib::g_rpc::{Lease, NetworkConfig}, error::NetavarkError, }; #[derive(Parser, Debug)] pub struct Teardown {} impl Teardown { pub async fn exec(&self, p: &str, config: NetworkConfig) -> Result { debug!("Entering teardown"); config.clone().drop_lease(p).await } } containers-netavark-68510f4/src/dns/000077500000000000000000000000001475240420400173245ustar00rootroot00000000000000containers-netavark-68510f4/src/dns/aardvark.rs000066400000000000000000000417131475240420400214730ustar00rootroot00000000000000use crate::error::{NetavarkError, NetavarkResult}; use fs2::FileExt; use libc::pid_t; use nix::sys::signal::{self, Signal}; use nix::unistd::Pid; use std::ffi::{OsStr, OsString}; use std::fs; use std::fs::File; use std::fs::OpenOptions; use std::io::Result; use std::io::{prelude::*, ErrorKind}; use std::net::Ipv4Addr; use std::net::{IpAddr, Ipv6Addr}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; const SYSTEMD_CHECK_PATH: &str = "/run/systemd/system"; const SYSTEMD_RUN: &str = "systemd-run"; const AARDVARK_COMMIT_LOCK: &str = "aardvark.lock"; #[derive(Clone, Debug)] pub struct AardvarkEntry<'a> { pub network_name: &'a str, pub network_gateways: Vec, pub network_dns_servers: &'a Option>, pub container_id: &'a str, pub container_ips_v4: Vec, pub container_ips_v6: Vec, pub container_names: Vec, pub container_dns_servers: &'a Option>, pub is_internal: bool, } #[derive(Debug, Clone)] pub struct Aardvark { /// aardvark's config directory pub config: PathBuf, /// tells if container is rootfull or rootless pub rootless: bool, /// path to the aardvark-dns binary pub aardvark_bin: OsString, /// port to bind to pub port: OsString, } impl Aardvark { pub fn new(config: PathBuf, rootless: bool, aardvark_bin: OsString, port: u16) -> Self { Aardvark { config, rootless, aardvark_bin, port: port.to_string().into(), } } /// On success returns aardvark server's pid or returns -1; fn get_aardvark_pid(&self) -> NetavarkResult { let path = Path::new(&self.config).join("aardvark.pid"); let pid: i32 = match fs::read_to_string(path) { Ok(content) => match content.parse::() { Ok(val) => val, Err(e) => { return Err(NetavarkError::msg(format!("parse aardvark pid: {e}"))); } }, Err(e) => { return Err(NetavarkError::Io(e)); } }; Ok(pid) } fn is_executable_in_path(program: &str) -> bool { if let Ok(path) = std::env::var("PATH") { for p in path.split(':') { let p_str = format!("{p}/{program}"); if fs::metadata(p_str).is_ok() { return true; } } } false } pub fn start_aardvark_server(&self) -> Result<()> { log::debug!("Spawning aardvark server"); let mut aardvark_args = vec![]; // only use systemd when it is booted, see sd_booted(3) if Path::new(SYSTEMD_CHECK_PATH).exists() && Aardvark::is_executable_in_path(SYSTEMD_RUN) { // TODO: This could be replaced by systemd-api. aardvark_args = vec![ OsStr::new(SYSTEMD_RUN), OsStr::new("-q"), OsStr::new("--scope"), ]; if self.rootless { aardvark_args.push(OsStr::new("--user")); } } aardvark_args.extend(vec![ self.aardvark_bin.as_os_str(), OsStr::new("--config"), self.config.as_os_str(), OsStr::new("-p"), self.port.as_os_str(), OsStr::new("run"), ]); log::debug!("start aardvark-dns: {:?}", aardvark_args); // After https://github.com/containers/aardvark-dns/pull/148 this command // will block till aardvark-dns's parent process returns back and let // aardvark inherit all the fds. let out = Command::new(aardvark_args[0]) .args(&aardvark_args[1..]) .stdin(Stdio::null()) .stdout(Stdio::null()) // set RUST_LOG for aardvark .env("RUST_LOG", log::max_level().as_str()) .output()?; if out.status.success() { return Ok(()); } if out.stderr.is_empty() { return Err(std::io::Error::new( std::io::ErrorKind::Other, "aardvark-dns exited unexpectedly without error message", )); } // aardvark-dns failed capture stderr let msg = String::from_utf8(out.stderr).map_err(|e| { std::io::Error::new( std::io::ErrorKind::Other, format!("failed to parse aardvark-dns stderr message: {e}"), ) })?; Err(std::io::Error::new( std::io::ErrorKind::Other, format!("aardvark-dns failed to start: {}", msg.trim()), )) } fn check_netns(&self, pid: pid_t) { // This should never fail but ignore errors anyway let cur_ns = match fs::read_link("/proc/self/ns/net") { Ok(p) => p, Err(_) => return, }; // This might fail let aardvark_ns = match fs::read_link(format!("/proc/{pid}/ns/net")) { Ok(p) => p, // In case of errors ignore them and do not warn. When the process is exiting then // several different errors can happen. I have observed ENOENT, ESRCH and EACCES so // to be safe just ignore all errors as this warning here is just best effort anyway. // https://github.com/containers/podman/issues/22103 Err(_) => return, }; if aardvark_ns != cur_ns { // netns does not match, this means dns will not work. // see https://github.com/containers/podman/issues/20396 for how that might happen // We do not not really what the problem in the aardvark-dns config files so we // cannot really self heal here and must ask the user to fix it. // I am not sure if this should be a hard error?? log::error!( "aardvark-dns runs in a different netns, dns will not work for this container. To resolve please stop all containers, kill the aardvark-dns process, remove the {} directory and then start the containers again", self.config.display() ); } } pub fn notify(&self, start: bool, is_update: bool) -> NetavarkResult<()> { match self.get_aardvark_pid() { Ok(pid) => { match signal::kill(Pid::from_raw(pid), Signal::SIGHUP) { Ok(_) => { // We do not want to check the netns when doing an update // this is not working because podman does not enter the // rootless netns for the update as we only change the file // and send SIGHUP. if !is_update { self.check_netns(pid) } return Ok(()); } Err(err) => { // ESRCH == process does not exists // start new sever below in that case and not error if err != nix::errno::Errno::ESRCH { return Err(NetavarkError::msg(format!( "failed to send SIGHUP to aardvark: {err}" ))); } } } } Err(err) => { if !start { return Err(NetavarkError::wrap("failed to get aardvark pid", err)); } } }; self.start_aardvark_server()?; Ok(()) } pub fn commit_entries(&self, entries: &[AardvarkEntry]) -> Result<()> { // Acquire fs lock to ensure other instance of aardvark cannot commit // or start aardvark instance till already running instance has not // completed its `commit` phase. let lockfile_path = Path::new(&self.config) .join("..") .join(AARDVARK_COMMIT_LOCK); let lockfile = match OpenOptions::new() .read(true) .write(true) .create(true) .truncate(true) .open(&lockfile_path) { Ok(file) => file, Err(e) => { return Err(std::io::Error::new( std::io::ErrorKind::Other, format!("Failed to open/create lockfile {:?}: {}", &lockfile_path, e), )); } }; if let Err(er) = lockfile.lock_exclusive() { return Err(std::io::Error::new( std::io::ErrorKind::Other, format!("Failed to acquire exclusive lock on {lockfile_path:?}: {er}"), )); } for entry in entries { let mut path = Path::new(&self.config).join(entry.network_name); if entry.is_internal { let new_path = Path::new(&self.config).join(entry.network_name.to_owned() + "%int"); let _ = std::fs::rename(&path, &new_path); path = new_path; } let file = match OpenOptions::new().write(true).create_new(true).open(&path) { Ok(mut f) => { // collect gateway let gws = entry .network_gateways .iter() .map(|g| g.to_string()) .collect::>() .join(","); // collect network dns servers if specified let network_dns_servers = if let Some(network_dns_servers) = &entry.network_dns_servers { if !network_dns_servers.is_empty() { let dns_server_collected = network_dns_servers .iter() .map(|g| g.to_string()) .collect::>() .join(","); format!(" {dns_server_collected}") } else { "".to_string() } } else { "".to_string() }; let data = format!("{gws}{network_dns_servers}\n"); f.write_all(data.as_bytes())?; // return error if write fails f } Err(ref e) if e.kind() == ErrorKind::AlreadyExists => { OpenOptions::new().append(true).open(&path)? } Err(e) => { return Err(e); } }; match Aardvark::commit_entry(entry, file) { Err(er) => { return Err(std::io::Error::new( std::io::ErrorKind::Other, format!("Failed to commit entry {entry:?}: {er}"), )); } Ok(_) => continue, } } Ok(()) } fn commit_entry(entry: &AardvarkEntry, mut file: File) -> Result<()> { let container_names = entry.container_names.join(","); let ipv4s = entry .container_ips_v4 .iter() .map(|g| g.to_string()) .collect::>() .join(","); let ipv6s = entry .container_ips_v6 .iter() .map(|g| g.to_string()) .collect::>() .join(","); let dns_server = if let Some(dns_servers) = &entry.container_dns_servers { if !dns_servers.is_empty() { let dns_server_collected = dns_servers .iter() .map(|g| g.to_string()) .collect::>() .join(","); format!(" {dns_server_collected}") } else { "".to_string() } } else { "".to_string() }; let data = format!( "{} {} {} {}{}\n", entry.container_id, ipv4s, ipv6s, container_names, dns_server ); file.write_all(data.as_bytes())?; // return error if write fails Ok(()) } pub fn commit_netavark_entries(&self, entries: Vec) -> NetavarkResult<()> { if !entries.is_empty() { self.commit_entries(&entries)?; match self.notify(true, false) { Ok(_) => (), Err(e) => { if let Err(err) = self.delete_from_netavark_entries(&entries) { log::warn!( "Failed to delete aardvark-dns entries after failed start: {err}" ); }; return Err(e); } }; } Ok(()) } pub fn delete_entry(&self, container_id: &str, network_name: &str) -> Result<()> { let mut path = Path::new(&self.config).join(network_name); if !path.exists() { path = Path::new(&self.config).join(network_name.to_owned() + "%int"); } let file_content = fs::read_to_string(&path)?; let lines: Vec<&str> = file_content.split_terminator('\n').collect(); let mut idx = 0; let mut file = File::create(&path)?; for line in lines { if line.contains(container_id) { continue; } file.write_all(line.as_bytes())?; file.write_all(b"\n")?; idx += 1; } // nothing left in file (only header), remove it if idx <= 1 { fs::remove_file(&path)? } Ok(()) } // Modifies network dns_servers for a specific network and notifies aardvark-dns server // with the change. // Note: If no aardvark dns config exists for a network function will return success without // doing anything, because `podman network update` is applicable for networks even when no // container is attached to it. pub fn modify_network_dns_servers( &self, network_name: &str, network_dns_servers: &[String], ) -> NetavarkResult<()> { let mut dns_servers_modified = false; let path = Path::new(&self.config).join(network_name); let file_content = match fs::read_to_string(&path) { Ok(content) => content, Err(error) => { if error.kind() == std::io::ErrorKind::NotFound { // Most likely `podman network update` was called // but no container on the network is running hence // no aardvark file is there in such case return success // since podman database still got updated and it will be // populated correctly for the next container. return Ok(()); } else { return Err(NetavarkError::Io(error)); } } }; let mut file = File::create(&path)?; //for line in lines { for (idx, line) in file_content.split_terminator('\n').enumerate() { if idx == 0 { // If this is first line, we have to modify this // first line has a format of `... ..` // We will read the first line and get the first column and // override the second column with new network dns servers. let network_parts = line.split(' ').collect::>(); if network_parts.is_empty() { return Err(NetavarkError::msg(format!( "invalid network configuration file: {}", path.display() ))); } let network_dns_servers_collected = if !network_dns_servers.is_empty() { dns_servers_modified = true; let dns_server_collected = network_dns_servers .iter() .map(|g| g.to_string()) .collect::>() .join(","); format!(" {dns_server_collected}") } else { "".to_string() }; // Modify line to support new format let content = format!("{}{}", network_parts[0], network_dns_servers_collected); file.write_all(content.as_bytes())?; } else { file.write_all(line.as_bytes())?; } file.write_all(b"\n")?; } // If dns servers were updated notify the aardvark-dns server // if refresh is needed. if dns_servers_modified { self.notify(false, true)?; } Ok(()) } pub fn delete_from_netavark_entries(&self, entries: &[AardvarkEntry]) -> NetavarkResult<()> { for entry in entries { self.delete_entry(entry.container_id, entry.network_name)?; } self.notify(false, false) } } containers-netavark-68510f4/src/dns/mod.rs000066400000000000000000000000221475240420400204430ustar00rootroot00000000000000pub mod aardvark; containers-netavark-68510f4/src/error/000077500000000000000000000000001475240420400176715ustar00rootroot00000000000000containers-netavark-68510f4/src/error/mod.rs000066400000000000000000000151471475240420400210260ustar00rootroot00000000000000use std::error::Error; use std::fmt; pub type NetavarkResult = Result; /// wrap any result into a NetavarkError and add the given msg #[macro_export] macro_rules! wrap { ($result:expr, $msg:expr) => { $result.map_err(|err| NetavarkError::wrap($msg, err.into())) }; } /// Contains a list of errors, this is useful for teardown operations since we /// should cleanup as much as possible before return all encountered errors. #[derive(Debug)] pub struct NetavarkErrorList(Vec); impl NetavarkErrorList { pub fn new() -> Self { Self(vec![]) } pub fn push(&mut self, err: NetavarkError) { match err { // make sure the flatten the error list, nested lists would just look ugly NetavarkError::List(mut list) => self.0.append(&mut list.0), err => self.0.push(err), } } pub fn is_empty(&self) -> bool { self.0.is_empty() } } // clippy wants the default implementation even if it is not needed impl Default for NetavarkErrorList { fn default() -> Self { Self::new() } } pub trait ErrorWrap { /// wrap NetavarkResult error into a NetavarkError and add the given msg fn wrap(self, msg: S) -> NetavarkResult where S: Into; } impl ErrorWrap for NetavarkResult { fn wrap(self, msg: S) -> NetavarkResult where S: Into, { self.map_err(|err| NetavarkError::wrap(msg, err)) } } /// The main Netavark error type #[derive(Debug)] pub enum NetavarkError { // A string message Message(String), // A string message that sets a specific exit code for Netavark ExitCode(String, i32), // A chain of multiple errors Chain(String, Box), Io(std::io::Error), Dbus(zbus::Error), DbusVariant(zbus::zvariant::Error), Sysctl(sysctl::SysctlError), Serde(serde_json::Error), Netlink(netlink_packet_core::error::ErrorMessage), DHCPProxy(tonic::Status), List(NetavarkErrorList), Nftables(nftables::helper::NftablesError), SubnetParse(ipnet::AddrParseError), AddrParse(std::net::AddrParseError), } /// Internal struct for JSON output #[derive(Debug, Serialize, Deserialize)] pub struct JsonError { pub error: String, } impl NetavarkError { pub fn msg(msg: S) -> NetavarkError where S: Into, { NetavarkError::Message(msg.into()) } pub fn wrap(msg: S, chained: NetavarkError) -> NetavarkError where S: Into, { NetavarkError::Chain(msg.into(), Box::new(chained)) } /// Print the error in a standardized JSON format recognized by callers of /// Netavark. pub fn print_json(&self) { let to_json = JsonError { error: self.to_string(), }; println!( "{}", serde_json::to_string(&to_json).unwrap_or(format!( "Failed to serialize error message: {}", to_json.error )) ); } /// Get the exit code that Netavark should exit with pub fn get_exit_code(&self) -> i32 { match *self { NetavarkError::ExitCode(_, i) => i, _ => 1, } } /// unwrap the chain error recursively until we a non chain type error pub fn unwrap(&self) -> &NetavarkError { match self { NetavarkError::Chain(_, inner) => inner.unwrap(), _ => self, } } } impl fmt::Display for NetavarkError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { NetavarkError::Message(s) => write!(f, "{s}"), NetavarkError::ExitCode(s, _) => write!(f, "{s}"), NetavarkError::Chain(s, e) => write!(f, "{s}: {e}"), NetavarkError::Io(e) => write!(f, "IO error: {e}"), NetavarkError::Dbus(e) => write!(f, "DBus error: {e}"), NetavarkError::DbusVariant(e) => write!(f, "DBus Variant Error: {e}"), NetavarkError::Sysctl(e) => write!(f, "Sysctl error: {e}"), NetavarkError::Serde(e) => write!(f, "JSON Decoding error: {e}"), NetavarkError::Netlink(e) => write!(f, "Netlink error: {e}"), NetavarkError::DHCPProxy(e) => write!(f, "dhcp proxy error: {e}"), NetavarkError::List(list) => { if list.0.len() == 1 { write!(f, "{}", list.0[0]) } else { write!(f, "netavark encountered multiple errors:")?; for e in &list.0 { write!(f, "\n\t- {e}")?; } Ok(()) } } NetavarkError::Nftables(e) => write!(f, "nftables error: {e}"), NetavarkError::SubnetParse(e) => write!(f, "parsing IP subnet error: {e}"), NetavarkError::AddrParse(e) => write!(f, "parsing IP address error: {e}"), } } } impl Error for NetavarkError {} impl From for NetavarkError { fn from(err: std::io::Error) -> NetavarkError { NetavarkError::Io(err) } } impl From for NetavarkError { fn from(err: zbus::Error) -> NetavarkError { NetavarkError::Dbus(err) } } impl From for NetavarkError { fn from(err: zbus::zvariant::Error) -> NetavarkError { NetavarkError::DbusVariant(err) } } impl From for NetavarkError { fn from(err: sysctl::SysctlError) -> NetavarkError { NetavarkError::Sysctl(err) } } impl From for NetavarkError { fn from(err: serde_json::Error) -> NetavarkError { NetavarkError::Serde(err) } } impl From for NetavarkError { fn from(e: ipnet::PrefixLenError) -> Self { NetavarkError::Message(format!("{e}")) } } impl From for NetavarkError { fn from(err: netlink_packet_core::error::ErrorMessage) -> Self { NetavarkError::Netlink(err) } } impl From for NetavarkError { fn from(err: tonic::Status) -> Self { NetavarkError::DHCPProxy(err) } } impl From for NetavarkError { fn from(err: nftables::helper::NftablesError) -> Self { NetavarkError::Nftables(err) } } impl From for NetavarkError { fn from(err: ipnet::AddrParseError) -> Self { NetavarkError::SubnetParse(err) } } impl From for NetavarkError { fn from(err: std::net::AddrParseError) -> Self { NetavarkError::AddrParse(err) } } containers-netavark-68510f4/src/firewall/000077500000000000000000000000001475240420400203455ustar00rootroot00000000000000containers-netavark-68510f4/src/firewall/firewalld.rs000066400000000000000000001166211475240420400226730ustar00rootroot00000000000000use crate::error::{NetavarkError, NetavarkResult}; use crate::network::internal_types; use crate::network::internal_types::{PortForwardConfig, TearDownNetwork, TeardownPortForward}; use crate::network::types::PortMapping; use crate::{firewall, wrap}; use core::convert::TryFrom; use log::{debug, info, warn}; use std::collections::HashMap; use std::net::IpAddr; use std::vec::Vec; use zbus::{ blocking::Connection, zvariant::{Array, OwnedValue, Signature, Value}, }; const ZONENAME: &str = "netavark_zone"; const POLICYNAME: &str = "netavark_policy"; const PORTPOLICYNAME: &str = "netavark_portfwd"; const HOSTFWDPOLICYNAME: &str = "netavark_host_fwd"; const HOSTTOZONEPOLICYNAME: &str = "netavark_zone_acc"; // Firewalld driver - uses a dbus connection to communicate with firewalld. pub struct FirewallD { conn: Connection, } pub fn new(conn: Connection) -> Result, NetavarkError> { Ok(Box::new(FirewallD { conn })) } impl firewall::FirewallDriver for FirewallD { fn driver_name(&self) -> &str { firewall::FIREWALLD } fn setup_network( &self, network_setup: internal_types::SetupNetwork, _dbus_con: &Option, ) -> NetavarkResult<()> { let mut need_reload = false; need_reload |= match create_zone_if_not_exist(&self.conn, ZONENAME) { Ok(b) => b, Err(e) => { return Err(NetavarkError::wrap( format!("Error creating zone {ZONENAME}"), e, )) } }; need_reload |= match add_policy_if_not_exist( &self.conn, POLICYNAME, ZONENAME, "ANY", "ACCEPT", true, None, ) { Ok(b) => b, Err(e) => { return Err(NetavarkError::wrap( format!("Error creating policy {POLICYNAME}"), e, )) } }; need_reload |= match add_policy_if_not_exist( &self.conn, PORTPOLICYNAME, "ANY", "HOST", "CONTINUE", false, None, ) { Ok(b) => b, Err(e) => { return Err(NetavarkError::wrap( format!("Error creating policy {PORTPOLICYNAME}"), e, )) } }; need_reload |= match add_policy_if_not_exist( &self.conn, HOSTFWDPOLICYNAME, "HOST", "ANY", "CONTINUE", false, None, ) { Ok(b) => b, Err(e) => { return Err(NetavarkError::wrap( format!("Error creating policy {HOSTFWDPOLICYNAME}"), e, )) } }; need_reload |= match add_policy_if_not_exist( &self.conn, HOSTTOZONEPOLICYNAME, "ANY", ZONENAME, "CONTINUE", true, None, ) { Ok(b) => b, Err(e) => { return Err(NetavarkError::wrap( format!("Error creating policy {HOSTTOZONEPOLICYNAME}"), e, )) } }; if need_reload { debug!("Reloading firewalld config to bring up zone and policy"); let _ = self.conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1", Some("org.fedoraproject.FirewallD1"), "reload", &(), )?; } // MUST come after the reload; otherwise the zone we made might not be // in the running config. if let Some(nets) = network_setup.subnets { match add_source_subnets_to_zone(&self.conn, ZONENAME, &nets) { Ok(_) => {} Err(e) => { return Err(NetavarkError::wrap( format!("Error adding source subnets to zone {ZONENAME}"), e, )) } }; } Ok(()) } fn teardown_network(&self, tear: TearDownNetwork) -> NetavarkResult<()> { if !tear.complete_teardown { return Ok(()); } if let Some(subnets) = tear.config.subnets { for subnet in subnets { debug!("Removing subnet {} from zone {}", subnet, ZONENAME); let _ = self.conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1", Some("org.fedoraproject.FirewallD1.zone"), "removeSource", &(ZONENAME, subnet.to_string()), )?; } } Ok(()) } fn setup_port_forward( &self, setup_portfw: PortForwardConfig, _dbus_con: &Option, ) -> Result<(), NetavarkError> { // NOTE: There is a serious TOCTOU risk in this function if netavark // is either run in parallel, or is not the only thing to edit this // policy. // Because of Podman's locking, this should be safe in the typical // case. // I don't think there's a safer way, unfortunately. let sig_ssss = match Signature::try_from("(ssss)") { Ok(s) => s, Err(e) => { return Err(NetavarkError::wrap( "Error creating signature for new DBus array", e.into(), )) } }; let sig_s = match Signature::try_from("s") { Ok(s) => s, Err(e) => { return Err(NetavarkError::wrap( "Error creating signature for new DBus array", e.into(), )) } }; let mut port_forwarding_rules: Array = Array::new(sig_ssss); let mut rich_rules: Array = Array::new(sig_s.clone()); let mut localhost_rich_rules: Array = Array::new(sig_s); // Create any necessary port forwarding rule(s) and add them to the // policy config we grabbed above. // Note that this does *absolutely no* conflict detection or // prevention - if two ports end up mapped to different containers, // that is not detected, and firewalld will allow it to happen. // Only one of them will win and be active, though. if let Some(ports) = setup_portfw.port_mappings { for port in ports { if !port.host_ip.is_empty() && port.host_ip != "0.0.0.0" && port.host_ip != "::" { // Have to special-case forwarding off localhost. if port.host_ip == "127.0.0.1" { if let Some(v4) = setup_portfw.container_ip_v4 { let rule = get_localhost_pf_rich_rule(port, &v4); debug!("Adding localhost pf rule: {}", rule); localhost_rich_rules.append(Value::new(rule))?; } continue; } else if port.host_ip == "::1" { continue; } // Need a rich rule for traffic with a non-wildcard host IP. let host_ip: IpAddr = match port.host_ip.parse() { Ok(i) => i, Err(_) => { return Err(NetavarkError::msg(format!( "invalid host ip \"{}\" provided for port {}", port.host_ip, port.host_port ))); } }; match host_ip { IpAddr::V4(_) => { if let Some(ctr_ip_v4) = setup_portfw.container_ip_v4 { let rule = get_port_forwarding_hostip_rich_rule(port, &ctr_ip_v4); rich_rules.append(Value::new(rule))?; } } IpAddr::V6(_) => { if let Some(ctr_ip_v6) = setup_portfw.container_ip_v6 { let rule = get_port_forwarding_hostip_rich_rule(port, &ctr_ip_v6); rich_rules.append(Value::new(rule))?; } } } } else { if let Some(v4) = setup_portfw.container_ip_v4 { if port.host_ip != "::" { port_forwarding_rules .append(Value::new(make_port_tuple(port, &v4.to_string())))?; let localhost_rule = get_localhost_pf_rich_rule(port, &v4); debug!("Adding localhost pf rule: {}", localhost_rule); localhost_rich_rules.append(Value::new(localhost_rule))?; } } if let Some(v6) = setup_portfw.container_ip_v6 { if port.host_ip != "0.0.0.0" { port_forwarding_rules .append(Value::new(make_port_tuple(port, &v6.to_string())))?; // No localhost rule. Kernel does not support localhost v6 DNAT. } } } } }; // dns port forwarding requires rich rules as we also want to match destination ip // only bother if configured dns port isn't 53 if setup_portfw.dns_port != 53 && !setup_portfw.dns_server_ips.is_empty() { for dns_ip in setup_portfw.dns_server_ips { let rule = get_dns_pf_rich_rule(dns_ip, setup_portfw.dns_port); rich_rules.append(Value::new(rule))?; } } // Only update if we actually generated rules if !port_forwarding_rules.is_empty() || !rich_rules.is_empty() { let policy_config = get_policy_config(&self.conn, PORTPOLICYNAME.to_string())?; // The updated config for the policy. Firewalld won't alter keys we // don't mention, so safest to make a new map with only the keys we // altered. let mut new_policy_config = HashMap::<&str, &Value>::new(); let mut pf_opt: Option = None; if !port_forwarding_rules.is_empty() { let final_port_forwarding_rules: Array = match policy_config.get("forward_ports") { Some(a) => match a.try_to_owned()?.into() { Value::Array(arr) => { let mut new_arr = arr.try_clone()?; for rule in port_forwarding_rules.iter() { new_arr.append(rule.try_clone()?)?; } new_arr } _ => { return Err(NetavarkError::msg( "forward_ports in firewalld policy object has a bad type", )) } }, None => port_forwarding_rules, }; pf_opt = Some(Value::new(final_port_forwarding_rules)); } if let Some(v) = &pf_opt { new_policy_config.insert("forward_ports", v); } let mut rich_rules_opt: Option = None; if !rich_rules.is_empty() { let final_rich_rules: Array = match policy_config.get("rich_rules") { Some(a) => match a.try_to_owned()?.into() { Value::Array(arr) => { let mut new_arr = arr.try_clone()?; for rule in rich_rules.iter() { new_arr.append(rule.try_clone()?)?; } new_arr } _ => { return Err(NetavarkError::msg( "rich_rules in firewalld policy object has a bad type", )) } }, None => rich_rules, }; rich_rules_opt = Some(Value::new(final_rich_rules)); } if let Some(v) = &rich_rules_opt { new_policy_config.insert("rich_rules", v); } // Send the new config back update_policy_config(&self.conn, PORTPOLICYNAME, new_policy_config)?; } // Same thing, but for our localhost forwarding policy if !localhost_rich_rules.is_empty() { let policy_config = get_policy_config(&self.conn, HOSTFWDPOLICYNAME.to_string())?; // The updated config for the policy. Firewalld won't alter keys we // don't mention, so safest to make a new map with only the keys we // altered. let final_rich_rules: Array = match policy_config.get("rich_rules") { Some(a) => match a.try_to_owned()?.into() { Value::Array(arr) => { let mut new_arr = arr.try_clone()?; for rule in localhost_rich_rules.iter() { new_arr.append(rule.try_clone()?)?; } new_arr } _ => { return Err(NetavarkError::msg( "rich_rules in firewalld policy object has a bad type", )) } }, None => localhost_rich_rules, }; let mut new_policy_config = HashMap::<&str, &Value>::new(); let value_rich_rules = Value::new(final_rich_rules); new_policy_config.insert("rich_rules", &value_rich_rules); update_policy_config(&self.conn, HOSTFWDPOLICYNAME, new_policy_config)?; } info!( "Successfully added port-forwarding rules for container {}", setup_portfw.container_id ); Ok(()) } fn teardown_port_forward(&self, teardown_pf: TeardownPortForward) -> NetavarkResult<()> { // Get the current configuration for the policy let policy_config = get_policy_config(&self.conn, PORTPOLICYNAME.to_string())?; let localhost_policy_config = get_policy_config(&self.conn, HOSTFWDPOLICYNAME.to_string())?; let old_port_forwarding_rules_option: Option = match policy_config.get("forward_ports") { Some(a) => match a.try_to_owned()?.into() { Value::Array(arr) => Some(arr.try_clone()?), _ => { return Err(NetavarkError::msg( "forward-port in firewalld policy object has a bad type", )); } }, None => { // No existing rules - skip None } }; let mut port_forwarding_rules_option: Option = None; if let Some(old_port_forwarding_rules) = old_port_forwarding_rules_option { let sig = match Signature::try_from("(ssss)") { Ok(s) => s, Err(e) => { return Err(NetavarkError::wrap( "Error creating signature for new dbus array", e.into(), )) } }; let mut port_forwarding_rules = Array::new(sig); let ipv4 = teardown_pf.config.container_ip_v4.map(|i| i.to_string()); let ipv6 = teardown_pf.config.container_ip_v6.map(|i| i.to_string()); // Iterate through old rules, remove anything with the IPv4 or IPv6 of // this container as the destination IP. for port_tuple in old_port_forwarding_rules.iter() { match port_tuple { Value::Structure(s) => { let fields = s.fields(); if fields.len() != 4 { return Err(NetavarkError::msg( "Port forwarding rule that was not a 4-tuple encountered", )); } let port_ip = match &fields[3] { Value::Str(s) => s.as_str().to_string(), _ => return Err(NetavarkError::msg("Port forwarding tuples must contain only strings, encountered a non-string object")), }; let mut is_match = false; if let Some(v4) = &ipv4 { debug!("Checking firewalld IP {} against our IP {}", port_ip, v4); if *v4 == port_ip { is_match = true; } } if let Some(v6) = &ipv6 { debug!("Checking firewalld IP {} against our IP {}", port_ip, v6); if *v6 == port_ip { is_match = true; } } if !is_match { port_forwarding_rules.append(port_tuple.try_clone()?)?; } } _ => { return Err(NetavarkError::msg( "Port forwarding rule that was not a structure encountered", )) } } } port_forwarding_rules_option = Some(port_forwarding_rules) } // iterate through rich rules to remove dns forwarding if this // is the last container of the network e.g. teardown complete // only bother if configured dns port isn't 53 let mut rich_rules_option: Option = None; let old_rich_rules_option: Option = match policy_config.get("rich_rules") { Some(a) => match a.try_to_owned()?.into() { Value::Array(arr) => Some(arr.try_clone()?), _ => { return Err(NetavarkError::msg( "rich_rules in firewalld policy object has a bad type", )) } }, None => None, }; if let Some(old_rich_rules) = old_rich_rules_option { let sig = match Signature::try_from("s") { Ok(s) => s, Err(e) => { return Err(NetavarkError::wrap( "Error creating signature for new dbus array", e.into(), )) } }; let mut new_rich_rules = Array::new(sig); let ipv4 = teardown_pf.config.container_ip_v4.map(|i| i.to_string()); let ipv6 = teardown_pf.config.container_ip_v6.map(|i| i.to_string()); // DNS rules: get a vector of all rules we'll want to remove. let mut dns_rules: Vec = Vec::new(); if teardown_pf.complete_teardown && teardown_pf.config.dns_port != 53 && !teardown_pf.config.dns_server_ips.is_empty() { for dns_ip in teardown_pf.config.dns_server_ips { dns_rules.push(get_dns_pf_rich_rule(dns_ip, teardown_pf.config.dns_port)) } } for rule in old_rich_rules.iter() { match rule { Value::Str(old_rule) => { let mut is_match = false; if dns_rules.contains(&old_rule.to_string()) { is_match = true; } // Remove any rule using our IPv4 or IPv6 as daddr. if let Some(v4) = &ipv4 { let daddr = format!("to-addr=\"{}\"", v4); debug!( "Checking if {} contains string {}", old_rule.to_string(), daddr ); if old_rule.to_string().contains(&daddr) { is_match = true; } } if let Some(v6) = &ipv6 { let daddr = format!("to-addr=\"{}\"", v6); if old_rule.to_string().contains(&daddr) { is_match = true; } } if !is_match { new_rich_rules.append(rule.try_clone()?)?; } } _ => { return Err(NetavarkError::msg( "Rich rule that was not a string encountered", )) } } } rich_rules_option = Some(new_rich_rules); } // Now handle the localhost forwarding policy. let mut localhost_rich_rules_option: Option = None; let old_localhost_rich_rules_option: Option = match localhost_policy_config.get("rich_rules") { Some(a) => match a.try_to_owned()?.into() { Value::Array(arr) => Some(arr.try_clone()?), _ => { return Err(NetavarkError::msg( "rich_rules in firewalld localhost policy object has a bad type", )) } }, None => None, }; if let Some(old_localhost_rich_rules) = old_localhost_rich_rules_option { let sig = match Signature::try_from("s") { Ok(s) => s, Err(e) => { return Err(NetavarkError::wrap( "Error creating signature for new dbus array", e.into(), )) } }; let mut new_rich_rules = Array::new(sig); let ipv4 = teardown_pf.config.container_ip_v4.map(|i| i.to_string()); for rule in old_localhost_rich_rules.iter() { match rule { Value::Str(old_rule) => { let mut is_match = false; // Remove any rule using our IPv4 as daddr. // We don't do IPv6 localhost forwarding. if let Some(v4) = &ipv4 { let daddr = format!("to-addr=\"{}\"", v4); debug!( "Checking if {} contains string {}", old_rule.to_string(), daddr ); if old_rule.to_string().contains(&daddr) { is_match = true; } } if !is_match { new_rich_rules.append(rule.try_clone()?)?; } } _ => { return Err(NetavarkError::msg( "Rich rule that was not a string encountered", )) } } } localhost_rich_rules_option = Some(new_rich_rules); } // Firewalld won't alter keys we don't mention, so make a new config // map - with only the changes to ports. let mut new_policy_config = HashMap::<&str, &Value>::new(); let new_pf_rules = port_forwarding_rules_option.map(Value::new); if let Some(pf) = &new_pf_rules { new_policy_config.insert("forward_ports", pf); } let new_rich_rules = rich_rules_option.map(Value::new); if let Some(rich) = &new_rich_rules { new_policy_config.insert("rich_rules", rich); } // Send the updated configuration back to firewalld. update_policy_config(&self.conn, PORTPOLICYNAME, new_policy_config)?; // And again for the localhost policy let mut new_localhost_policy_config = HashMap::<&str, &Value>::new(); let new_localhost_rich_rules = localhost_rich_rules_option.map(Value::new); if let Some(rich) = &new_localhost_rich_rules { new_localhost_policy_config.insert("rich_rules", rich); } update_policy_config(&self.conn, HOSTFWDPOLICYNAME, new_localhost_policy_config)?; Ok(()) } } /// Create a firewalld zone to hold all our interfaces. fn create_zone_if_not_exist(conn: &Connection, zone_name: &str) -> NetavarkResult { debug!("Creating firewall zone {}", zone_name); // First, double-check if the zone exists in the running config. let zones_msg = conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1", Some("org.fedoraproject.FirewallD1.zone"), "getZones", &(), )?; let body = zones_msg.body(); let zones: Vec<&str> = wrap!( body.deserialize(), format!("Error decoding DBus message for active zones") )?; for &zone in zones.iter() { if zone == zone_name { debug!("Zone exists and is running"); return Ok(false); } } // Zone is not in running config - check permanent config. let perm_zones_msg = conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1/config", Some("org.fedoraproject.FirewallD1.config"), "getZoneNames", &(), )?; let body = perm_zones_msg.body(); let zones_perm: Vec<&str> = body.deserialize().map_err(|e| { NetavarkError::wrap("Error decoding DBus message for permanent zones", e.into()) })?; for &zone in zones_perm.iter() { if zone == zone_name { debug!("Zone exists and is not running"); return Ok(true); } } // We can probably avoid the permanent zones check about if we create // unconditionally and parse error strings to look for "duplicate name" // errors - but I really don't want to deal with matching error strings and // the complexities that could entail. // TODO: We can add a description to the zone, should do that. let _ = conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1/config", Some("org.fedoraproject.FirewallD1.config"), "addZone2", &(zone_name, HashMap::<&str, &Value>::new()), )?; Ok(true) } /// Add source subnets to the zone. pub fn add_source_subnets_to_zone( conn: &Connection, zone_name: &str, subnets: &[ipnet::IpNet], ) -> NetavarkResult<()> { for net in subnets { // Check if subnet already exists in zone let subnet_zone = conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1", Some("org.fedoraproject.FirewallD1.zone"), "getZoneOfSource", &(net.to_string()), )?; let body = subnet_zone.body(); let zone_string: String = wrap!( body.deserialize(), "Error decoding DBus message for zone of subnet" )?; if zone_string == zone_name { debug!("Subnet {} already exists in zone {}", net, zone_name); return Ok(()); } debug!("Adding subnet {} to zone {} as source", net, zone_name); let _ = conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1", Some("org.fedoraproject.FirewallD1.zone"), "changeZoneOfSource", &(zone_name, net.to_string()), )?; } Ok(()) } /// Add a policy object for the zone to handle masquerading. fn add_policy_if_not_exist( conn: &Connection, policy_name: &str, ingress_zone_name: &str, egress_zone_name: &str, target: &str, masquerade: bool, priority: Option, ) -> NetavarkResult { debug!( "Adding firewalld policy {} (ingress zone {}, egress zone {})", policy_name, ingress_zone_name, egress_zone_name ); // Does policy exist in running policies? let policies_msg = conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1", Some("org.fedoraproject.FirewallD1.policy"), "getPolicies", &(), )?; let policies_body = policies_msg.body(); let policies: Vec<&str> = wrap!( policies_body.deserialize(), "Error decoding policy list response" )?; for &policy in policies.iter() { if policy == policy_name { debug!("Policy exists and is running"); return Ok(false); } } // Does the policy exist in permanent policies? let perm_policies_msg = conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1/config", Some("org.fedoraproject.FirewallD1.config"), "getPolicyNames", &(), )?; let perm_policies_body = perm_policies_msg.body(); let perm_policies: Vec<&str> = wrap!( perm_policies_body.deserialize(), "Error decoding permanent policy list response" )?; for &policy in perm_policies.iter() { if policy == policy_name { debug!("Policy exists and is not running"); return Ok(true); } } // Options for the new policy let mut policy_opts = HashMap::<&str, &Value>::new(); let egress_zones = Value::new(Array::from(vec![egress_zone_name])); let ingress_zones = Value::new(Array::from(vec![ingress_zone_name])); let priority_val = priority.map(Value::new); if let Some(prio) = &priority_val { policy_opts.insert("priority", prio); } policy_opts.insert("egress_zones", &egress_zones); policy_opts.insert("ingress_zones", &ingress_zones); let masquerade_bool = Value::new(true); if masquerade { policy_opts.insert("masquerade", &masquerade_bool); } let target = Value::new(target); policy_opts.insert("target", &target); // Policy does not exist, create it. // Returns object path, which we don't need. let _ = conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1/config", Some("org.fedoraproject.FirewallD1.config"), "addPolicy", &(policy_name, &policy_opts), )?; Ok(true) } /// Make a port-forward tuple for firewalld /// Port forward rules are a 4-tuple of: /// (port, protocol, to-port, to-addr) /// Port, to-port can be ranges (separated via hyphen) /// Also accepts IP address to forward to. fn make_port_tuple(port: &PortMapping, addr: &str) -> (String, String, String, String) { if port.range > 1 { // Subtract 1 as these are 1-indexed strings - range of 2 is 1000-1001 let end_host_range = port.host_port + port.range - 1; let end_ctr_range = port.container_port + port.range - 1; ( format!("{}-{}", port.host_port, end_host_range), port.protocol.clone(), format!("{}-{}", port.container_port, end_ctr_range), addr.to_string(), ) } else { let to_return = ( port.host_port.to_string(), port.protocol.clone(), port.container_port.to_string(), addr.to_string(), ); debug!("Port is {:?}", to_return); to_return } } /// Get the configuration of the given policy. fn get_policy_config( conn: &Connection, policy_name: String, ) -> NetavarkResult> { let policy_config_msg = conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1", Some("org.fedoraproject.FirewallD1.policy"), "getPolicySettings", &policy_name, )?; let mut policy_config: HashMap = HashMap::new(); match policy_config_msg .body() .deserialize::>() { Ok(m) => { for (k, v) in m { policy_config.insert(k.to_string(), v.try_to_owned()?); } } Err(e) => { return Err(NetavarkError::wrap( format!("Error decoding DBus message for policy {policy_name} configuration"), e.into(), )) } }; Ok(policy_config) } /// Update a policy config object fn update_policy_config( conn: &Connection, policy_name: &str, new_config: HashMap<&str, &Value>, ) -> NetavarkResult<()> { match conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1", Some("org.fedoraproject.FirewallD1.policy"), "setPolicySettings", &(policy_name, new_config), ) { Ok(_) => {} Err(e) => { return Err(NetavarkError::wrap( format!( "Failed to update firewalld policy {} port forwarding rules", policy_name ), e.into(), )) } }; Ok(()) } /// Get a rich rule to handle DNS port forwarding. fn get_dns_pf_rich_rule(dns_ip: &IpAddr, dns_port: u16) -> String { let ip_family = get_rich_rule_ip_family(dns_ip); get_pf_rich_rule( &ip_family, &dns_ip.to_string(), "53", "udp", &dns_port.to_string(), &dns_ip.to_string(), ) } /// Get a rich rule to handle port forwarding to a specific IP. fn get_port_forwarding_hostip_rich_rule(port: &PortMapping, ctr_ip: &IpAddr) -> String { let ip_family = get_rich_rule_ip_family(ctr_ip); let host_port = get_rich_rule_port(port.host_port, port.range); let ctr_port = get_rich_rule_port(port.container_port, port.range); get_pf_rich_rule( &ip_family, &port.host_ip, &host_port, &port.protocol, &ctr_port, &ctr_ip.to_string(), ) } /// Get a localhost port forwarding rich rule. IPv4 only. fn get_localhost_pf_rich_rule(port: &PortMapping, ctr_ip: &IpAddr) -> String { let host_port = get_rich_rule_port(port.host_port, port.range); let ctr_port = get_rich_rule_port(port.container_port, port.range); get_pf_rich_rule( "ipv4", "127.0.0.1", &host_port, &port.protocol, &ctr_port, &ctr_ip.to_string(), ) } /// Get a port string for a rich rule fn get_rich_rule_port(port: u16, range: u16) -> String { if range > 1 { format!("{}-{}", port, (port + range - 1)) } else { port.to_string() } } /// Get appropriate address family for an IP address fn get_rich_rule_ip_family(ip: &IpAddr) -> String { if ip.is_ipv6() { "ipv6".to_string() } else { "ipv4".to_string() } } /// Get a port forwarding rich rule. fn get_pf_rich_rule( ip_family: &str, host_ip: &str, host_port: &str, protocol: &str, ctr_port: &str, ctr_ip: &str, ) -> String { format!("rule family=\"{}\" destination address=\"{}\" forward-port port=\"{}\" protocol=\"{}\" to-port=\"{}\" to-addr=\"{}\"", ip_family, host_ip, host_port, protocol, ctr_port, ctr_ip) } /// Check if firewalld is running. /// Not used within the firewalld driver, but by other drivers that may need to /// interact with firewalld. pub fn is_firewalld_running(conn: &Connection) -> bool { conn.call_method( Some("org.freedesktop.DBus"), "/org/freedesktop/DBus", Some("org.freedesktop.DBus"), "GetNameOwner", &"org.fedoraproject.FirewallD1", ) .is_ok() } /// If possible, add a firewalld rule to allow traffic. /// Ignore all errors, beyond possibly logging them. /// Not used within the firewalld driver, but by other drivers that may need to /// interact with firewalld. pub fn add_firewalld_if_possible(dbus_conn: &Option, net: &ipnet::IpNet) { let conn = match dbus_conn { Some(conn) => conn, None => return, }; if !is_firewalld_running(conn) { return; } debug!("Adding firewalld rules for network {}", net.to_string()); match add_source_subnets_to_zone(conn, "trusted", &[*net]) { Ok(_) => {} Err(e) => warn!( "Error adding subnet {} from firewalld trusted zone: {}", net.to_string(), e ), } } /// If possible, remove a firewalld rule to allow traffic. /// Ignore all errors, beyond possibly logging them. /// Not used within the firewalld driver, but by other drivers that may need to /// interact with firewalld. pub fn rm_firewalld_if_possible(net: &ipnet::IpNet) { let conn = match Connection::system() { Ok(conn) => conn, Err(_) => return, }; if !is_firewalld_running(&conn) { return; } debug!("Removing firewalld rules for IPs {}", net.to_string()); match conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1", Some("org.fedoraproject.FirewallD1.zone"), "removeSource", &("trusted", net.to_string()), ) { Ok(_) => {} Err(e) => warn!( "Error removing subnet {} from firewalld trusted zone: {}", net.to_string(), e ), }; } /// Check whether firewalld's StrictForwardPorts setting is enabled. /// Returns false if firewalld is not installed, not running, or there is any /// error with the process. pub fn is_firewalld_strict_forward_enabled(dbus_con: &Option) -> bool { let conn = match dbus_con { Some(conn) => conn, None => return false, }; if !is_firewalld_running(conn) { return false; } // Fetch current running config match conn.call_method( Some("org.fedoraproject.FirewallD1"), "/org/fedoraproject/FirewallD1/config", Some("org.freedesktop.DBus.Properties"), "Get", &("org.fedoraproject.FirewallD1.config", "StrictForwardPorts"), ) { Ok(b) => b.body().deserialize().unwrap_or(false), Err(_) => { // Assume any error is related to the property not existing // (As it will not on older firewalld versions) // Return false given that. false } } } /// Check if firewalld's StrictForwardPorts setting is enabled and, if so, /// whether the container has requested any ports be forwarded. If both are true /// return a helpful error that port forwarding cannot be performed. pub fn check_can_forward_ports( dbus_conn: &Option, setup_portfw: &PortForwardConfig, ) -> NetavarkResult<()> { if is_firewalld_strict_forward_enabled(dbus_conn) { let mut portfw_used = setup_portfw.dns_port != 53; if let Some(ports) = setup_portfw.port_mappings { portfw_used = portfw_used || !ports.is_empty(); } if portfw_used { return Err(NetavarkError::msg( "Port forwarding not possible as firewalld StrictForwardPorts enabled", )); } } Ok(()) } containers-netavark-68510f4/src/firewall/fwnone.rs000066400000000000000000000022061475240420400222070ustar00rootroot00000000000000use crate::firewall; use crate::firewall::NetavarkResult; use crate::network::internal_types::{ PortForwardConfig, SetupNetwork, TearDownNetwork, TeardownPortForward, }; // Iptables driver - uses direct iptables commands via the iptables crate. pub struct Fwnone {} pub fn new() -> NetavarkResult> { Ok(Box::new(Fwnone {})) } impl firewall::FirewallDriver for Fwnone { fn driver_name(&self) -> &str { firewall::NONE } fn setup_network( &self, _network_setup: SetupNetwork, _dbus_conn: &Option, ) -> NetavarkResult<()> { Ok(()) } // teardown_network should only be called in the case of // a complete teardown. fn teardown_network(&self, _tear: TearDownNetwork) -> NetavarkResult<()> { Ok(()) } fn setup_port_forward( &self, _setup_portfw: PortForwardConfig, _dbus_conn: &Option, ) -> NetavarkResult<()> { Ok(()) } fn teardown_port_forward(&self, _tear: TeardownPortForward) -> NetavarkResult<()> { Ok(()) } } containers-netavark-68510f4/src/firewall/iptables.rs000066400000000000000000000167121475240420400225250ustar00rootroot00000000000000use crate::error::{NetavarkError, NetavarkResult}; use crate::firewall; use crate::firewall::firewalld; use crate::firewall::varktables::types::TeardownPolicy::OnComplete; use crate::firewall::varktables::types::{ create_network_chains, get_network_chains, get_port_forwarding_chains, TeardownPolicy, }; use crate::network::internal_types::{ PortForwardConfig, SetupNetwork, TearDownNetwork, TeardownPortForward, }; use iptables; use iptables::IPTables; pub(crate) const MAX_HASH_SIZE: usize = 13; // Iptables driver - uses direct iptables commands via the iptables crate. pub struct IptablesDriver { conn: IPTables, conn6: IPTables, } pub fn new() -> NetavarkResult> { // create an iptables connection let ipt = match iptables::new(false) { Ok(i) => i, Err(e) => return Err(NetavarkError::Message(format!("iptables: {e}"))), }; let ipt6 = match iptables::new(true) { Ok(i) => i, Err(e) => return Err(NetavarkError::Message(format!("ip6tables: {e}"))), }; let driver = IptablesDriver { conn: ipt, conn6: ipt6, }; Ok(Box::new(driver)) } impl firewall::FirewallDriver for IptablesDriver { fn driver_name(&self) -> &str { firewall::IPTABLES } fn setup_network( &self, network_setup: SetupNetwork, dbus_con: &Option, ) -> NetavarkResult<()> { if let Some(subnet) = network_setup.subnets { for network in subnet { let is_ipv6 = network.network().is_ipv6(); let mut conn = &self.conn; if is_ipv6 { conn = &self.conn6; } let chains = get_network_chains( conn, network, &network_setup.network_hash_name, is_ipv6, network_setup.bridge_name.clone(), network_setup.isolation, network_setup.dns_port, ); create_network_chains(chains)?; firewalld::add_firewalld_if_possible(dbus_con, &network); } } Ok(()) } // teardown_network should only be called in the case of // a complete teardown. fn teardown_network(&self, tear: TearDownNetwork) -> NetavarkResult<()> { // Remove network specific general NAT rules if let Some(subnet) = tear.config.subnets { for network in subnet { let is_ipv6 = network.network().is_ipv6(); let mut conn = &self.conn; if is_ipv6 { conn = &self.conn6; } let chains = get_network_chains( conn, network, &tear.config.network_hash_name, is_ipv6, tear.config.bridge_name.clone(), tear.config.isolation, tear.config.dns_port, ); for c in &chains { c.remove_rules(tear.complete_teardown)?; } for c in chains { match &c.td_policy { None => {} Some(policy) => { if tear.complete_teardown && *policy == OnComplete { c.remove()?; } } } } if tear.complete_teardown { firewalld::rm_firewalld_if_possible(&network) } } } Result::Ok(()) } fn setup_port_forward( &self, setup_portfw: PortForwardConfig, dbus_con: &Option, ) -> NetavarkResult<()> { firewalld::check_can_forward_ports(dbus_con, &setup_portfw)?; if let Some(v4) = setup_portfw.container_ip_v4 { let subnet_v4 = match setup_portfw.subnet_v4 { Some(s) => s, None => { return Err(std::io::Error::new( std::io::ErrorKind::Other, "ipv4 address but provided but no v4 subnet provided", ) .into()) } }; let chains = get_port_forwarding_chains(&self.conn, &setup_portfw, &v4, &subnet_v4, false)?; create_network_chains(chains)?; } if let Some(v6) = setup_portfw.container_ip_v6 { let subnet_v6 = match setup_portfw.subnet_v6 { Some(s) => s, None => { return Err(std::io::Error::new( std::io::ErrorKind::Other, "ipv6 address but provided but no v6 subnet provided", ) .into()) } }; let chains = get_port_forwarding_chains(&self.conn6, &setup_portfw, &v6, &subnet_v6, true)?; create_network_chains(chains)?; }; Result::Ok(()) } fn teardown_port_forward(&self, tear: TeardownPortForward) -> NetavarkResult<()> { if let Some(v4) = tear.config.container_ip_v4 { let subnet_v4 = match tear.config.subnet_v4 { Some(s) => s, None => { return Err(std::io::Error::new( std::io::ErrorKind::Other, "ipv4 address but provided but no v4 subnet provided", ) .into()) } }; let chains = get_port_forwarding_chains(&self.conn, &tear.config, &v4, &subnet_v4, false)?; for chain in &chains { chain.remove_rules(tear.complete_teardown)?; } for chain in &chains { if !tear.complete_teardown || !chain.create { continue; } match &chain.td_policy { None => {} Some(policy) => { if *policy == TeardownPolicy::OnComplete { chain.remove()?; } } } } } if let Some(v6) = tear.config.container_ip_v6 { let subnet_v6 = match tear.config.subnet_v6 { Some(s) => s, None => { return Err(std::io::Error::new( std::io::ErrorKind::Other, "ipv6 address but provided but no v6 subnet provided", ) .into()) } }; let chains = get_port_forwarding_chains(&self.conn6, &tear.config, &v6, &subnet_v6, true)?; for chain in &chains { chain.remove_rules(tear.complete_teardown)?; } for chain in &chains { if !tear.complete_teardown || !chain.create { continue; } match &chain.td_policy { None => {} Some(policy) => { if *policy == TeardownPolicy::OnComplete { chain.remove()?; } } } } } Result::Ok(()) } } containers-netavark-68510f4/src/firewall/mod.rs000066400000000000000000000106501475240420400214740ustar00rootroot00000000000000use crate::error::{NetavarkError, NetavarkResult}; use crate::network::internal_types::{ PortForwardConfig, SetupNetwork, TearDownNetwork, TeardownPortForward, }; use log::{debug, info}; use zbus::blocking::Connection; pub mod firewalld; pub mod fwnone; pub mod iptables; pub mod nft; pub mod state; mod varktables; const IPTABLES: &str = "iptables"; const FIREWALLD: &str = "firewalld"; const NFTABLES: &str = "nftables"; const NONE: &str = "none"; /// Firewall drivers have the ability to set up per-network firewall forwarding /// and port mappings. pub trait FirewallDriver { /// Set up firewall rules for the given network, fn setup_network( &self, network_setup: SetupNetwork, dbus_con: &Option, ) -> NetavarkResult<()>; /// Tear down firewall rules for the given network. fn teardown_network(&self, tear: TearDownNetwork) -> NetavarkResult<()>; /// Set up port-forwarding firewall rules for a given container. fn setup_port_forward( &self, setup_pw: PortForwardConfig, dbus_con: &Option, ) -> NetavarkResult<()>; /// Tear down port-forwarding firewall rules for a single container. fn teardown_port_forward(&self, teardown_pf: TeardownPortForward) -> NetavarkResult<()>; /// Return the name of the driver. fn driver_name(&self) -> &str; } /// Types of firewall backend enum FirewallImpl { Iptables, Firewalld(Connection), Nftables, Fwnone, } /// What firewall implementations does this system support? fn get_firewall_impl(driver_name: Option) -> NetavarkResult { // It respects "firewalld", "iptables", "nftables", "none". if let Some(driver) = driver_name { debug!("Forcibly using firewall driver {driver}"); match driver.to_lowercase().as_str() { FIREWALLD => { let conn = match Connection::system() { Ok(c) => c, Err(e) => { return Err(NetavarkError::wrap( "Error retrieving dbus connection for requested firewall backend", e.into(), )) } }; return Ok(FirewallImpl::Firewalld(conn)); } IPTABLES => return Ok(FirewallImpl::Iptables), NFTABLES => return Ok(FirewallImpl::Nftables), NONE => return Ok(FirewallImpl::Fwnone), any => { return Err(NetavarkError::Message(format!( "Must provide a valid firewall backend, got {any}" ))) } } } get_default_fw_impl() // Is firewalld running? // let conn = match Connection::system() { // Ok(conn) => conn, // Err(_) => return FirewallImpl::Iptables, // }; // match conn.call_method( // Some("org.freedesktop.DBus"), // "/org/freedesktop/DBus", // Some("org.freedesktop.DBus"), // "GetNameOwner", // &"org.fedoraproject.FirewallD1", // ) { // Ok(_) => FirewallImpl::Firewalld(conn), // Err(_) => FirewallImpl::Iptables, // } } #[cfg(default_fw = "nftables")] fn get_default_fw_impl() -> NetavarkResult { Ok(FirewallImpl::Nftables) } #[cfg(default_fw = "iptables")] fn get_default_fw_impl() -> NetavarkResult { Ok(FirewallImpl::Iptables) } #[cfg(default_fw = "none")] fn get_default_fw_impl() -> NetavarkResult { Ok(FirewallImpl::Fwnone) } /// Get the preferred firewall implementation for the current system /// configuration. pub fn get_supported_firewall_driver( driver_name: Option, ) -> NetavarkResult> { match get_firewall_impl(driver_name) { Ok(fw) => match fw { FirewallImpl::Iptables => { info!("Using iptables firewall driver"); iptables::new() } FirewallImpl::Firewalld(conn) => { info!("Using firewalld firewall driver"); firewalld::new(conn) } FirewallImpl::Nftables => { info!("Using nftables firewall driver"); nft::new() } FirewallImpl::Fwnone => { info!("Not using firewall"); fwnone::new() } }, Err(e) => Err(e), } } containers-netavark-68510f4/src/firewall/nft.rs000066400000000000000000001401231475240420400215030ustar00rootroot00000000000000use crate::error::{NetavarkError, NetavarkResult}; use crate::firewall; use crate::firewall::firewalld; use crate::network::internal_types; use crate::network::internal_types::IsolateOption; use crate::network::types::PortMapping; use ipnet::IpNet; use nftables::batch::Batch; use nftables::expr; use nftables::helper::{self}; use nftables::schema; use nftables::stmt; use nftables::types; use std::collections::HashSet; use std::net::{IpAddr, Ipv4Addr}; const TABLENAME: &str = "netavark"; const INPUTCHAIN: &str = "INPUT"; const FORWARDCHAIN: &str = "FORWARD"; const POSTROUTINGCHAIN: &str = "POSTROUTING"; const PREROUTINGCHAIN: &str = "PREROUTING"; const OUTPUTCHAIN: &str = "OUTPUT"; const DNATCHAIN: &str = "NETAVARK-HOSTPORT-DNAT"; const MASKCHAIN: &str = "NETAVARK-HOSTPORT-SETMARK"; const ISOLATION1CHAIN: &str = "NETAVARK-ISOLATION-1"; const ISOLATION2CHAIN: &str = "NETAVARK-ISOLATION-2"; const ISOLATION3CHAIN: &str = "NETAVARK-ISOLATION-3"; const MASK: u32 = 0x2000; /// The dnat priority for chains /// This (and the below) are based on https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks#Priority_within_hook const DNATPRIO: i32 = -100; /// The srcnat priority for chains const SRCNATPRIO: i32 = 100; /// The filter priority for chains const FILTERPRIO: i32 = 0; const IPV4_LOCALHOST: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); pub struct Nftables {} pub fn new() -> Result, NetavarkError> { Ok(Box::new(Nftables {})) } impl firewall::FirewallDriver for Nftables { fn driver_name(&self) -> &str { firewall::NFTABLES } fn setup_network( &self, network_setup: internal_types::SetupNetwork, dbus_conn: &Option, ) -> NetavarkResult<()> { let mut batch = Batch::new(); // Overall table batch.add(schema::NfListObject::Table(schema::Table { family: types::NfFamily::INet, name: TABLENAME.to_string(), ..schema::Table::default() })); // Five default chains, one for each hook we have to monitor batch.add(make_complex_chain( INPUTCHAIN, types::NfChainType::Filter, types::NfHook::Input, FILTERPRIO, )); batch.add(make_complex_chain( FORWARDCHAIN, types::NfChainType::Filter, types::NfHook::Forward, FILTERPRIO, )); batch.add(make_complex_chain( POSTROUTINGCHAIN, types::NfChainType::NAT, types::NfHook::Postrouting, SRCNATPRIO, )); batch.add(make_complex_chain( PREROUTINGCHAIN, types::NfChainType::NAT, types::NfHook::Prerouting, DNATPRIO, )); batch.add(make_complex_chain( OUTPUTCHAIN, types::NfChainType::NAT, types::NfHook::Output, DNATPRIO, )); // dnat rules. Not used here, but need to be created first, because they have rules that must be first in their chains. // A lot of these are thus conditional on if the rule already exists or not. let existing_rules = get_netavark_rules()?; // Two extra chains, not hooked to anything, for our NAT pf rules batch.add(make_basic_chain(DNATCHAIN)); batch.add(make_basic_chain(MASKCHAIN)); // Three extra chains, not hooked to anything, for isolation. batch.add(make_basic_chain(ISOLATION1CHAIN)); batch.add(make_basic_chain(ISOLATION2CHAIN)); batch.add(make_basic_chain(ISOLATION3CHAIN)); // Postrouting chain needs a single rule to masquerade if mask is set. // But only one copy of that rule. So check if such a rule exists. let match_meta_masq = |r: &schema::Rule| -> bool { // Match on any rule that matches against 0x2000 for statement in &r.expr { match statement { stmt::Statement::Match(m) => match &m.right { expr::Expression::Number(n) => { if *n == MASK { return true; } } _ => continue, }, _ => continue, } } false }; if get_matching_rules_in_chain(&existing_rules, POSTROUTINGCHAIN, match_meta_masq) .is_empty() { // Postrouting: meta mark & 0x2000 == 0x2000 masquerade batch.add(make_rule( POSTROUTINGCHAIN, vec![ stmt::Statement::Match(stmt::Match { left: expr::Expression::BinaryOperation(expr::BinaryOperation::AND( Box::new(expr::Expression::Named(expr::NamedExpression::Meta( expr::Meta { key: expr::MetaKey::Mark, }, ))), Box::new(expr::Expression::Number(MASK)), )), right: expr::Expression::Number(MASK), op: stmt::Operator::EQ, }), stmt::Statement::Masquerade(None), ], )); } // Mask chain needs a single rule to apply the mask. // But only one copy of that rule. So check if such a rule exists. let match_meta_mark = |r: &schema::Rule| -> bool { // Match on any mangle rule. for statement in &r.expr { match statement { stmt::Statement::Mangle(_) => return true, _ => continue, } } false }; if get_matching_rules_in_chain(&existing_rules, MASKCHAIN, match_meta_mark).is_empty() { // Mask chain: mark or 0x2000 batch.add(make_rule( MASKCHAIN, vec![stmt::Statement::Mangle(stmt::Mangle { key: expr::Expression::Named(expr::NamedExpression::Meta(expr::Meta { key: expr::MetaKey::Mark, })), value: expr::Expression::BinaryOperation(expr::BinaryOperation::OR( Box::new(expr::Expression::Named(expr::NamedExpression::Meta( expr::Meta { key: expr::MetaKey::Mark, }, ))), Box::new(expr::Expression::Number(MASK)), )), })], )); } // We need rules in Prerouting and Output pointing to our dnat chain. // But only if they do not exist. let match_jump_dnat = get_rule_matcher_jump_to(DNATCHAIN.to_string()); // Prerouting: fib daddr type local jump // Output: fib daddr type local jump let mut rules_hash: HashSet = HashSet::new(); rules_hash.insert(expr::FibFlag::Daddr); let base_conditions: Vec = vec![ stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::Fib(expr::Fib { result: expr::FibResult::Type, flags: rules_hash, })), right: expr::Expression::String("local".to_string()), op: stmt::Operator::EQ, }), get_jump_action(DNATCHAIN), ]; if get_matching_rules_in_chain(&existing_rules, PREROUTINGCHAIN, &match_jump_dnat) .is_empty() { batch.add(make_rule(PREROUTINGCHAIN, base_conditions.clone())); } if get_matching_rules_in_chain(&existing_rules, OUTPUTCHAIN, &match_jump_dnat).is_empty() { batch.add(make_rule(OUTPUTCHAIN, base_conditions.clone())); } // Forward chain: ct state invalid drop let match_deny = |r: &schema::Rule| -> bool { for statement in &r.expr { match statement { stmt::Statement::Drop(_) => return true, _ => continue, } } false }; if get_matching_rules_in_chain(&existing_rules, FORWARDCHAIN, match_deny).is_empty() { batch.add(make_rule( FORWARDCHAIN, vec![ stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::CT(expr::CT { key: "state".to_string(), family: None, dir: None, })), right: expr::Expression::String("invalid".to_string()), op: stmt::Operator::IN, }), stmt::Statement::Drop(None), ], )); } // Forward chain: jump NETAVARK-ISOLATION-1 if get_matching_rules_in_chain( &existing_rules, FORWARDCHAIN, get_rule_matcher_jump_to(ISOLATION1CHAIN.to_string()), ) .is_empty() { batch.add(make_rule( FORWARDCHAIN, vec![get_jump_action(ISOLATION1CHAIN)], )); } let match_our_bridge = get_rule_matcher_bridge(&network_setup.bridge_name); // If and only if isolation is enabled: add isolation chains. // Some isolation rules are shared. Other rules are specific to one type // of isolation. if let IsolateOption::Normal | IsolateOption::Strict = network_setup.isolation { // NETAVARK-ISOLATION-1: iifname oifname != jump NETAVARK-ISOLATION-{2,3} // (Exact target varies based on Strict vs Normal Isolation - strict goes to 3, otherwise 2) let isolation_1_jump_target = if let IsolateOption::Strict = network_setup.isolation { ISOLATION3CHAIN } else { ISOLATION2CHAIN }; if get_matching_rules_in_chain(&existing_rules, ISOLATION1CHAIN, &match_our_bridge) .is_empty() { batch.add(make_rule( ISOLATION1CHAIN, vec![ stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::Meta( expr::Meta { key: expr::MetaKey::Iifname, }, )), right: expr::Expression::String(network_setup.bridge_name.clone()), op: stmt::Operator::EQ, }), stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::Meta( expr::Meta { key: expr::MetaKey::Oifname, }, )), right: expr::Expression::String(network_setup.bridge_name.clone()), op: stmt::Operator::NEQ, }), get_jump_action(isolation_1_jump_target), ], )); } // NETAVARK-ISOLATION-2: oifname == drop if get_matching_rules_in_chain(&existing_rules, ISOLATION2CHAIN, match_our_bridge) .is_empty() { batch.add(make_rule( ISOLATION2CHAIN, vec![ get_dest_bridge_match(&network_setup.bridge_name), stmt::Statement::Drop(None), ], )); } } else { // No isolation: insert a rule at position 1 in ISOLATION3 to drop traffic. // Do this to make sure the jump from ISOLATION3 to ISOLATION2 below is always the last thing in the chain. // NETAVARK-ISOLATION-3: oifname == drop if get_matching_rules_in_chain(&existing_rules, ISOLATION3CHAIN, match_our_bridge) .is_empty() { batch.add_cmd(schema::NfCmd::Insert(make_rule( ISOLATION3CHAIN, vec![ get_dest_bridge_match(&network_setup.bridge_name), stmt::Statement::Drop(None), ], ))); } } // Always exists, even when isolation disabled. Must be the last item in the ISOLATION3 chain. // NETAVARK-ISOLATION-3: jump NETAVARK-ISOLATION-2 if get_matching_rules_in_chain( &existing_rules, ISOLATION3CHAIN, get_rule_matcher_jump_to(ISOLATION2CHAIN.to_string()), ) .is_empty() { batch.add(make_rule( ISOLATION3CHAIN, vec![get_jump_action(ISOLATION2CHAIN)], )); } // Basic forwarding for all subnets if let Some(nets) = network_setup.subnets { for subnet in nets { let chain = get_subnet_chain_name(subnet, &network_setup.network_id, false); // Add us to firewalld if necessary. // Do this first, as firewalld doesn't wipe our rules - so after a reload, we skip everything below. firewalld::add_firewalld_if_possible(dbus_conn, &subnet); // Do we already have a chain for the subnet? if get_chain(&existing_rules, &chain).is_some() { continue; } // We don't. Make one. batch.add(make_basic_chain(&chain)); log::info!("Creating container chain {chain}"); // Subnet chain: ip daddr accept batch.add(make_rule( &chain, vec![ get_subnet_match(&subnet, "daddr", stmt::Operator::EQ), stmt::Statement::Accept(None), ], )); // Subnet chain: ip daddr != 224.0.0.0/4 masquerade let multicast_address: IpNet = match subnet { IpNet::V4(_) => "224.0.0.0/4".parse()?, IpNet::V6(_) => "ff::00/8".parse()?, }; batch.add(make_rule( &chain, vec![ get_subnet_match(&multicast_address, "daddr", stmt::Operator::NEQ), stmt::Statement::Masquerade(None), ], )); // Next, populate basic chains with forwarding rules // Input chain: ip saddr udp dport 53 accept batch.add(make_rule( INPUTCHAIN, vec![ get_subnet_match(&subnet, "saddr", stmt::Operator::EQ), stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::Meta( expr::Meta { key: expr::MetaKey::L4proto, }, )), right: expr::Expression::Named(expr::NamedExpression::Set(vec![ expr::SetItem::Element(expr::Expression::String("udp".to_string())), expr::SetItem::Element(expr::Expression::String("tcp".to_string())), ])), op: stmt::Operator::EQ, }), stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::Payload( expr::Payload::PayloadField(expr::PayloadField { protocol: "th".to_string(), field: "dport".to_string(), }), )), right: expr::Expression::Number(53), op: stmt::Operator::EQ, }), stmt::Statement::Accept(None), ], )); // Forward chain: ip daddr ct state related,established accept batch.add(make_rule( FORWARDCHAIN, vec![ get_subnet_match(&subnet, "daddr", stmt::Operator::EQ), stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::CT(expr::CT { key: "state".to_string(), family: None, dir: None, })), right: expr::Expression::List(vec![ expr::Expression::String("established".to_string()), expr::Expression::String("related".to_string()), ]), op: stmt::Operator::IN, }), stmt::Statement::Accept(None), ], )); // Forward chain: ip saddr accept batch.add(make_rule( FORWARDCHAIN, vec![ get_subnet_match(&subnet, "saddr", stmt::Operator::EQ), stmt::Statement::Accept(None), ], )); // Postrouting chain: ip saddr jump batch.add(make_rule( POSTROUTINGCHAIN, vec![ get_subnet_match(&subnet, "saddr", stmt::Operator::EQ), get_jump_action(&chain), ], )); } } let rules = batch.to_nftables(); helper::apply_ruleset(&rules, None, None)?; Ok(()) } fn teardown_network(&self, tear: internal_types::TearDownNetwork) -> NetavarkResult<()> { let mut batch = Batch::new(); let existing_rules = get_netavark_rules()?; if let Some(nets) = tear.config.subnets { for subnet in nets { // Match subnet, either saddr or daddr. let match_subnet = |r: &schema::Rule| -> bool { // Statement matching: We only care about match statements. // Don't bother with left side. Just check if what they compare to is our subnet. for statement in &r.expr { match statement { stmt::Statement::Match(m) => match &m.right { expr::Expression::Named(expr::NamedExpression::Prefix(p)) => { match p.addr.as_ref() { expr::Expression::String(s) => { if *s == subnet.addr().to_string() && subnet.prefix_len() as u32 == p.len { return true; } } _ => continue, } } _ => continue, }, _ => continue, } } false }; let mut to_remove: Vec = Vec::new(); to_remove.append(&mut get_matching_rules_in_chain( &existing_rules, INPUTCHAIN, match_subnet, )); to_remove.append(&mut get_matching_rules_in_chain( &existing_rules, FORWARDCHAIN, match_subnet, )); to_remove.append(&mut get_matching_rules_in_chain( &existing_rules, POSTROUTINGCHAIN, match_subnet, )); log::debug!("Removing {} rules", to_remove.len()); for rule in to_remove { batch.delete(schema::NfListObject::Rule(rule)); } // Delete the chain last let chain = get_subnet_chain_name(subnet, &tear.config.network_id, false); if let Some(c) = get_chain(&existing_rules, &chain) { batch.delete(schema::NfListObject::Chain(c)); } // After all nftables work is done, remove us from firewalld. firewalld::rm_firewalld_if_possible(&subnet); } } let match_our_bridge = get_rule_matcher_bridge(&tear.config.bridge_name); let mut isolation_rules: Vec = Vec::new(); isolation_rules.append(&mut get_matching_rules_in_chain( &existing_rules, ISOLATION1CHAIN, &match_our_bridge, )); isolation_rules.append(&mut get_matching_rules_in_chain( &existing_rules, ISOLATION2CHAIN, &match_our_bridge, )); isolation_rules.append(&mut get_matching_rules_in_chain( &existing_rules, ISOLATION3CHAIN, &match_our_bridge, )); log::debug!( "Removing {} isolation rules for network", isolation_rules.len() ); for rule in isolation_rules { batch.delete(schema::NfListObject::Rule(rule)); } let rules = batch.to_nftables(); helper::apply_ruleset(&rules, None, None)?; Ok(()) } fn setup_port_forward( &self, setup_portfw: internal_types::PortForwardConfig, dbus_conn: &Option, ) -> NetavarkResult<()> { firewalld::check_can_forward_ports(dbus_conn, &setup_portfw)?; let mut batch = Batch::new(); let existing_rules = get_netavark_rules()?; // Need DNAT rules for DNS if Aardvark is not on port 53. // Only need one per DNS server IP, so check if they already exist first. if setup_portfw.dns_port != 53 { for ip in setup_portfw.dns_server_ips { let match_dns_ip_dnat = |r: &schema::Rule| { for statement in &r.expr { match statement { stmt::Statement::Match(m) => match &m.right { expr::Expression::String(s) => { if *s == ip.to_string() { return true; } } _ => continue, }, _ => continue, } } false }; if !get_matching_rules_in_chain(&existing_rules, DNATCHAIN, match_dns_ip_dnat) .is_empty() { continue; } // We have multiple DNS server IPs. Potentially v4 and v6 both. // Only add those when the container has an IP address matching the family. match ip { IpAddr::V4(_) => { if setup_portfw.container_ip_v4.is_some() { // rule should be first so it is ordered before the normal contianer DNAT, // thus use insert over the normal add batch.add_cmd(schema::NfCmd::Insert(make_dns_dnat_rule( ip, setup_portfw.dns_port, ))); } } IpAddr::V6(_) => { if setup_portfw.container_ip_v6.is_some() { // rule should be first so it is ordered before the normal contianer DNAT, // thus use insert over the normal add batch.add_cmd(schema::NfCmd::Insert(make_dns_dnat_rule( ip, setup_portfw.dns_port, ))); } } } } } if let Some(ip_v4) = setup_portfw.container_ip_v4 { if let Some(subnet_v4) = setup_portfw.subnet_v4 { for rule in get_dnat_rules_for_addr_family( ip_v4, subnet_v4, &setup_portfw.network_id, &existing_rules, &setup_portfw, )? { batch.add(rule); } } } if let Some(ip_v6) = setup_portfw.container_ip_v6 { if let Some(subnet_v6) = setup_portfw.subnet_v6 { for rule in get_dnat_rules_for_addr_family( ip_v6, subnet_v6, &setup_portfw.network_id, &existing_rules, &setup_portfw, )? { batch.add(rule); } } } let rules = batch.to_nftables(); helper::apply_ruleset(&rules, None, None)?; Ok(()) } fn teardown_port_forward( &self, teardown_pf: internal_types::TeardownPortForward, ) -> NetavarkResult<()> { let mut batch = Batch::new(); let existing_rules = get_netavark_rules()?; let dnat_chain_v4 = teardown_pf .config .subnet_v4 .map(|s| get_subnet_chain_name(s, &teardown_pf.config.network_id, true)); let dnat_chain_v6 = teardown_pf .config .subnet_v6 .map(|s| get_subnet_chain_name(s, &teardown_pf.config.network_id, true)); if let Some(ip_v4) = teardown_pf.config.container_ip_v4 { if let Some(subnet_v4) = teardown_pf.config.subnet_v4 { delete_port_rules(ip_v4, subnet_v4, &teardown_pf, &existing_rules, &mut batch)?; } } if let Some(ip_v6) = teardown_pf.config.container_ip_v6 { if let Some(subnet_v6) = teardown_pf.config.subnet_v6 { delete_port_rules(ip_v6, subnet_v6, &teardown_pf, &existing_rules, &mut batch)?; } } if teardown_pf.complete_teardown { let match_dns_dnat = |r: &schema::Rule| -> bool { for statement in &r.expr { match statement { // Match any DNS server IP stmt::Statement::Match(m) => match &m.right { expr::Expression::String(s) => { for ip in teardown_pf.config.dns_server_ips { if *s == ip.to_string() { return true; } } } _ => continue, }, _ => continue, } } false }; for rule in get_matching_rules_in_chain(&existing_rules, DNATCHAIN, match_dns_dnat) { batch.delete(schema::NfListObject::Rule(rule)); } if let Some(v4) = dnat_chain_v4 { if let Some(c) = get_chain(&existing_rules, &v4) { batch.delete(schema::NfListObject::Chain(c)); } } if let Some(v6) = dnat_chain_v6 { if let Some(c) = get_chain(&existing_rules, &v6) { batch.delete(schema::NfListObject::Chain(c)); } } } let rules = batch.to_nftables(); helper::apply_ruleset(&rules, None, None)?; Ok(()) } } // compare two rules, we only check the chain name and expr, // while we can do rule1 == rule2 it will not work how we like. // As we use this to compare rules from nft against rules created // by us in memory it means the handle id and index can never match. fn cmp_rules(rule1: &schema::Rule, rule2: &schema::Rule) -> bool { if rule1.chain == rule2.chain && rule1.expr == rule2.expr { return true; } false } fn delete_port_rules( ip: IpAddr, subnet: IpNet, teardown_pf: &internal_types::TeardownPortForward, existing_rules: &schema::Nftables, batch: &mut Batch, ) -> NetavarkResult<()> { let port_rules = get_dnat_rules_for_addr_family( ip, subnet, &teardown_pf.config.network_id, existing_rules, &teardown_pf.config, )?; for object in &existing_rules.objects { match object { schema::NfObject::CmdObject(_) => continue, schema::NfObject::ListObject(list) => match list.as_ref() { schema::NfListObject::Rule(ref rule) => { for port_rule in &port_rules { match port_rule { schema::NfListObject::Rule(r) => { if cmp_rules(r, rule) { batch.delete(*list.clone()); } } _ => continue, } } } _ => continue, }, } } Ok(()) } /// Convert a subnet into a chain name. fn get_subnet_chain_name(subnet: IpNet, net_id: &str, dnat: bool) -> String { // nftables is very lenient around chain name lengths. // So let's use the full IP to be unambiguous. // Replace . and : with _, and / with _nm (netmask), to remove special characters. let subnet_clean = subnet .to_string() .replace('.', "_") .replace(':', "-") .replace('/', "_nm"); let net_id_clean = if net_id.len() > 8 { net_id.split_at(8).0 } else { net_id }; if dnat { format!("nv_{}_{}_dnat", net_id_clean, subnet_clean) } else { format!("nv_{}_{}", net_id_clean, subnet_clean) } } /// Get a statement to match the given destination bridge. /// Always matches using ==. fn get_dest_bridge_match(bridge: &str) -> stmt::Statement { stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::Meta(expr::Meta { key: expr::MetaKey::Oifname, })), right: expr::Expression::String(bridge.to_string()), op: stmt::Operator::EQ, }) } /// Get a statement to match the given IP address. /// Field should be either "saddr" or "daddr" for matching source or destination. fn get_ip_match(ip: &IpAddr, field: &str, op: stmt::Operator) -> stmt::Statement { stmt::Statement::Match(stmt::Match { left: ip_to_payload(ip, field), right: expr::Expression::String(ip.to_string()), op, }) } /// Convert a single IP into a Payload field. /// Basically, pasts in "ip" or "ip6" in protocol field based on whether this is a v4 or v6 address. fn ip_to_payload(addr: &IpAddr, field: &str) -> expr::Expression { let proto = match addr { IpAddr::V4(_) => "ip".to_string(), IpAddr::V6(_) => "ip6".to_string(), }; expr::Expression::Named(expr::NamedExpression::Payload(expr::Payload::PayloadField( expr::PayloadField { protocol: proto, field: field.to_string(), }, ))) } /// Get a statement to match the given subnet. /// Field should be either "saddr" or "daddr" for matching source or destination. fn get_subnet_match(net: &IpNet, field: &str, op: stmt::Operator) -> stmt::Statement { stmt::Statement::Match(stmt::Match { left: subnet_to_payload(net, field), right: expr::Expression::Named(expr::NamedExpression::Prefix(expr::Prefix { addr: Box::new(expr::Expression::String(net.addr().to_string())), len: net.prefix_len() as u32, })), op, }) } /// Convert a subnet into a Payload field. /// Basically, pastes in "ip" or "ip6" in protocol field based on whether this /// is a v4 or v6 subnet. fn subnet_to_payload(net: &IpNet, field: &str) -> expr::Expression { let proto = match net { IpNet::V4(_) => "ip".to_string(), IpNet::V6(_) => "ip6".to_string(), }; expr::Expression::Named(expr::NamedExpression::Payload(expr::Payload::PayloadField( expr::PayloadField { protocol: proto, field: field.to_string(), }, ))) } /// Get a condition to match destination port/ports based on a given PortMapping. /// Properly handles port ranges, protocol, etc. fn get_dport_cond(port: &PortMapping) -> stmt::Statement { stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::Payload(expr::Payload::PayloadField( expr::PayloadField { protocol: port.protocol.clone(), field: "dport".to_string(), }, ))), right: if port.range > 1 { // Ranges are a vector with a length of 2. // First value start, second value end. let range_vec = vec![ expr::Expression::Number(port.host_port as u32), expr::Expression::Number((port.host_port + port.range - 1) as u32), ]; expr::Expression::Range(expr::Range { range: range_vec }) } else { expr::Expression::Number(port.host_port as u32) }, op: stmt::Operator::EQ, }) } /// Make the first container DNAT chain rule, which is used for both IP and IPv6 DNAT. fn get_subnet_dport_match( dnat_chain: &str, subnet: &Option, host_ip_match: &Option, dport_match: &stmt::Statement, ) -> schema::NfListObject { // ip saddr ip daddr dport jump MARKCHAIN let mut statements: Vec = Vec::new(); if let Some(net) = &subnet { statements.push(get_subnet_match(net, "saddr", stmt::Operator::EQ)); } if let Some(stmt) = host_ip_match { statements.push(stmt.clone()); } statements.push(dport_match.clone()); statements.push(get_jump_action(MASKCHAIN)); make_rule(dnat_chain, statements) } /// Create DNAT rules for each port to be forwarded. /// Used for both IP and IPv6 DNAT. fn get_dnat_port_rules( dnat_chain: &str, port: &PortMapping, ip: &IpAddr, host_ip_cond: &Option, ) -> Vec { let mut rules: Vec = Vec::new(); // Container dnat chain: ip daddr dport dnat to // Unfortunately: We don't have range support in the schema. So we need 1 rule per port. let range = if port.range == 0 { 1 } else { port.range }; for i in 0..range { let host_port: u32 = (port.host_port + i) as u32; let ctr_port: u32 = (port.container_port + i) as u32; let mut statements: Vec = Vec::new(); if let Some(stmt) = host_ip_cond { statements.push(stmt.clone()); } statements.push(stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::Payload( expr::Payload::PayloadField(expr::PayloadField { protocol: port.protocol.clone(), field: "dport".to_string(), }), )), right: expr::Expression::Number(host_port), op: stmt::Operator::EQ, })); statements.push(stmt::Statement::DNAT(Some(stmt::NAT { addr: Some(expr::Expression::String(ip.to_string())), family: Some(if ip.is_ipv6() { stmt::NATFamily::IP6 } else { stmt::NATFamily::IP }), port: Some(ctr_port), flags: None, }))); rules.push(make_rule(dnat_chain, statements)); } rules } fn get_dnat_rules_for_addr_family( ip: IpAddr, subnet: IpNet, net_id: &str, existing_rules: &schema::Nftables, setup_portfw: &internal_types::PortForwardConfig, ) -> NetavarkResult> { let mut rules: Vec = Vec::new(); if let Some(ports) = setup_portfw.port_mappings { let subnet_dnat_chain = get_subnet_chain_name(subnet, net_id, true); // Make the chain if it does not exist if get_chain(existing_rules, &subnet_dnat_chain).is_none() { rules.push(make_basic_chain(&subnet_dnat_chain)); } for port in ports { // Condition to match destination ports (ports on the host) let dport_cond = get_dport_cond(port); // Destination address is only if user set an IP on the host to bind to. // Used by multiple rules in this section. // We need to ignore wildcards, but only if our IP family matches the wildcard. // If it doesn't, don't add any rules. let daddr: Option = if !port.host_ip.is_empty() { if port.host_ip == "0.0.0.0" { if ip.is_ipv6() { continue; } None } else if port.host_ip == "::" { if ip.is_ipv4() { continue; } None } else { match port.host_ip.parse() { Ok(i) => Some(i), Err(_) => { return Err(NetavarkError::msg(format!( "invalid host ip \"{}\" provided for port {}", port.host_ip, port.host_port ))); } } } } else { None }; // Do not add rules where the address family of host address does not match container address. if let Some(host_ip) = daddr { if ip.is_ipv4() != host_ip.is_ipv4() { continue; } } let mut jump_statements = Vec::with_capacity(3); let daddr_cond: Option = daddr.map(|i| { let daddr = get_ip_match(&i, "daddr", stmt::Operator::EQ); jump_statements.push(daddr.clone()); daddr }); jump_statements.push(dport_cond.clone()); jump_statements.push(get_jump_action(&subnet_dnat_chain)); // dnat chain: [ip daddr ] dport jump rules.push(make_rule(DNATCHAIN, jump_statements)); // Container dnat chain: ip saddr ip daddr dport jump SETMARKCHAIN rules.push(get_subnet_dport_match( &subnet_dnat_chain, &Some(subnet), &daddr_cond, &dport_cond, )); // This rule is only used for v4. if ip.is_ipv4() { // Container dnat chain: ip saddr 127.0.0.1 ip daddr dport jump SETMARKCHAIN let mut localhost_jump_statements: Vec = Vec::new(); localhost_jump_statements.push(get_ip_match( &IPV4_LOCALHOST, "saddr", stmt::Operator::EQ, )); if let Some(stmt) = &daddr_cond { localhost_jump_statements.push(stmt.clone()); } localhost_jump_statements.push(dport_cond); localhost_jump_statements.push(get_jump_action(MASKCHAIN)); rules.push(make_rule(&subnet_dnat_chain, localhost_jump_statements)); } rules.append(&mut get_dnat_port_rules( &subnet_dnat_chain, port, &ip, &daddr_cond, )); } } Ok(rules) } /// Make a DNAT rule to allow DNS traffic to a DNS server on a non-standard port (53 -> actual port). fn make_dns_dnat_rule(dns_ip: &IpAddr, dns_port: u16) -> schema::NfListObject { let rule = schema::Rule { family: types::NfFamily::INet, table: TABLENAME.to_string(), chain: DNATCHAIN.to_string(), expr: vec![ get_ip_match(dns_ip, "daddr", stmt::Operator::EQ), stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::Meta(expr::Meta { key: expr::MetaKey::L4proto, })), right: expr::Expression::Named(expr::NamedExpression::Set(vec![ expr::SetItem::Element(expr::Expression::String("udp".to_string())), expr::SetItem::Element(expr::Expression::String("tcp".to_string())), ])), op: stmt::Operator::EQ, }), stmt::Statement::Match(stmt::Match { left: expr::Expression::Named(expr::NamedExpression::Payload( expr::Payload::PayloadField(expr::PayloadField { protocol: "th".to_string(), field: "dport".to_string(), }), )), right: expr::Expression::Number(53), op: stmt::Operator::EQ, }), stmt::Statement::DNAT(Some(stmt::NAT { addr: Some(expr::Expression::String(dns_ip.to_string())), family: Some(if dns_ip.is_ipv6() { stmt::NATFamily::IP6 } else { stmt::NATFamily::IP }), port: Some(dns_port as u32), flags: None, })), ], ..schema::Rule::default() }; schema::NfListObject::Rule(rule) } /// Create a statement to jump to the given target fn get_jump_action(target: &str) -> stmt::Statement { stmt::Statement::Jump(stmt::JumpTarget { target: target.to_string(), }) } /// Create an instruction to make a basic chain (no hooks, no priority). /// Chain is always inet, always in our overall netavark table. fn make_basic_chain(name: &str) -> schema::NfListObject { schema::NfListObject::Chain(schema::Chain { family: types::NfFamily::INet, table: TABLENAME.to_string(), name: name.to_string(), ..schema::Chain::default() }) } /// Create a more complicated chain with hooks and priority. /// Policy is always accept, because we don't need anything else. fn make_complex_chain( name: &str, chain_type: types::NfChainType, hook: types::NfHook, priority: i32, ) -> schema::NfListObject { schema::NfListObject::Chain(schema::Chain { family: types::NfFamily::INet, table: TABLENAME.to_string(), name: name.to_string(), _type: Some(chain_type), hook: Some(hook), prio: Some(priority), policy: Some(types::NfChainPolicy::Accept), ..schema::Chain::default() }) } /// Make a rule in the given chain with the given conditions fn make_rule(chain: &str, conditions: Vec) -> schema::NfListObject { schema::NfListObject::Rule(schema::Rule { family: types::NfFamily::INet, table: TABLENAME.to_string(), chain: chain.to_string(), expr: conditions, ..schema::Rule::default() }) } /// Make a closure that matches any rule that jumps to the given chain. fn get_rule_matcher_jump_to(jump_target: String) -> Box bool> { Box::new(move |r: &schema::Rule| -> bool { for statement in &r.expr { match statement { stmt::Statement::Jump(j) => { return j.target == jump_target; } _ => continue, } } false }) } /// Make a closure that matches any rule that tests for a match to a given bridge interface. fn get_rule_matcher_bridge(bridge: &String) -> impl '_ + Fn(&schema::Rule) -> bool { move |r: &schema::Rule| -> bool { for statement in &r.expr { match statement { stmt::Statement::Match(m) => match &m.right { expr::Expression::String(s) => { if *s == *bridge { return true; } } _ => continue, }, _ => continue, } } false } } /// Find all rules in the given chain which match the given closure (true == include). /// Returns all those rules, in a vector. Vector will be empty if there are none. fn get_matching_rules_in_chain bool>( base_rules: &schema::Nftables, chain: &str, rule_match: F, ) -> Vec { let mut rules: Vec = Vec::new(); // Basically, we get back a big, flat array of everything in the table. // That makes this an absolute destructuring nightmare, but there's no avoiding it. // Ignore everything we get back that is not a rule. // Then ignore everything that is not in our table (not passed, but we only use one table). // Then ignore everything that is not in the given chain. // Then check conditions and add to the vector if it matches. for object in &base_rules.objects { match object { schema::NfObject::CmdObject(_) => continue, schema::NfObject::ListObject(obj) => match obj.as_ref() { schema::NfListObject::Rule(r) => { if r.chain != *chain { continue; } if rule_match(r) { log::debug!("Matched {:?}", r); rules.push(r.clone()); } } _ => continue, }, } } rules } /// Get a chain with the given name in the Netavark table. fn get_chain(base_rules: &schema::Nftables, chain: &str) -> Option { for object in &base_rules.objects { match object { schema::NfObject::CmdObject(_) => continue, schema::NfObject::ListObject(obj) => match obj.as_ref() { schema::NfListObject::Chain(c) => { if c.name == *chain { log::debug!("Found chain {}", chain); return Some(c.clone()); } } _ => continue, }, } } None } fn get_netavark_rules() -> Result { match helper::get_current_ruleset(None, Some(vec!["list", "table", "inet", TABLENAME])) { Ok(rules) => Ok(rules), Err(err) => match err { helper::NftablesError::NftFailed { program: _, hint: _, stdout: _, ref stderr, } => { // OK this is hacky but seems to work, when we run the first time after the boot the // netavark table does not exists to the list table call will fail (nft exit code 1). // Just return an empty ruleset in this case. if stderr.contains("No such file or directory") { Ok(schema::Nftables { objects: vec![] }) } else { Err(err) } } err => Err(err), }, } } containers-netavark-68510f4/src/firewall/state.rs000066400000000000000000000302361475240420400220370ustar00rootroot00000000000000use std::{ fs::{self, File, OpenOptions}, io::{self, ErrorKind, Write}, path::{Path, PathBuf}, }; use fs2::FileExt; use serde::de::DeserializeOwned; use crate::{ error::{NetavarkError, NetavarkResult}, network::internal_types::{PortForwardConfig, PortForwardConfigOwned, SetupNetwork}, wrap, }; // File layout looks like this // $config/firewall/ // - firewall-driver -> name of the firewall driver // - networks/$netID -> network config setup // - ports/$netID_$conID -> port config const FIREWALL_DIR: &str = "firewall"; const FIREWALL_DRIVER_FILE: &str = "firewall-driver"; const FIREWALL_LOCK_FILE: &str = "firewall-reload.lock"; const NETWORK_CONF_DIR: &str = "networks"; const PORT_CONF_DIR: &str = "ports"; struct FilePaths { fw_driver_file: PathBuf, net_conf_file: PathBuf, port_conf_file: PathBuf, /// The file is returned locked, it does not need /// to be unlocked as rust does it automatically on drop. /// This file is required to ensure that remove_fw_config is not racing against /// the firewall reload service, i.e. without it would be possible that we read /// the config files and then during re-adding the rules the file got removed. /// This leaves a chance that the service will add rules that should not be added /// anymore. lock_file: File, } /// macro to quickly wrap the IO error with useful context /// First argument is the function, second the path, third the extra error message. /// The full error is "$msg $path: $org_error" macro_rules! fs_err { ($func:expr, $path:expr, $msg:expr) => { $func($path).map_err(|err| { NetavarkError::wrap(format!("{} {:?}", $msg, $path.display()), err.into()) }) }; } macro_rules! ignore_enoent { ($call:expr, $action:expr) => { match $call { Ok(ok) => Ok(ok), Err(err) if err.kind() == std::io::ErrorKind::NotFound => $action, Err(e) => Err(e), } }; } fn remove_file_ignore_enoent>(path: P) -> io::Result<()> { match fs::remove_file(path) { Ok(ok) => Ok(ok), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(e) => Err(e), } } fn firewall_config_dir(config_dir: &Path) -> PathBuf { Path::new(config_dir).join(FIREWALL_DIR) } /// Assemble file paths for the config files, when create_dirs is set to true /// it will create the parent dirs as well so the caller does not have to. /// /// As a special case when network_id and container_id is empty it will return /// the paths for the directories instead which are used to walk the dir for all configs. fn get_file_paths( config_dir: &Path, network_id: &str, container_id: &str, create_dirs: bool, ) -> NetavarkResult { let path = firewall_config_dir(config_dir); let fw_driver_file = path.join(FIREWALL_DRIVER_FILE); let mut net_conf_file = path.join(NETWORK_CONF_DIR); let mut port_conf_file = path.join(PORT_CONF_DIR); // we need to always create this for the lockfile fs_err!(fs::create_dir_all, &path, "create firewall config dir")?; if create_dirs { fs_err!( fs::create_dir_all, &net_conf_file, "create network config dir" )?; fs_err!( fs::create_dir_all, &port_conf_file, "create port config dir" )?; } if !network_id.is_empty() && !container_id.is_empty() { net_conf_file.push(network_id); port_conf_file.push(network_id.to_string() + "_" + container_id); } let lock_file = fs_err!( File::create, &path.join(FIREWALL_LOCK_FILE), "create firewall lock file" )?; wrap!(lock_file.lock_exclusive(), "lock firewall lock file")?; Ok(FilePaths { fw_driver_file, net_conf_file, port_conf_file, lock_file, }) } /// Store the firewall configs on disk. /// This should be caller after firewall setup to allow the firewalld reload /// service to read the configs later and readd the rules. pub fn write_fw_config( config_dir: &Path, network_id: &str, container_id: &str, fw_driver: &str, net_conf: &SetupNetwork, port_conf: &PortForwardConfig, ) -> NetavarkResult<()> { let paths = get_file_paths(config_dir, network_id, container_id, true)?; fs_err!( File::create, &paths.fw_driver_file, "create firewall-driver file" )? .write_all(fw_driver.as_bytes()) .map_err(|err| NetavarkError::wrap("failed to write firewall-driver file", err.into()))?; match OpenOptions::new() .write(true) .create_new(true) .open(&paths.net_conf_file) { Ok(f) => serde_json::to_writer(f, &net_conf)?, // net config file already exists no need to write the same stuff again. Err(ref e) if e.kind() == ErrorKind::AlreadyExists => (), Err(e) => { return Err(NetavarkError::wrap( format!("create network config {:?}", &paths.net_conf_file.display()), e.into(), )); } }; let ports_file = fs_err!(File::create, &paths.port_conf_file, "create port config")?; serde_json::to_writer(ports_file, &port_conf)?; Ok(()) } /// Remove firewall config files. /// On firewall teardown remove the specific config files again so the /// firewalld reload service does not keep using them. pub fn remove_fw_config( config_dir: &Path, network_id: &str, container_id: &str, complete_teardown: bool, ) -> NetavarkResult<()> { let paths = get_file_paths(config_dir, network_id, container_id, false)?; fs_err!( remove_file_ignore_enoent, &paths.port_conf_file, "remove port config" )?; if complete_teardown { fs_err!( remove_file_ignore_enoent, &paths.net_conf_file, "remove network config" )?; } Ok(()) } pub struct FirewallConfig { /// Name of the firewall driver pub driver: String, /// All the network firewall configs pub net_confs: Vec, /// All port forwarding configs pub port_confs: Vec, /// Lock file for the firewall code to prevent us from adding rules while the state files /// have been removed in the meantime. /// We never do anything with it but we need to keep it open as closing it closes the lock /// So once this struct is dropped the lock is closed automatically. #[allow(dead_code)] lock_file: File, } /// Read all firewall configs files from the dir. pub fn read_fw_config(config_dir: &Path) -> NetavarkResult> { let paths = get_file_paths(config_dir, "", "", false)?; // now it is possible the firewall-reload is started before any containers were started so we just // return None in this case. let driver = wrap!( ignore_enoent!(fs::read_to_string(&paths.fw_driver_file), return Ok(None)), format!("read firewall-driver {:?}", &paths.fw_driver_file.display()) )?; let net_confs = read_dir_conf(paths.net_conf_file)?; let port_confs = read_dir_conf(paths.port_conf_file)?; Ok(Some(FirewallConfig { driver, net_confs, port_confs, lock_file: paths.lock_file, })) } fn read_dir_conf(dir: PathBuf) -> NetavarkResult> { let mut confs = Vec::new(); for entry in fs_err!(fs::read_dir, &dir, "read dir")? { let path = ignore_enoent!(entry, continue)?.path(); let content = wrap!( ignore_enoent!(fs::read_to_string(&path), continue), format!("read config {:?}", path.display()) )?; // Note one might think we should use from_reader() instead of reading // into one string. However the files we act on are small enough that it // should't matter to have the content into memory at once and based on // https://github.com/serde-rs/json/issues/160 this here is much faster. let conf: T = serde_json::from_str(&content)?; confs.push(conf); } Ok(confs) } #[cfg(test)] mod tests { use std::net::{IpAddr, Ipv4Addr}; use crate::network::internal_types::IsolateOption; use super::*; use tempfile::Builder; #[test] fn test_fw_config() { let network_id = "abc"; let container_id = "123"; let driver = "iptables"; let tmpdir = Builder::new().prefix("netavark-tests").tempdir().unwrap(); let config_dir = tmpdir.path(); let net_conf = SetupNetwork { subnets: Some(vec!["10.0.0.0/24".parse().unwrap()]), network_id: "c2c8a073252874648259997d53b0a1bffa491e21f04bc1bf8609266359931395" .to_string(), bridge_name: "bridge".to_string(), network_hash_name: "hash".to_string(), isolation: IsolateOption::Never, dns_port: 53, }; let net_conf_json = r#"{"subnets":["10.0.0.0/24"],"bridge_name":"bridge","network_id":"c2c8a073252874648259997d53b0a1bffa491e21f04bc1bf8609266359931395","network_hash_name":"hash","isolation":"Never","dns_port":53}"#; let port_conf = PortForwardConfig { container_id: container_id.to_string(), network_id: "c2c8a073252874648259997d53b0a1bffa491e21f04bc1bf8609266359931395" .to_string(), port_mappings: &None, network_name: "name".to_string(), network_hash_name: "hash".to_string(), container_ip_v4: Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2))), subnet_v4: Some("10.0.0.0/24".parse().unwrap()), container_ip_v6: None, subnet_v6: None, dns_port: 53, dns_server_ips: &vec![], }; let port_conf_json = r#"{"container_id":"123","network_id":"c2c8a073252874648259997d53b0a1bffa491e21f04bc1bf8609266359931395","port_mappings":null,"network_name":"name","network_hash_name":"hash","container_ip_v4":"10.0.0.2","subnet_v4":"10.0.0.0/24","container_ip_v6":null,"subnet_v6":null,"dns_port":53,"dns_server_ips":[]}"#; let res = write_fw_config( config_dir, network_id, container_id, driver, &net_conf, &port_conf, ); assert!(res.is_ok(), "write_fw_config failed"); let paths = get_file_paths(config_dir, network_id, container_id, false).unwrap(); drop(paths.lock_file); // unlock to prevent deadlock with other calls let res = fs::read_to_string(paths.fw_driver_file).unwrap(); assert_eq!(res, "iptables", "read fw driver"); let res = fs::read_to_string(&paths.net_conf_file).unwrap(); assert_eq!(res, net_conf_json, "read net conf"); let res = fs::read_to_string(&paths.port_conf_file).unwrap(); assert_eq!(res, port_conf_json, "read port conf"); let res = read_fw_config(config_dir) .unwrap() .expect("no fw config files"); assert_eq!(res.driver, driver, "correct fw driver"); assert_eq!(res.net_confs, vec![net_conf], "same net configs"); let port_confs_ref: Vec = res.port_confs.iter().map(|f| f.into()).collect(); assert_eq!(port_confs_ref, vec![port_conf], "same port configs"); // unlock lock file drop(res); let res = remove_fw_config(config_dir, network_id, container_id, true); assert!(res.is_ok(), "remove_fw_config failed"); assert!(!paths.net_conf_file.exists(), "net conf should not exists"); assert!( !paths.port_conf_file.exists(), "port conf should not exists" ); // now again since we ignore ENOENT it should still return no error let res = remove_fw_config(config_dir, network_id, container_id, true); assert!(res.is_ok(), "remove_fw_config failed second time"); } #[test] fn test_read_fw_config_empty() { let tmpdir = Builder::new().prefix("netavark-tests").tempdir().unwrap(); let config_dir = tmpdir.path(); let res = read_fw_config(config_dir).expect("no read_fw_config error"); assert!(res.is_none(), "no firewall config should be given"); } } containers-netavark-68510f4/src/firewall/varktables/000077500000000000000000000000001475240420400225035ustar00rootroot00000000000000containers-netavark-68510f4/src/firewall/varktables/helpers.rs000066400000000000000000000070251475240420400245170ustar00rootroot00000000000000use crate::error::{NetavarkError, NetavarkResult}; use iptables::IPTables; use log::debug; // append a rule to chain if it does not exist // Note: While there is an API provided for this exact thing, the API returns // an error that is not defined if the rule exists. This function just returns // an error if there is a problem. pub fn append_unique( driver: &IPTables, table: &str, chain: &str, rule: &str, ) -> NetavarkResult<()> { let exists = match driver.exists(table, chain, rule) { Ok(b) => b, Err(e) => return Err(NetavarkError::Message(e.to_string())), }; if exists { debug_rule_exists(table, chain, rule.to_string()); return Ok(()); } if let Err(e) = driver .append(table, chain, rule) .map(|_| debug_rule_create(table, chain, rule.to_string())) { return Err(NetavarkError::Message(format!( "unable to append rule '{rule}' to table '{table}': {e}", ))); } Result::Ok(()) } // add a chain if it does not exist, else do nothing pub fn add_chain_unique(driver: &IPTables, table: &str, new_chain: &str) -> NetavarkResult<()> { // Note: while there is an API provided to check if a chain exists in a table // by iptables, it, for some reason, is slow. Instead we just get a list of // chains in a table and iterate. Same is being done in golang implementations let exists = chain_exists(driver, table, new_chain)?; if exists { debug_chain_exists(table, new_chain); return Ok(()); } match driver .new_chain(table, new_chain) .map(|_| debug_chain_create(table, new_chain)) { Ok(_) => Ok(()), Err(e) => Err(NetavarkError::Message(e.to_string())), } } // returns a bool as to whether the chain exists fn chain_exists(driver: &IPTables, table: &str, chain: &str) -> NetavarkResult { let c = match driver.list_chains(table) { Ok(b) => b, Err(e) => return Err(NetavarkError::Message(e.to_string())), }; if c.iter().any(|i| i == chain) { debug_chain_exists(table, chain); return serde::__private::Result::Ok(true); } serde::__private::Result::Ok(false) } pub fn remove_if_rule_exists( driver: &IPTables, table: &str, chain: &str, rule: &str, ) -> NetavarkResult<()> { // If the rule is not present, do not error let exists = match driver.exists(table, chain, rule) { Ok(b) => b, Err(e) => return Err(NetavarkError::Message(e.to_string())), }; if !exists { debug_rule_no_exists(table, chain, rule.to_string()); return Ok(()); } if let Err(e) = driver.delete(table, chain, rule) { return Err(NetavarkError::Message(format!( "failed to remove rule '{rule}' from table '{chain}': {e}" ))); } Result::Ok(()) } fn debug_chain_create(table: &str, chain: &str) { debug!("chain {} created on table {}", chain, table); } fn debug_chain_exists(table: &str, chain: &str) { debug!("chain {} exists on table {}", chain, table); } pub fn debug_rule_create(table: &str, chain: &str, rule: String) { debug!( "rule {} created on table {} and chain {}", rule, table, chain ); } fn debug_rule_exists(table: &str, chain: &str, rule: String) { debug!( "rule {} exists on table {} and chain {}", rule, table, chain ); } fn debug_rule_no_exists(table: &str, chain: &str, rule: String) { debug!( "no rule {} exists on table {} and chain {}", rule, table, chain ); } containers-netavark-68510f4/src/firewall/varktables/mod.rs000066400000000000000000000000561475240420400236310ustar00rootroot00000000000000pub(crate) mod helpers; pub(crate) mod types; containers-netavark-68510f4/src/firewall/varktables/types.rs000066400000000000000000000546571475240420400242360ustar00rootroot00000000000000use crate::error::{NetavarkError, NetavarkResult}; use crate::firewall::varktables::helpers::{ add_chain_unique, append_unique, remove_if_rule_exists, }; use crate::firewall::varktables::types::TeardownPolicy::{Never, OnComplete}; use crate::network::internal_types::{IsolateOption, PortForwardConfig}; use ipnet::IpNet; use iptables::IPTables; use log::debug; use std::net::IpAddr; // Chain names const NAT: &str = "nat"; const FILTER: &str = "filter"; const POSTROUTING: &str = "POSTROUTING"; const PREROUTING: &str = "PREROUTING"; const NETAVARK_FORWARD: &str = "NETAVARK_FORWARD"; const NETAVARK_FIREWALL_RULE_BUILDER: &str = "-m comment --comment 'netavark firewall rules' -j "; const NETAVARK_INPUT: &str = "NETAVARK_INPUT"; const OUTPUT: &str = "OUTPUT"; const INPUT: &str = "INPUT"; const FORWARD: &str = "FORWARD"; const ACCEPT: &str = "ACCEPT"; const NETAVARK_HOSTPORT_DNAT: &str = "NETAVARK-HOSTPORT-DNAT"; const NETAVARK_HOSTPORT_SETMARK: &str = "NETAVARK-HOSTPORT-SETMARK"; const NETAVARK_HOSTPORT_MASK: &str = "NETAVARK-HOSTPORT-MASQ"; const MASQUERADE: &str = "MASQUERADE"; const MARK: &str = "MARK"; const DNAT: &str = "DNAT"; const NETAVARK_ISOLATION_1: &str = "NETAVARK_ISOLATION_1"; const NETAVARK_ISOLATION_2: &str = "NETAVARK_ISOLATION_2"; const NETAVARK_ISOLATION_3: &str = "NETAVARK_ISOLATION_3"; const CONTAINER_DN_CHAIN: &str = "NETAVARK-DN-"; const HEXMARK: &str = "0x2000"; const MULTICAST_NET_V4: &str = "224.0.0.0/4"; const MULTICAST_NET_V6: &str = "ff00::/8"; #[derive(PartialEq, Eq, Debug, Clone)] pub enum TeardownPolicy { OnComplete, Never, } #[derive(Clone, Debug)] pub struct VarkRule { // Formatted string of the rule itself pub rule: String, pub td_policy: Option, /// position can be set to specify the exact rule position, /// if None the rule will be appended. pub position: Option, } impl VarkRule { fn new(rule: String, policy: Option) -> VarkRule { VarkRule { rule, td_policy: policy, position: None, } } fn to_str(&self) -> &str { &self.rule } } // Varkchain is an iptable chain with extra info pub struct VarkChain<'a> { // name of chain pub chain_name: String, // should the chain be created by us pub create: bool, // the connection to iptables, v4 or v6 pub driver: &'a IPTables, // an array of iptables rules to be added to the chain pub rules: Vec, // name of table pub table: String, // if the chain should be removed pub td_policy: Option, } impl VarkChain<'_> { fn new( driver: &IPTables, table: String, chain_name: String, td_policy: Option, ) -> VarkChain { VarkChain { driver, chain_name, table, rules: vec![], create: false, td_policy, } } // create a queue of rules in a vector fn build_rule(&mut self, rule: VarkRule) { self.rules.push(rule) } // actually add the rules to iptables pub fn add_rules(&self) -> NetavarkResult<()> { for rule in &self.rules { // If the rule comes with an optional position, then instead of append // we should use insert if it does not already exist match rule.position { None => { append_unique(self.driver, &self.table, &self.chain_name, rule.to_str())?; } Some(pos) => { let exists = match self .driver .exists(&self.table, &self.chain_name, &rule.rule) { Ok(b) => b, Err(e) => return Err(NetavarkError::Message(e.to_string())), }; if !exists { match self .driver .insert(&self.table, &self.chain_name, &rule.rule, pos) { Ok(_) => {} Err(e) => return Err(NetavarkError::Message(e.to_string())), }; } } } } Ok(()) } // remove a vector of rules pub fn remove_rules(&self, complete_teardown: bool) -> NetavarkResult<()> { for rule in &self.rules { // If the rule policy is Never or this is not a // complete teardown of the network, then we skip removal // of the rule match &rule.td_policy { None => {} Some(policy) => { if *policy == TeardownPolicy::Never || !complete_teardown { continue; } } } remove_if_rule_exists(self.driver, &self.table, &self.chain_name, rule.to_str())?; } Ok(()) } // remove the chain itself. pub fn remove(&self) -> NetavarkResult<()> { // this might be a perf hit but we are going to start this // way and think of faster AND logical approach. let remaining_rules = match self.driver.list(&self.table, &self.chain_name) { Ok(o) => o, Err(e) => return Err(NetavarkError::Message(e.to_string())), }; // if for some reason there is a rule left, dont remove the chain and // also dont make this a fatal error. The vec returned by list always // reserves [0] for the chain name (-A chain_name), hence the <= 1 if remaining_rules.len() <= 1 { match self.driver.delete_chain(&self.table, &self.chain_name) { Ok(_) => {} Err(e) => return Err(NetavarkError::Message(e.to_string())), }; } Result::Ok(()) } } pub fn create_network_chains(chains: Vec>) -> NetavarkResult<()> { // we have to create first all chains because some might be referenced by other rules // and this will fail if they do not exist yet for c in &chains { // If the chain needs to be created, we make it if c.create { add_chain_unique(c.driver, &c.table, &c.chain_name)?; } } for c in &chains { c.add_rules()? } Ok(()) } pub fn get_network_chains<'a>( conn: &'a IPTables, network: IpNet, network_hash_name: &'a str, is_ipv6: bool, interface_name: String, isolation: IsolateOption, dns_port: u16, ) -> Vec> { let mut chains = Vec::new(); let prefixed_network_hash_name = format!("{}-{}", "NETAVARK", network_hash_name); // NETAVARK-HASH let mut hashed_network_chain = VarkChain::new( conn, NAT.to_string(), prefixed_network_hash_name.clone(), Some(OnComplete), ); hashed_network_chain.create = true; hashed_network_chain.build_rule(VarkRule::new( format!("-d {network} -j {ACCEPT}"), Some(TeardownPolicy::OnComplete), )); let mut multicast_dest = MULTICAST_NET_V4; if is_ipv6 { multicast_dest = MULTICAST_NET_V6; } hashed_network_chain.build_rule(VarkRule::new( format!("! -d {multicast_dest} -j {MASQUERADE}"), Some(TeardownPolicy::OnComplete), )); chains.push(hashed_network_chain); // POSTROUTING let mut postrouting_chain = VarkChain::new(conn, NAT.to_string(), POSTROUTING.to_string(), None); postrouting_chain.build_rule(VarkRule::new( format!("-s {network} -j {prefixed_network_hash_name}"), Some(TeardownPolicy::OnComplete), )); chains.push(postrouting_chain); // FORWARD chain let mut forward_chain: VarkChain<'_> = VarkChain::new(conn, FILTER.to_string(), FORWARD.to_string(), None); // INPUT chain let mut input_chain: VarkChain<'_> = VarkChain::new(conn, FILTER.to_string(), INPUT.to_string(), None); // used to prepend specific rules let mut ind = 1; // NETAVARK_ISOLATION_2 // NETAVARK_ISOLATION_2 chain must always exist, // because non-isolation creates DROP rule in NETAVARK_ISOLATION_3 // and NETAVARK_ISOLATION_3 references this as a jump target. let mut netavark_isolation_chain_2 = VarkChain::new( conn, FILTER.to_string(), NETAVARK_ISOLATION_2.to_string(), None, ); netavark_isolation_chain_2.create = true; // NETAVARK_ISOLATION_3 // NETAVARK_ISOLATION_3 chain must exist when IsolateOption is Never or Strict. // bacause non-isolation creates DROP rule in NETAVARK_ISOLATION_3. // and strict isolation references NETAVARK_ISOLATION_3 as a jump target. let mut netavark_isolation_chain_3 = VarkChain::new( conn, FILTER.to_string(), NETAVARK_ISOLATION_3.to_string(), None, ); netavark_isolation_chain_3.create = true; if let IsolateOption::Normal | IsolateOption::Strict = isolation { debug!("Add extra isolate rules"); // NETAVARK_ISOLATION_1 let mut netavark_isolation_chain_1 = VarkChain::new( conn, FILTER.to_string(), NETAVARK_ISOLATION_1.to_string(), None, ); netavark_isolation_chain_1.create = true; // -A FORWARD -j NETAVARK_ISOLATION_1 forward_chain.build_rule(VarkRule { rule: format!("-j {NETAVARK_ISOLATION_1}"), position: Some(ind), td_policy: Some(TeardownPolicy::OnComplete), }); let netavark_isolation_1_target = if let IsolateOption::Strict = isolation { // NETAVARK_ISOLATION_1 -i bridge_name ! -o bridge_name -j NETAVARK_ISOLATION_3 NETAVARK_ISOLATION_3 } else { // NETAVARK_ISOLATION_1 -i bridge_name ! -o bridge_name -j NETAVARK_ISOLATION_2 NETAVARK_ISOLATION_2 }; netavark_isolation_chain_1.build_rule(VarkRule { rule: format!( "-i {interface_name} ! -o {interface_name} -j {netavark_isolation_1_target}" ), position: Some(ind), td_policy: Some(TeardownPolicy::OnComplete), }); // NETAVARK_ISOLATION_2 -o bridge_name -j DROP netavark_isolation_chain_2.build_rule(VarkRule { rule: format!("-o {} -j {}", interface_name, "DROP"), position: Some(ind), td_policy: Some(TeardownPolicy::OnComplete), }); // NETAVARK_ISOLATION_3 -j NETAVARK_ISOLATION_2 netavark_isolation_chain_3.build_rule(VarkRule { rule: format!("-j {NETAVARK_ISOLATION_2}"), position: Some(ind), td_policy: Some(TeardownPolicy::Never), }); ind += 1; // PUSH CHAIN chains.push(netavark_isolation_chain_1); } else { // create DROP rule for non-isolations to enforce strict isolation rules. // NETAVARK_ISOLATION_3 -o bridge_name -j DROP netavark_isolation_chain_3.build_rule(VarkRule { rule: format!("-o {} -j {}", interface_name, "DROP"), position: Some(ind), td_policy: Some(TeardownPolicy::OnComplete), }); // NETAVARK_ISOLATION_3 -j NETAVARK_ISOLATION_2 netavark_isolation_chain_3.build_rule(VarkRule { rule: format!("-j {NETAVARK_ISOLATION_2}"), // position +1 to place this rule under all of NETAVARK_ISOLATION_3 DROP rules. position: Some(ind + 1), td_policy: Some(TeardownPolicy::Never), }); } // PUSH CHAIN chains.push(netavark_isolation_chain_2); chains.push(netavark_isolation_chain_3); forward_chain.build_rule(VarkRule { rule: format!("{} {}", NETAVARK_FIREWALL_RULE_BUILDER, NETAVARK_FORWARD), position: Some(ind), td_policy: Some(TeardownPolicy::Never), }); chains.push(forward_chain); // NETAVARK_FORWARD let mut netavark_forward_chain = VarkChain::new(conn, FILTER.to_string(), NETAVARK_FORWARD.to_string(), None); netavark_forward_chain.create = true; // Add NETAVARK_INPUT chain to INPUT chain input_chain.build_rule(VarkRule { rule: format!("{} {}", NETAVARK_FIREWALL_RULE_BUILDER, NETAVARK_INPUT), position: Some(1), td_policy: Some(TeardownPolicy::Never), }); chains.push(input_chain); // NETAVARK_INPUT let mut netavark_input_chain = VarkChain::new(conn, FILTER.to_string(), NETAVARK_INPUT.to_string(), None); netavark_input_chain.create = true; // Always add ACCEPT rules in firewall for dns traffic from containers // to gateway when using bridge network with internal dns. for proto in ["udp", "tcp"] { netavark_input_chain.build_rule(VarkRule::new( format!( "-p {} -s {} --dport {} -j {}", proto, network, dns_port, ACCEPT ), Some(TeardownPolicy::OnComplete), )); } chains.push(netavark_input_chain); // Drop all invalid packages, due a race the container source ip could be leaked on the local // network and we should avoid that, https://bugzilla.redhat.com/show_bug.cgi?id=2230144 // This should't harm anything so just add one global rule instead of filtering per subnet. netavark_forward_chain.build_rule(VarkRule::new( "-m conntrack --ctstate INVALID -j DROP".to_string(), Some(TeardownPolicy::Never), )); // Create incoming traffic rule // CNI did this by IP address, this is implemented per subnet netavark_forward_chain.build_rule(VarkRule::new( format!("-d {network} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT"), Some(TeardownPolicy::OnComplete), )); // Create outgoing traffic rule // CNI did this by IP address, this is implemented per subnet netavark_forward_chain.build_rule(VarkRule::new( format!("-s {network} -j ACCEPT"), Some(TeardownPolicy::OnComplete), )); chains.push(netavark_forward_chain); chains } pub fn get_port_forwarding_chains<'a>( conn: &'a IPTables, pfwd: &PortForwardConfig, container_ip: &IpAddr, network_address: &IpNet, is_ipv6: bool, ) -> NetavarkResult>> { let mut localhost_ip = "127.0.0.1"; if is_ipv6 { localhost_ip = "::1"; } let mut chains = Vec::new(); // Set up all chains let network_dn_chain_name = CONTAINER_DN_CHAIN.to_owned() + &pfwd.network_hash_name; let comment_dn_network_cid = format!( "-m comment --comment 'dnat name: {} id: {}'", pfwd.network_name, pfwd.container_id ); // // NETAVARK-HASH // NETAVARK-DN-HASH let mut netavark_hashed_dn_chain = VarkChain::new( conn, NAT.to_string(), CONTAINER_DN_CHAIN.to_string() + &pfwd.network_hash_name, Some(OnComplete), ); // NETAVARK_HOSTPORT_DNAT // We need to create that chain for prerouting/output chain rules // using it, even if there are no port mappings. let mut netavark_hostport_dn_chain = VarkChain::new( conn, NAT.to_string(), NETAVARK_HOSTPORT_DNAT.to_string(), None, ); netavark_hostport_dn_chain.create = true; // Setup one-off rules that have nothing to do with ports // PREROUTING let mut prerouting_chain = VarkChain::new(conn, NAT.to_string(), PREROUTING.to_string(), None); prerouting_chain.build_rule(VarkRule::new( format!("-j {NETAVARK_HOSTPORT_DNAT} -m addrtype --dst-type LOCAL"), Some(TeardownPolicy::Never), )); // OUTPUT let mut output_chain = VarkChain::new(conn, NAT.to_string(), OUTPUT.to_string(), None); output_chain.build_rule(VarkRule::new( format!("-j {NETAVARK_HOSTPORT_DNAT} -m addrtype --dst-type LOCAL"), Some(TeardownPolicy::Never), )); // NETAVARK-HOSTPORT-SETMARK let mut netavark_hostport_setmark = VarkChain::new( conn, NAT.to_string(), NETAVARK_HOSTPORT_SETMARK.to_string(), None, ); netavark_hostport_setmark.create = true; netavark_hostport_setmark.build_rule(VarkRule::new( format!("-j {MARK} --set-xmark {HEXMARK}/{HEXMARK}"), Some(TeardownPolicy::Never), )); chains.push(netavark_hostport_setmark); // NETAVARK-HOSTPORT-MASQ let mut netavark_hostport_masq_chain = VarkChain::new( conn, NAT.to_string(), NETAVARK_HOSTPORT_MASK.to_string(), None, ); netavark_hostport_masq_chain.create = true; netavark_hostport_masq_chain.build_rule(VarkRule::new( format!( "-j {MASQUERADE} -m comment --comment 'netavark portfw masq mark' -m mark --mark {HEXMARK}/{HEXMARK}" ), Some(TeardownPolicy::Never), )); netavark_hostport_masq_chain.create = true; chains.push(netavark_hostport_masq_chain); // POSTROUTING let mut postrouting = VarkChain::new(conn, NAT.to_string(), POSTROUTING.to_string(), None); // This rule must be in the first position postrouting.build_rule(VarkRule { rule: format!("-j {NETAVARK_HOSTPORT_MASK} "), position: Some(1), td_policy: Some(Never), }); chains.push(postrouting); // Determine if we need to create chains if pfwd.port_mappings.is_some() { netavark_hashed_dn_chain.create = true; } // Create redirection for aardvark-dns on non-standard port if pfwd.dns_port != 53 { for dns_ip in pfwd.dns_server_ips { if is_ipv6 != dns_ip.is_ipv6() { continue; } let mut ip_value = dns_ip.to_string(); if is_ipv6 { ip_value = format!("[{ip_value}]") } netavark_hostport_dn_chain.create = true; for proto in ["udp", "tcp"] { netavark_hostport_dn_chain.build_rule(VarkRule { rule: format!( "-j {} -d {} -p {} --dport {} --to-destination {}:{}", DNAT, dns_ip, proto, 53, ip_value, pfwd.dns_port ), // rule should be first otherwise another container might hijack all 53 traffic to itself position: Some(1), td_policy: Some(TeardownPolicy::OnComplete), }); } } } if let Some(ports) = pfwd.port_mappings { for i in ports { let host_ip = if i.host_ip.is_empty() { None } else { match i.host_ip.parse() { Ok(ip) => match ip { IpAddr::V4(v4) => { if is_ipv6 { continue; } if !v4.is_unspecified() { Some(IpAddr::V4(v4)) } else { None } } IpAddr::V6(v6) => { if !is_ipv6 { continue; } if !v6.is_unspecified() { Some(IpAddr::V6(v6)) } else { None } } }, Err(_) => { return Err(NetavarkError::msg(format!( "invalid host ip \"{}\" provided for port {}", i.host_ip, i.host_port, ))); } } }; // hostport dnat let is_range = i.range > 1; let mut host_port = i.host_port.to_string(); if is_range { host_port = format!("{}:{}", i.host_port, (i.host_port + (i.range - 1))) } netavark_hostport_dn_chain.build_rule(VarkRule::new( format!( // I'm leaving this commented code for now in the case // we need to revert. // "-j {} -p {} -m multiport --destination-ports {} {}", "-j {} -p {} --dport {} {}", network_dn_chain_name, i.protocol, &host_port, comment_dn_network_cid ), None, )); let mut dn_setmark_rule_localhost = format!( "-j {} -s {} -p {} --dport {}", NETAVARK_HOSTPORT_SETMARK, network_address, i.protocol, &host_port ); let mut dn_setmark_rule_subnet = format!( "-j {} -s {} -p {} --dport {}", NETAVARK_HOSTPORT_SETMARK, localhost_ip, i.protocol, &host_port ); // if a destination ip address is provided, we need to alter // the rule a bit if let Some(host_ip) = host_ip { dn_setmark_rule_localhost = format!("{dn_setmark_rule_localhost} -d {host_ip}"); dn_setmark_rule_subnet = format!("{dn_setmark_rule_subnet} -d {host_ip}"); } // dn container (the actual port usages) netavark_hashed_dn_chain.build_rule(VarkRule::new(dn_setmark_rule_localhost, None)); netavark_hashed_dn_chain.build_rule(VarkRule::new(dn_setmark_rule_subnet, None)); let mut container_ip_value = container_ip.to_string(); if is_ipv6 { container_ip_value = format!("[{container_ip_value}]") } let mut container_port = i.container_port.to_string(); if is_range { container_port = format!( "{}-{}/{}", i.container_port, (i.container_port + (i.range - 1)), i.host_port ); } let mut dnat_rule = format!( "-j {} -p {} --to-destination {}:{} --destination-port {}", DNAT, i.protocol, container_ip_value, container_port, &host_port ); // if a destination ip address is provided, we need to alter // the rule a bit if let Some(host_ip) = host_ip { dnat_rule = format!("{dnat_rule} -d {host_ip}") } netavark_hashed_dn_chain.build_rule(VarkRule::new(dnat_rule, None)); } }; // The order is important here. Be certain before changing it chains.push(netavark_hashed_dn_chain); chains.push(netavark_hostport_dn_chain); chains.push(prerouting_chain); chains.push(output_chain); Ok(chains) } containers-netavark-68510f4/src/lib.rs000066400000000000000000000002601475240420400176520ustar00rootroot00000000000000#[macro_use] extern crate serde; extern crate serde_json; pub mod commands; pub mod dhcp_proxy; pub mod dns; pub mod error; pub mod firewall; pub mod network; pub mod plugin; containers-netavark-68510f4/src/main.rs000066400000000000000000000063431475240420400200400ustar00rootroot00000000000000use std::ffi::OsString; use clap::{Parser, Subcommand}; use netavark::commands::dhcp_proxy; use netavark::commands::firewalld_reload; use netavark::commands::setup; use netavark::commands::teardown; use netavark::commands::update; use netavark::commands::version; #[derive(Parser, Debug)] #[clap(version = env!("CARGO_PKG_VERSION"))] struct Opts { /// Instead of reading from STDIN, read the configuration to be applied from the given file. #[clap(short, long)] file: Option, /// Select netavark's firewall driver // There is no suitable short argument like -F, so there is no short argument. #[clap(long, env = "NETAVARK_FW")] firewall_driver: Option, /// config directory for aardvark, usually path to a tmpfs. #[clap(short, long)] config: Option, /// Tells if current netavark invocation is for rootless container. #[clap(short, long)] rootless: Option, #[clap(short, long)] /// Path to the aardvark-dns binary. aardvark_binary: Option, /// Path to netavark plugin directories, can be set multiple times to specify more than one directory. #[clap(long, long = "plugin-directory")] plugin_directories: Option>, /// Netavark trig command #[clap(subcommand)] subcmd: SubCommand, } #[derive(Subcommand, Debug)] enum SubCommand { /// Configures the given network namespace with the given configuration. Setup(setup::Setup), /// Updates network dns servers for an already configured network. Update(update::Update), /// Undo any configuration applied via setup command. Teardown(teardown::Teardown), /// Display info about netavark. Version(version::Version), /// Start dhcp-proxy DHCPProxy(dhcp_proxy::Opts), /// Listen for the firewalld reload event and reload fw rules #[command(name = "firewalld-reload")] FirewallDReload, } fn main() { env_logger::builder().format_timestamp(None).init(); let opts = Opts::parse(); // aardvark config directory must be supplied by parent or it defaults to /tmp/aardvark let config = opts.config; let rootless = opts.rootless.unwrap_or(false); let aardvark_bin = opts .aardvark_binary .unwrap_or_else(|| OsString::from("/usr/libexec/podman/aardvark-dns")); let result = match opts.subcmd { SubCommand::Setup(setup) => setup.exec( opts.file, config, opts.firewall_driver, aardvark_bin, opts.plugin_directories, rootless, ), SubCommand::Teardown(teardown) => teardown.exec( opts.file, config, opts.firewall_driver, aardvark_bin, opts.plugin_directories, rootless, ), SubCommand::Update(mut update) => update.exec(config, aardvark_bin, rootless), SubCommand::Version(version) => version.exec(), SubCommand::DHCPProxy(proxy) => dhcp_proxy::serve(proxy), SubCommand::FirewallDReload => firewalld_reload::listen(config), }; match result { Ok(_) => {} Err(err) => { err.print_json(); std::process::exit(err.get_exit_code()); } } } #[cfg(test)] mod test; containers-netavark-68510f4/src/network/000077500000000000000000000000001475240420400202315ustar00rootroot00000000000000containers-netavark-68510f4/src/network/bridge.rs000066400000000000000000001066201475240420400220400ustar00rootroot00000000000000use std::{collections::HashMap, net::IpAddr, os::fd::BorrowedFd, sync::Once}; use ipnet::IpNet; use log::{debug, error}; use netlink_packet_route::link::{ BridgeVlanInfoFlags, InfoBridge, InfoData, InfoKind, InfoVeth, LinkAttribute, LinkInfo, LinkMessage, }; use crate::network::dhcp::{dhcp_teardown, get_dhcp_lease}; use crate::{ dns::aardvark::AardvarkEntry, error::{ErrorWrap, NetavarkError, NetavarkErrorList, NetavarkResult}, exec_netns, firewall::{ iptables::MAX_HASH_SIZE, state::{remove_fw_config, write_fw_config}, }, network::{constants, core_utils::disable_ipv6_autoconf, types}, }; use super::{ constants::{ ISOLATE_OPTION_FALSE, ISOLATE_OPTION_STRICT, ISOLATE_OPTION_TRUE, NO_CONTAINER_INTERFACE_ERROR, OPTION_HOST_INTERFACE_NAME, OPTION_ISOLATE, OPTION_METRIC, OPTION_MODE, OPTION_MTU, OPTION_NO_DEFAULT_ROUTE, OPTION_VLAN, OPTION_VRF, }, core_utils::{self, get_ipam_addresses, join_netns, parse_option, CoreUtils}, driver::{self, DriverInfo}, internal_types::{ IPAMAddresses, IsolateOption, PortForwardConfig, SetupNetwork, TearDownNetwork, TeardownPortForward, }, netlink, types::StatusBlock, }; const NO_BRIDGE_NAME_ERROR: &str = "no bridge interface name given"; #[derive(Clone, Copy, PartialEq)] enum BridgeMode { /// The bridge is managed by netavark. Managed, /// The bridge was created externally and we only add/remove veths. Unmanaged, } struct InternalData { /// interface name of the veth pair inside the container netns container_interface_name: String, /// interace name of the veth pair in the host netns host_interface_name: String, /// interface name of the bridge for on the host bridge_interface_name: String, /// static mac address mac_address: Option>, /// ip addresses ipam: IPAMAddresses, /// mtu for the network interfaces (0 if default) mtu: u32, /// if this network should be isolated from others isolate: IsolateOption, /// Route metric for any default routes added for the network metric: Option, /// Management mode of the bridge. mode: BridgeMode, /// if set, no default gateway will be added no_default_route: bool, /// sef vrf for bridge vrf: Option, /// vlan id of the interface attached to the bridge vlan: Option, } pub struct Bridge<'a> { info: DriverInfo<'a>, data: Option, } impl<'a> Bridge<'a> { pub fn new(info: DriverInfo<'a>) -> Self { Bridge { info, data: None } } } impl driver::NetworkDriver for Bridge<'_> { fn network_name(&self) -> String { self.info.network.name.clone() } fn validate(&mut self) -> NetavarkResult<()> { let bridge_name = get_interface_name(self.info.network.network_interface.clone())?; if self.info.per_network_opts.interface_name.is_empty() { return Err(NetavarkError::msg(NO_CONTAINER_INTERFACE_ERROR)); } let ipam = get_ipam_addresses(self.info.per_network_opts, self.info.network)?; let mode: Option = parse_option(&self.info.network.options, OPTION_MODE)?; let mtu: u32 = parse_option(&self.info.network.options, OPTION_MTU)?.unwrap_or(0); let isolate: IsolateOption = get_isolate_option(&self.info.network.options)?; let metric: u32 = parse_option(&self.info.network.options, OPTION_METRIC)?.unwrap_or(100); let no_default_route: bool = parse_option(&self.info.network.options, OPTION_NO_DEFAULT_ROUTE)?.unwrap_or(false); let vrf: Option = parse_option(&self.info.network.options, OPTION_VRF)?; let vlan: Option = parse_option(&self.info.network.options, OPTION_VLAN)?; let host_interface_name = parse_option( &self.info.per_network_opts.options, OPTION_HOST_INTERFACE_NAME, )? .unwrap_or_else(|| "".to_string()); let static_mac = match &self.info.per_network_opts.static_mac { Some(mac) => Some(CoreUtils::decode_address_from_hex(mac)?), None => None, }; let mode = get_bridge_mode_from_string(mode.as_deref())?; // Cannot chain both conditions with "&&" // until https://github.com/rust-lang/rust/issues/53667 is stable if ipam.dhcp_enabled { if let BridgeMode::Managed = mode { return Err(NetavarkError::msg( "cannot use dhcp ipam driver without using the option mode=unmanaged", )); } } self.data = Some(InternalData { bridge_interface_name: bridge_name, container_interface_name: self.info.per_network_opts.interface_name.clone(), host_interface_name, mac_address: static_mac, ipam, mtu, isolate, metric: Some(metric), mode, no_default_route, vrf, vlan, }); Ok(()) } fn setup( &self, netlink_sockets: (&mut netlink::Socket, &mut netlink::Socket), ) -> NetavarkResult<(StatusBlock, Option)> { let data = match &self.data { Some(d) => d, None => return Err(NetavarkError::msg("must call validate() before setup()")), }; debug!("Setup network {}", self.info.network.name); debug!( "Container interface name: {} with IP addresses {:?}", data.container_interface_name, data.ipam.container_addresses ); debug!( "Bridge name: {} with IP addresses {:?}", data.bridge_interface_name, data.ipam.gateway_addresses ); if let BridgeMode::Managed = data.mode { if !self.info.network.internal { setup_ipv4_fw_sysctl()?; if data.ipam.ipv6_enabled { setup_ipv6_fw_sysctl()?; } } } let (host_sock, netns_sock) = netlink_sockets; let container_veth_mac = create_interfaces( host_sock, netns_sock, data, self.info.network.internal, self.info.netns_host, self.info.netns_container, )?; // StatusBlock response let mut response = types::StatusBlock { dns_server_ips: Some(Vec::::new()), dns_search_domains: Some(Vec::::new()), interfaces: Some(HashMap::new()), }; // interfaces map, but we only ever expect one, for response let mut interfaces: HashMap = HashMap::new(); // if dhcp is enabled, we need to call the dhcp proxy to perform // a dhcp lease. it will also perform the IP address assignment // to the container interface. let subnets = if data.ipam.dhcp_enabled { let (subnets, dns_servers, domain_name) = get_dhcp_lease( &data.bridge_interface_name, &data.container_interface_name, self.info.netns_path, &container_veth_mac, self.info.container_hostname.as_deref().unwrap_or(""), self.info.container_id, )?; // do not overwrite dns servers set by dns podman flag if !self.info.container_dns_servers.is_some() { response.dns_server_ips = dns_servers; } if domain_name.is_some() { response.dns_search_domains = domain_name; } subnets } else { data.ipam.net_addresses.clone() }; let interface = types::NetInterface { mac_address: container_veth_mac, subnets: Option::from(subnets), }; // Add interface to interfaces (part of StatusBlock) interfaces.insert(data.container_interface_name.clone(), interface); let _ = response.interfaces.insert(interfaces); let aardvark_entry = if self.info.network.dns_enabled { let _ = response .dns_server_ips .insert(data.ipam.nameservers.clone()); // Note: this is being added so podman setup is backward compatible with the design // which we had with dnsname/dnsmasq. I believe this can be fixed in later releases. let _ = response .dns_search_domains .insert(vec![constants::PODMAN_DEFAULT_SEARCH_DOMAIN.to_string()]); let mut ipv4 = Vec::new(); let mut ipv6 = Vec::new(); for ipnet in &data.ipam.container_addresses { match ipnet.addr() { IpAddr::V4(v4) => { ipv4.push(v4); } IpAddr::V6(v6) => { ipv6.push(v6); } } } let mut names = vec![self.info.container_name.to_string()]; if let Some(n) = &self.info.per_network_opts.aliases { names.extend(n.clone()); } let gw = data .ipam .gateway_addresses .iter() .map(|ipnet| ipnet.addr()) .collect(); Some(AardvarkEntry { network_name: &self.info.network.name, container_id: self.info.container_id, network_gateways: gw, network_dns_servers: &self.info.network.network_dns_servers, container_ips_v4: ipv4, container_ips_v6: ipv6, container_names: names, container_dns_servers: self.info.container_dns_servers, is_internal: self.info.network.internal, }) } else { // If --dns-enable=false and --dns was set then return following DNS servers // in status_block so podman can use these and populate resolv.conf if let Some(container_dns_servers) = self.info.container_dns_servers { let _ = response .dns_server_ips .insert(container_dns_servers.clone()); } None }; if let BridgeMode::Managed = data.mode { // if the network is internal block routing and do not setup firewall rules if self.info.network.internal { CoreUtils::apply_sysctl_value( format!( "/proc/sys/net/ipv4/conf/{}/forwarding", data.bridge_interface_name ), "0", )?; if data.ipam.ipv6_enabled { CoreUtils::apply_sysctl_value( format!( "/proc/sys/net/ipv6/conf/{}/forwarding", data.bridge_interface_name ), "0", )?; } } else { self.setup_firewall(data)? } } Ok((response, aardvark_entry)) } fn teardown( &self, netlink_sockets: (&mut netlink::Socket, &mut netlink::Socket), ) -> NetavarkResult<()> { let mode: Option = parse_option(&self.info.network.options, OPTION_MODE)?; let mode = get_bridge_mode_from_string(mode.as_deref())?; let (host_sock, netns_sock) = netlink_sockets; let mut error_list = NetavarkErrorList::new(); dhcp_teardown(&self.info, netns_sock)?; let routes = core_utils::create_route_list(&self.info.network.routes)?; for route in routes.iter() { netns_sock .del_route(route) .unwrap_or_else(|err| error_list.push(err)) } let bridge_name = get_interface_name(self.info.network.network_interface.clone())?; let complete_teardown = match remove_link( host_sock, netns_sock, mode, &bridge_name, &self.info.per_network_opts.interface_name, ) { Ok(teardown) => teardown, Err(err) => { error_list.push(err); false } }; if !self.info.network.internal && mode == BridgeMode::Managed { match self.teardown_firewall(complete_teardown, bridge_name) { Ok(_) => {} Err(err) => { error_list.push(err); } } } if !error_list.is_empty() { return Err(NetavarkError::List(error_list)); } Ok(()) } } fn get_interface_name(name: Option) -> NetavarkResult { let name = match name { None => return Err(NetavarkError::msg(NO_BRIDGE_NAME_ERROR)), Some(n) => { if n.is_empty() { return Err(NetavarkError::msg(NO_BRIDGE_NAME_ERROR)); } n } }; Ok(name) } impl<'a> Bridge<'a> { fn get_firewall_conf( &'a self, container_addresses: &Vec, nameservers: &'a Vec, isolate: IsolateOption, bridge_name: String, ) -> NetavarkResult<(SetupNetwork, PortForwardConfig<'a>)> { let id_network_hash = CoreUtils::create_network_hash(&self.info.network.name, MAX_HASH_SIZE); let sn = SetupNetwork { subnets: self .info .network .subnets .as_ref() .map(|nets| nets.iter().map(|n| n.subnet).collect()), bridge_name, network_id: self.info.network.id.clone(), network_hash_name: id_network_hash.clone(), isolation: isolate, dns_port: self.info.dns_port, }; let mut has_ipv4 = false; let mut has_ipv6 = false; let mut addr_v4: Option = None; let mut addr_v6: Option = None; let mut net_v4: Option = None; let mut net_v6: Option = None; for net in container_addresses { match net { IpNet::V4(v4) => { if has_ipv4 { continue; } addr_v4 = Some(IpAddr::V4(v4.addr())); net_v4 = Some(IpNet::new(v4.network().into(), v4.prefix_len())?); has_ipv4 = true; } IpNet::V6(v6) => { if has_ipv6 { continue; } addr_v6 = Some(IpAddr::V6(v6.addr())); net_v6 = Some(IpNet::new(v6.network().into(), v6.prefix_len())?); has_ipv6 = true; } } } let spf = PortForwardConfig { container_id: self.info.container_id.clone(), network_id: self.info.network.id.clone(), port_mappings: self.info.port_mappings, network_name: self.info.network.name.clone(), network_hash_name: id_network_hash, container_ip_v4: addr_v4, subnet_v4: net_v4, container_ip_v6: addr_v6, subnet_v6: net_v6, dns_port: self.info.dns_port, dns_server_ips: nameservers, }; Ok((sn, spf)) } fn setup_firewall(&self, data: &InternalData) -> NetavarkResult<()> { let (sn, spf) = self.get_firewall_conf( &data.ipam.container_addresses, &data.ipam.nameservers, data.isolate, data.bridge_interface_name.clone(), )?; if !self.info.rootless { write_fw_config( self.info.config_dir, &self.info.network.id, self.info.container_id, self.info.firewall.driver_name(), &sn, &spf, )?; } let system_dbus = match zbus::blocking::Connection::system() { Ok(c) => Some(c), Err(_) => None, }; self.info.firewall.setup_network(sn, &system_dbus)?; if spf.port_mappings.is_some() { // Need to enable sysctl localnet so that traffic can pass // through localhost to containers CoreUtils::apply_sysctl_value( format!( "net.ipv4.conf.{}.route_localnet", data.bridge_interface_name ), "1", )?; } self.info.firewall.setup_port_forward(spf, &system_dbus)?; Ok(()) } fn teardown_firewall( &self, complete_teardown: bool, bridge_name: String, ) -> NetavarkResult<()> { // we have to allocate the vecoros here in the top level to avoid // "borrow later used" problems let (container_addresses, nameservers); let (container_addresses_ref, nameservers_ref, isolate) = match &self.data { Some(d) => (&d.ipam.container_addresses, &d.ipam.nameservers, d.isolate), None => { let isolate = get_isolate_option(&self.info.network.options).unwrap_or_else(|e| { // just log we still try to do as much as possible for cleanup error!("failed to parse {} option: {}", OPTION_ISOLATE, e); IsolateOption::Never }); (container_addresses, nameservers) = match get_ipam_addresses(self.info.per_network_opts, self.info.network) { Ok(i) => (i.container_addresses, i.nameservers), Err(e) => { // just log we still try to do as much as possible for cleanup error!("failed to parse ipam options: {}", e); (Vec::new(), Vec::new()) } }; (&container_addresses, &nameservers, isolate) } }; let (sn, spf) = self.get_firewall_conf( container_addresses_ref, nameservers_ref, isolate, bridge_name, )?; let tn = TearDownNetwork { config: sn, complete_teardown, }; if !self.info.rootless { // IMPORTANT: This must happen before we actually teardown rules. remove_fw_config( self.info.config_dir, &self.info.network.id, self.info.container_id, complete_teardown, )?; } if complete_teardown { // FIXME store error and continue self.info.firewall.teardown_network(tn)?; } let tpf = TeardownPortForward { config: spf, complete_teardown, }; self.info.firewall.teardown_port_forward(tpf)?; Ok(()) } } // sysctl forward static IPV4_FORWARD_ONCE: Once = Once::new(); static IPV6_FORWARD_ONCE: Once = Once::new(); const IPV4_FORWARD: &str = "net.ipv4.ip_forward"; const IPV6_FORWARD: &str = "net.ipv6.conf.all.forwarding"; fn setup_ipv4_fw_sysctl() -> NetavarkResult<()> { let mut result = Ok("".to_string()); IPV4_FORWARD_ONCE.call_once(|| { result = CoreUtils::apply_sysctl_value(IPV4_FORWARD, "1"); }); match result { Ok(_) => {} Err(e) => return Err(e.into()), }; Ok(()) } fn setup_ipv6_fw_sysctl() -> NetavarkResult<()> { let mut result = Ok("".to_string()); IPV6_FORWARD_ONCE.call_once(|| { result = CoreUtils::apply_sysctl_value(IPV6_FORWARD, "1"); }); match result { Ok(_) => {} Err(e) => return Err(e.into()), }; Ok(()) } /// returns the container veth mac address fn create_interfaces( host: &mut netlink::Socket, netns: &mut netlink::Socket, data: &InternalData, internal: bool, hostns_fd: BorrowedFd<'_>, netns_fd: BorrowedFd<'_>, ) -> NetavarkResult { let (bridge_index, mac) = match host.get_link(netlink::LinkID::Name( data.bridge_interface_name.to_string(), )) { Ok(bridge) => ( validate_bridge_link( bridge, data.vlan.is_some(), host, &data.bridge_interface_name, )?, None, ), Err(err) => match err.unwrap() { NetavarkError::Netlink(e) => { if -e.raw_code() != libc::ENODEV { // if bridge does not exists we will create it below, // for all other errors we want to return the error return Err(err).wrap("get bridge interface"); } if let BridgeMode::Unmanaged = data.mode { return Err(err) .wrap("in unmanaged mode, the bridge must already exist on the host"); } let mut create_link_opts = netlink::CreateLinkOptions::new( data.bridge_interface_name.to_string(), InfoKind::Bridge, ); create_link_opts.mtu = data.mtu; if data.vlan.is_some() { create_link_opts.info_data = Some(InfoData::Bridge(vec![InfoBridge::VlanFiltering(true)])); } if let Some(vrf_name) = &data.vrf { let vrf = match host.get_link(netlink::LinkID::Name(vrf_name.to_string())) { Ok(vrf) => check_link_is_vrf(vrf, vrf_name)?, Err(err) => return Err(err).wrap("get vrf to set up bridge interface"), }; create_link_opts.primary_index = vrf.header.index; } host.create_link(create_link_opts).wrap("create bridge")?; if data.ipam.ipv6_enabled { // Disable duplicate address detection if ipv6 enabled // Do not accept Router Advertisements if ipv6 is enabled let br_accept_dad = format!( "/proc/sys/net/ipv6/conf/{}/accept_dad", &data.bridge_interface_name ); let br_accept_ra = format!("net/ipv6/conf/{}/accept_ra", &data.bridge_interface_name); CoreUtils::apply_sysctl_value(br_accept_dad, "0")?; CoreUtils::apply_sysctl_value(br_accept_ra, "0")?; } // Disable strict reverse path search validation. On RHEL it is set to strict mode // which breaks port forwarding when multiple networks are attached as the package // may be routed over a different interface on the reverse path. // As documented for the sysctl for complicated or asymmetric routing loose mode (2) // is recommended. let br_rp_filter = format!( "/proc/sys/net/ipv4/conf/{}/rp_filter", &data.bridge_interface_name ); CoreUtils::apply_sysctl_value(br_rp_filter, "2")?; let link = host .get_link(netlink::LinkID::Name( data.bridge_interface_name.to_string(), )) .wrap("get bridge interface")?; let mut mac = None; for nla in link.attributes.into_iter() { if let LinkAttribute::Address(addr) = nla { mac = Some(addr); } } if mac.is_none() { return Err(NetavarkError::msg( "failed to get the mac address from the bridge interface", )); } for addr in &data.ipam.gateway_addresses { host.add_addr(link.header.index, addr) .wrap("add ip addr to bridge")?; } host.set_up(netlink::LinkID::ID(link.header.index)) .wrap("set bridge up")?; (link.header.index, mac) } _ => return Err(err), }, }; create_veth_pair( host, netns, data, bridge_index, mac, internal, hostns_fd, netns_fd, ) } /// return the container veth mac address #[allow(clippy::too_many_arguments)] fn create_veth_pair<'fd>( host: &mut netlink::Socket, netns: &mut netlink::Socket, data: &InternalData, primary_index: u32, bridge_mac: Option>, internal: bool, hostns_fd: BorrowedFd<'fd>, netns_fd: BorrowedFd<'fd>, ) -> NetavarkResult { let mut peer_opts = netlink::CreateLinkOptions::new(data.container_interface_name.to_string(), InfoKind::Veth); peer_opts.mac = data.mac_address.clone().unwrap_or_default(); peer_opts.mtu = data.mtu; peer_opts.netns = Some(netns_fd); let mut peer = LinkMessage::default(); netlink::parse_create_link_options(&mut peer, peer_opts); let mut host_veth = netlink::CreateLinkOptions::new(data.host_interface_name.clone(), InfoKind::Veth); host_veth.mtu = data.mtu; host_veth.primary_index = primary_index; host_veth.info_data = Some(InfoData::Veth(InfoVeth::Peer(peer))); host.create_link(host_veth).map_err(|err| match err { NetavarkError::Netlink(ref e) if -e.raw_code() == libc::EEXIST => NetavarkError::wrap( if data.host_interface_name.is_empty() { format!( "create veth pair: interface {} already exists on container namespace", data.container_interface_name ) } else { format!( "create veth pair: interface {} already exists on container namespace or {} exists on host namespace", data.container_interface_name, data.host_interface_name, ) }, err, ), _ => NetavarkError::wrap("create veth pair", err), })?; let veth = netns .get_link(netlink::LinkID::Name( data.container_interface_name.to_string(), )) .wrap("get container veth")?; let mut mac = String::from(""); let mut host_link = 0; for nla in veth.attributes.into_iter() { if let LinkAttribute::Address(ref addr) = nla { mac = CoreUtils::encode_address_to_hex(addr); } if let LinkAttribute::Link(link) = nla { host_link = link; } } if mac.is_empty() { return Err(NetavarkError::Message( "failed to get the mac address from the container veth interface".to_string(), )); } if let Some(vid) = data.vlan { host.set_vlan_id( host_link, vid, BridgeVlanInfoFlags::Pvid | BridgeVlanInfoFlags::Untagged, )?; } if let BridgeMode::Managed = data.mode { exec_netns!(hostns_fd, netns_fd, res, { disable_ipv6_autoconf(&data.container_interface_name)?; if data.ipam.ipv6_enabled { // Disable dad inside the container too let disable_dad_in_container = format!( "/proc/sys/net/ipv6/conf/{}/accept_dad", &data.container_interface_name ); core_utils::CoreUtils::apply_sysctl_value(disable_dad_in_container, "0")?; } let enable_arp_notify = format!( "/proc/sys/net/ipv4/conf/{}/arp_notify", &data.container_interface_name ); core_utils::CoreUtils::apply_sysctl_value(enable_arp_notify, "1")?; // disable strict reverse path search validation let rp_filter = format!( "/proc/sys/net/ipv4/conf/{}/rp_filter", &data.container_interface_name ); CoreUtils::apply_sysctl_value(rp_filter, "2")?; Ok::<(), NetavarkError>(()) }); // check the result and return error res?; if data.ipam.ipv6_enabled { let host_veth = host.get_link(netlink::LinkID::ID(host_link))?; for nla in host_veth.attributes.into_iter() { if let LinkAttribute::IfName(name) = nla { // Disable dad inside on the host too let disable_dad_in_container = format!("/proc/sys/net/ipv6/conf/{name}/accept_dad"); core_utils::CoreUtils::apply_sysctl_value(disable_dad_in_container, "0")?; } } } } host.set_up(netlink::LinkID::ID(host_link)) .wrap("failed to set host veth up")?; // Ok this is extremely strange, by default the kernel will always choose the mac address with the // lowest value from all connected interfaces for the bridge. This means as our veth interfaces are // added and removed the bridge mac can change randomly which causes problems with ARP. This causes // package loss until the old incorrect ARP entry is updated with the new bridge mac which for some // reason can take a very long time, we noticed delays up to 100s. // This here forces a static mac because we explicitly requested one even though we still just only // set the same autogenerated one. Not that this must happen after the first veth interface is // connected otherwise no connectivity is possible at all and I have no idea why but CNI does it // also in the same way. if let Some(m) = bridge_mac { host.set_mac_address(netlink::LinkID::ID(primary_index), m) .wrap("set static mac on bridge")?; } for addr in &data.ipam.container_addresses { netns .add_addr(veth.header.index, addr) .wrap("add ip addr to container veth")?; } netns .set_up(netlink::LinkID::ID(veth.header.index)) .wrap("set container veth up")?; if !internal && !data.no_default_route { core_utils::add_default_routes(netns, &data.ipam.gateway_addresses, data.metric)?; } // add static routes for route in data.ipam.routes.iter() { netns.add_route(route)? } Ok(mac) } /// Make sure the LinkMessage is of type bridge and if vlan is set also checks /// that the bridge has vlan_filtering enabled and if not enables it. Returns /// the link id or errors when the link is not a bridge. fn validate_bridge_link( msg: LinkMessage, vlan: bool, netlink: &mut netlink::Socket, br_name: &str, ) -> NetavarkResult { for nla in msg.attributes.iter() { if let LinkAttribute::LinkInfo(info) = nla { // when vlan is requested also check the VlanFiltering attribute if vlan { for inf in info.iter() { if let LinkInfo::Data(data) = inf { match data { InfoData::Bridge(vec) => { // set the return value here based on the VlanFiltering state let vlan_enabled = vec .iter() .find_map(|a| { if let InfoBridge::VlanFiltering(on) = a { Some(*on) } else { None } }) .unwrap_or(false); if !vlan_enabled { // vlan filtering not enabled, enable it now netlink.set_vlan_filtering(msg.header.index, true)?; } } _ => { return Err(NetavarkError::Message(format!( "bridge interface {br_name} doesn't contain any bridge data", ))) } } break; } } } for inf in info.iter() { if let LinkInfo::Kind(kind) = inf { if *kind == InfoKind::Bridge { return Ok(msg.header.index); } else { return Err(NetavarkError::Message(format!( "bridge interface {br_name} already exists but is a {kind:?} interface" ))); } } } } } Err(NetavarkError::Message(format!( "could not determine namespace link kind for bridge {br_name}" ))) } /// make sure the LinkMessage is the kind VRF fn check_link_is_vrf(msg: LinkMessage, vrf_name: &str) -> NetavarkResult { for nla in msg.attributes.iter() { if let LinkAttribute::LinkInfo(info) = nla { for inf in info.iter() { if let LinkInfo::Kind(kind) = inf { if *kind == InfoKind::Vrf { return Ok(msg); } else { return Err(NetavarkError::Message(format!( "vrf {} already exists but is a {:?} interface", vrf_name, kind ))); } } } } } Err(NetavarkError::Message(format!( "could not determine namespace link kind for vrf {}", vrf_name ))) } fn remove_link( host: &mut netlink::Socket, netns: &mut netlink::Socket, mode: BridgeMode, br_name: &str, container_veth_name: &str, ) -> NetavarkResult { netns .del_link(netlink::LinkID::Name(container_veth_name.to_string())) .wrap(format!( "failed to delete container veth {container_veth_name}" ))?; let br = host .get_link(netlink::LinkID::Name(br_name.to_string())) .wrap("failed to get bridge interface")?; let links = host .dump_links(&mut vec![LinkAttribute::Controller(br.header.index)]) .wrap("failed to get connected bridge interfaces")?; // no connected interfaces on that bridge we can remove it if links.is_empty() { if let BridgeMode::Managed = mode { log::info!("removing bridge {}", br_name); host.del_link(netlink::LinkID::ID(br.header.index)) .wrap(format!("failed to delete bridge {container_veth_name}"))?; return Ok(true); } } Ok(false) } fn get_isolate_option(opts: &Option>) -> NetavarkResult { let isolate = parse_option(opts, OPTION_ISOLATE)?.unwrap_or(ISOLATE_OPTION_FALSE.to_string()); // return isolate option value "false" if unknown value or no value passed Ok(match isolate.as_str() { ISOLATE_OPTION_STRICT => IsolateOption::Strict, ISOLATE_OPTION_TRUE => IsolateOption::Normal, ISOLATE_OPTION_FALSE => IsolateOption::Never, _ => IsolateOption::Never, }) } fn get_bridge_mode_from_string(mode: Option<&str>) -> NetavarkResult { match mode { // default to l3 when unset None | Some("") | Some("managed") => Ok(BridgeMode::Managed), Some("unmanaged") => Ok(BridgeMode::Unmanaged), Some(name) => Err(NetavarkError::msg(format!( "invalid bridge mode \"{name}\"" ))), } } containers-netavark-68510f4/src/network/constants.rs000066400000000000000000000023721475240420400226170ustar00rootroot00000000000000//Following module contains all the network constants // default search domain pub static PODMAN_DEFAULT_SEARCH_DOMAIN: &str = "dns.podman"; // IPAM drivers pub const IPAM_HOSTLOCAL: &str = "host-local"; pub const IPAM_DHCP: &str = "dhcp"; pub const IPAM_NONE: &str = "none"; pub const DRIVER_BRIDGE: &str = "bridge"; pub const DRIVER_IPVLAN: &str = "ipvlan"; pub const DRIVER_MACVLAN: &str = "macvlan"; pub const OPTION_ISOLATE: &str = "isolate"; pub const ISOLATE_OPTION_TRUE: &str = "true"; pub const ISOLATE_OPTION_FALSE: &str = "false"; pub const ISOLATE_OPTION_STRICT: &str = "strict"; pub const OPTION_MTU: &str = "mtu"; pub const OPTION_MODE: &str = "mode"; pub const OPTION_METRIC: &str = "metric"; pub const OPTION_NO_DEFAULT_ROUTE: &str = "no_default_route"; pub const OPTION_BCLIM: &str = "bclim"; pub const OPTION_VRF: &str = "vrf"; pub const OPTION_VLAN: &str = "vlan"; pub const OPTION_HOST_INTERFACE_NAME: &str = "host_interface_name"; /// 100 is the default metric for most Linux networking tools. pub const DEFAULT_METRIC: u32 = 100; pub const NO_CONTAINER_INTERFACE_ERROR: &str = "no container interface name given"; /// make sure this is the same rootful default as used in podman. pub const DEFAULT_CONFIG_DIR: &str = "/run/containers/networks"; containers-netavark-68510f4/src/network/core_utils.rs000066400000000000000000000345561475240420400227640ustar00rootroot00000000000000use crate::error::{ErrorWrap, NetavarkError, NetavarkResult}; use crate::network::{constants, internal_types, types}; use crate::wrap; use ipnet::IpNet; use log::debug; use netlink_packet_route::link::{IpVlanMode, MacVlanMode}; use nix::sched; use sha2::{Digest, Sha512}; use std::collections::HashMap; use std::env; use std::fmt::Display; use std::fs::File; use std::io::{self, Error}; use std::net::IpAddr; use std::net::Ipv4Addr; use std::net::Ipv6Addr; use std::os::unix::prelude::*; use std::str::FromStr; use sysctl::{Sysctl, SysctlError}; use super::netlink; use netlink_packet_route::link::LinkAttribute; pub struct CoreUtils { pub networkns: String, } pub fn get_netavark_dns_port() -> Result { match env::var("NETAVARK_DNS_PORT") { Ok(port_string) => match port_string.parse() { Ok(port) => Ok(port), Err(e) => Err(NetavarkError::Message(format!( "Invalid NETAVARK_DNS_PORT {port_string}: {e}" ))), }, Err(_) => Ok(53), } } pub fn parse_option( opts: &Option>, name: &str, ) -> NetavarkResult> where T: FromStr, ::Err: Display, { let val = match opts.as_ref().and_then(|map| map.get(name)) { Some(val) => match val.parse::() { Ok(mtu) => mtu, Err(err) => { return Err(NetavarkError::Message(format!( "unable to parse \"{name}\": {err}" ))); } }, // if no option is set return None None => return Ok(None), }; Ok(Some(val)) } pub fn get_ipam_addresses<'a>( per_network_opts: &'a types::PerNetworkOptions, network: &'a types::Network, ) -> Result { let addresses = match network .ipam_options .as_ref() .and_then(|map| map.get("driver").cloned()) .as_deref() { // when option is none default to host local Some(constants::IPAM_HOSTLOCAL) | None => { // static ip vector let mut container_addresses = Vec::new(); // gateway ip vector let mut gateway_addresses = Vec::new(); // network addresses for response let mut net_addresses: Vec = Vec::new(); // bool for ipv6 let mut ipv6_enabled = false; // nameservers which can be configured for this container let mut nameservers: Vec = Vec::new(); let static_ips = match per_network_opts.static_ips.as_ref() { None => { return Err(std::io::Error::new( std::io::ErrorKind::Other, "no static ips provided", )) } Some(i) => i, }; // prepare a vector of static aps with appropriate cidr for (idx, subnet) in network.subnets.iter().flatten().enumerate() { let subnet_mask_cidr = subnet.subnet.prefix_len(); if let Some(gw) = subnet.gateway { let gw_net = match ipnet::IpNet::new(gw, subnet_mask_cidr) { Ok(dest) => dest, Err(err) => { return Err(std::io::Error::new( std::io::ErrorKind::Other, format!("failed to parse address {gw}/{subnet_mask_cidr}: {err}"), )) } }; gateway_addresses.push(gw_net); nameservers.push(gw); } // for dual-stack network.ipv6_enabled could be false do explicit check if subnet.subnet.addr().is_ipv6() { ipv6_enabled = true; } // Build up response information let container_address: ipnet::IpNet = match format!("{}/{}", static_ips[idx], subnet_mask_cidr).parse() { Ok(i) => i, Err(e) => { return Err(Error::new(std::io::ErrorKind::Other, e)); } }; // Add the IP to the address_vector container_addresses.push(container_address); net_addresses.push(types::NetAddress { gateway: subnet.gateway, ipnet: container_address, }); } let routes: Vec = match create_route_list(&network.routes) { Ok(r) => r, Err(e) => { return Err(Error::new(std::io::ErrorKind::Other, e)); } }; internal_types::IPAMAddresses { container_addresses, dhcp_enabled: false, gateway_addresses, routes, net_addresses, nameservers, ipv6_enabled, } } Some(constants::IPAM_NONE) => { // no ipam just return empty vectors internal_types::IPAMAddresses { container_addresses: vec![], dhcp_enabled: false, gateway_addresses: vec![], routes: vec![], net_addresses: vec![], nameservers: vec![], ipv6_enabled: false, } } Some(constants::IPAM_DHCP) => internal_types::IPAMAddresses { container_addresses: vec![], dhcp_enabled: true, gateway_addresses: vec![], routes: vec![], ipv6_enabled: false, net_addresses: vec![], nameservers: vec![], }, Some(driver) => { return Err(std::io::Error::new( std::io::ErrorKind::Other, format!("unsupported ipam driver {driver}"), )); } }; Ok(addresses) } impl CoreUtils { pub fn encode_address_to_hex(bytes: &[u8]) -> String { let address: String = bytes .iter() .map(|x| format!("{x:02x}")) .collect::>() .join(":"); address } pub fn decode_address_from_hex(input: &str) -> Result, std::io::Error> { let bytes: Result, _> = input .split([':', '-']) .map(|b| u8::from_str_radix(b, 16)) .collect(); let result = match bytes { Ok(bytes) => { if bytes.len() != 6 { return Err(std::io::Error::new( std::io::ErrorKind::Other, format!("invalid mac length for address: {input}"), )); } bytes } Err(e) => { return Err(std::io::Error::new( std::io::ErrorKind::Other, format!("unable to parse mac address {input}: {e}"), )); } }; Ok(result) } pub fn get_macvlan_mode_from_string(mode: Option<&str>) -> NetavarkResult { match mode { // default to bridge when unset None | Some("") | Some("bridge") => Ok(MacVlanMode::Bridge), Some("private") => Ok(MacVlanMode::Private), Some("vepa") => Ok(MacVlanMode::Vepa), Some("passthru") => Ok(MacVlanMode::Passthrough), Some("source") => Ok(MacVlanMode::Source), // default to bridge Some(name) => Err(NetavarkError::msg(format!( "invalid macvlan mode \"{name}\"" ))), } } pub fn get_ipvlan_mode_from_string(mode: Option<&str>) -> NetavarkResult { match mode { // default to l2 when unset None | Some("") | Some("l2") => Ok(IpVlanMode::L2), Some("l3") => Ok(IpVlanMode::L3), Some("l3s") => Ok(IpVlanMode::L3S), Some(name) => Err(NetavarkError::msg(format!( "invalid ipvlan mode \"{name}\"" ))), } } pub fn create_network_hash(network_name: &str, length: usize) -> String { let mut hasher = Sha512::new(); hasher.update(network_name.as_bytes()); let result = hasher.finalize(); let hash_string = format!("{result:X}"); let response = &hash_string[0..length]; response.to_string() } /// Set a sysctl value by value's namespace. pub fn apply_sysctl_value( ns_value: impl AsRef, val: impl AsRef, ) -> Result { let ns_value = ns_value.as_ref(); let val = val.as_ref(); debug!("Setting sysctl value for {} to {}", ns_value, val); let ctl = sysctl::Ctl::new(ns_value)?; match ctl.value_string() { Ok(result) => { if result == val { return Ok(result); } } Err(e) => return Err(e), } ctl.set_value_string(val) } } pub fn join_netns(fd: Fd) -> NetavarkResult<()> { match sched::setns(fd, sched::CloneFlags::CLONE_NEWNET) { Ok(_) => Ok(()), Err(e) => Err(NetavarkError::wrap( "setns", NetavarkError::Io(io::Error::from(e)), )), } } /// safe way to join the namespace and join back to the host after the task is done /// This first arg should be the hostns fd, the second is the container ns fd. /// The third is the result variable name and the last the closure that should be /// executed in the ns. #[macro_export] macro_rules! exec_netns { ($host:expr, $netns:expr, $result:ident, $exec:expr) => { join_netns($netns)?; let $result = $exec; join_netns($host)?; }; } pub struct NamespaceOptions { /// Note we have to return the File object since the fd is only valid /// as long as the File object is valid pub file: File, pub netlink: netlink::Socket, } pub fn open_netlink_sockets( netns_path: &str, ) -> NetavarkResult<(NamespaceOptions, NamespaceOptions)> { let netns = open_netlink_socket(netns_path).wrap("open container netns")?; let hostns = open_netlink_socket("/proc/self/ns/net").wrap("open host netns")?; let host_socket = netlink::Socket::new().wrap("host netlink socket")?; exec_netns!( hostns.as_fd(), netns.as_fd(), res, netlink::Socket::new().wrap("netns netlink socket") ); let netns_sock = res?; Ok(( NamespaceOptions { file: hostns, netlink: host_socket, }, NamespaceOptions { file: netns, netlink: netns_sock, }, )) } fn open_netlink_socket(netns_path: &str) -> NetavarkResult { wrap!(File::open(netns_path), format!("open {netns_path}")) } pub fn add_default_routes( sock: &mut netlink::Socket, gws: &[ipnet::IpNet], metric: Option, ) -> NetavarkResult<()> { let mut ipv4 = false; let mut ipv6 = false; for addr in gws { let route = match addr { ipnet::IpNet::V4(v4) => { if ipv4 { continue; } ipv4 = true; netlink::Route::Ipv4 { dest: ipnet::Ipv4Net::new(Ipv4Addr::new(0, 0, 0, 0), 0)?, gw: v4.addr(), metric, } } ipnet::IpNet::V6(v6) => { if ipv6 { continue; } ipv6 = true; netlink::Route::Ipv6 { dest: ipnet::Ipv6Net::new(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), 0)?, gw: v6.addr(), metric, } } }; sock.add_route(&route) .wrap(format!("add default route {}", &route))?; } Ok(()) } pub fn create_route_list( routes: &Option>, ) -> NetavarkResult> { match routes { Some(rs) => rs .iter() .map(|r| { let gw = r.gateway; let dst = r.destination; let mtr = r.metric; match (gw, dst) { (IpAddr::V4(gw4), IpNet::V4(dst4)) => Ok(netlink::Route::Ipv4 { dest: dst4, gw: gw4, metric: mtr, }), (IpAddr::V6(gw6), IpNet::V6(dst6)) => Ok(netlink::Route::Ipv6 { dest: dst6, gw: gw6, metric: mtr, }), (IpAddr::V4(gw4), IpNet::V6(dst6)) => Err(NetavarkError::Message(format!( "Route with ipv6 destination and ipv4 gateway ({dst6} via {gw4})" ))), (IpAddr::V6(gw6), IpNet::V4(dst4)) => Err(NetavarkError::Message(format!( "Route with ipv4 destination and ipv6 gateway ({dst4} via {gw6})" ))), } }) .collect(), None => Ok(vec![]), } } pub fn disable_ipv6_autoconf(if_name: &str) -> NetavarkResult<()> { // make sure autoconf is off, we want manual config only if let Err(err) = CoreUtils::apply_sysctl_value(format!("/proc/sys/net/ipv6/conf/{if_name}/autoconf"), "0") { match err { SysctlError::NotFound(_) => { // if the sysctl is not found we likely run on a system without ipv6 // just ignore that case } // if we have a read only /proc we ignore it as well SysctlError::IoError(ref e) if e.raw_os_error() == Some(libc::EROFS) => {} _ => { return Err(NetavarkError::wrap( "failed to set autoconf sysctl", NetavarkError::Sysctl(err), )); } } }; Ok(()) } pub fn get_mac_address(v: Vec) -> NetavarkResult { for nla in v.into_iter() { if let LinkAttribute::Address(ref addr) = nla { return Ok(CoreUtils::encode_address_to_hex(addr)); } } Err(NetavarkError::msg( "failed to get the the container mac address", )) } containers-netavark-68510f4/src/network/dhcp.rs000066400000000000000000000135351475240420400215240ustar00rootroot00000000000000use crate::error::{ErrorWrap, NetavarkError, NetavarkResult}; use crate::network::types::NetAddress; use ipnet::IpNet; use std::net::IpAddr; use std::str::FromStr; use crate::dhcp_proxy::lib::g_rpc::NetworkConfig; use crate::dhcp_proxy::proxy_conf::DEFAULT_UDS_PATH; use super::driver::DriverInfo; use super::{core_utils, netlink}; pub type DhcpLeaseInfo = (Vec, Option>, Option>); /// dhcp performs the connection to the nv-proxy over grpc where it /// requests it to perform a lease via the host's network interface /// but passes it the network interface from the container netns.:w /// /// /// # Arguments /// /// * `host_network_interface`: host interface name in &str /// * `container_network_interface`: container network interface (eth0) /// * `ns_path`: path to the container netns /// * `container_macvlan_mac`: mac address of the container network interface above. /// /// returns: Result, NetavarkError> /// /// # Examples /// /// ``` /// /// ``` pub fn get_dhcp_lease( host_network_interface: &str, container_network_interface: &str, ns_path: &str, container_macvlan_mac: &str, container_hostname: &str, container_id: &str, ) -> NetavarkResult { let nvp_config = NetworkConfig { host_iface: host_network_interface.to_string(), // TODO add in domain name support domain_name: "".to_string(), host_name: container_hostname.to_string(), version: 0, ns_path: ns_path.to_string(), container_iface: container_network_interface.to_string(), container_mac_addr: container_macvlan_mac.to_string(), container_id: container_id.to_string(), }; let lease = match tokio::task::LocalSet::new().block_on( match &tokio::runtime::Builder::new_current_thread() .enable_io() .build() { Ok(r) => r, Err(e) => { return Err(NetavarkError::msg(format!("unable to build thread: {e}"))); } }, nvp_config.get_lease(DEFAULT_UDS_PATH), ) { Ok(l) => l, Err(e) => { return Err(NetavarkError::msg(format!("unable to obtain lease: {e}"))); } }; // Note: technically DHCP can return multiple gateways but // we are just plucking the one. gw may also not exist. let gw = if !lease.gateways.is_empty() { match IpAddr::from_str(&lease.gateways[0]) { Ok(g) => Some(g), Err(e) => { return Err(NetavarkError::msg(format!("bad gateway address: {e}"))); } } } else { None }; let dns_servers = if !lease.dns_servers.is_empty() { let servers = lease .dns_servers .into_iter() .map(|d| match IpAddr::from_str(&d) { Ok(d) => Ok(d), Err(e) => Err(NetavarkError::msg(format!("bad dns address: {e}"))), }) .collect::, NetavarkError>>()?; Some(servers) } else { None }; let domain_name = if !lease.domain_name.is_empty() { Some(vec![lease.domain_name]) } else { None }; let ip_addr = match IpAddr::from_str(&lease.yiaddr) { Ok(i) => i, Err(e) => return Err(NetavarkError::Message(e.to_string())), }; let subnet_mask = match std::net::Ipv4Addr::from_str(&lease.subnet_mask) { Ok(s) => s, Err(e) => return Err(NetavarkError::Message(e.to_string())), }; let prefix_len = u32::from(subnet_mask).count_ones(); let ip = match IpNet::new(ip_addr, prefix_len as u8) { Ok(i) => i, Err(e) => return Err(NetavarkError::msg(e.to_string())), }; let ns = NetAddress { gateway: gw, ipnet: ip, }; Ok((vec![ns], dns_servers, domain_name)) } pub fn release_dhcp_lease( host_network_interface: &str, container_network_interface: &str, ns_path: &str, container_macvlan_mac: &str, ) -> NetavarkResult<()> { let nvp_config = NetworkConfig { host_iface: host_network_interface.to_string(), // TODO add in domain name support domain_name: "".to_string(), host_name: "".to_string(), version: 0, ns_path: ns_path.to_string(), container_iface: container_network_interface.to_string(), container_mac_addr: container_macvlan_mac.to_string(), container_id: "".to_string(), }; match tokio::task::LocalSet::new().block_on( match &tokio::runtime::Builder::new_current_thread() .enable_io() .build() { Ok(r) => r, Err(e) => { return Err(NetavarkError::msg(format!("unable to build thread: {e}"))); } }, nvp_config.drop_lease(DEFAULT_UDS_PATH), ) { Ok(_) => {} Err(e) => { return Err(NetavarkError::Message(e.to_string())); } }; Ok(()) } pub fn dhcp_teardown(info: &DriverInfo, sock: &mut netlink::Socket) -> NetavarkResult<()> { let ipam = core_utils::get_ipam_addresses(info.per_network_opts, info.network)?; let if_name = info.per_network_opts.interface_name.clone(); // If we are using DHCP, we need to at least call to the proxy so that // the proxy's cache can get updated and the current lease can be released. if ipam.dhcp_enabled { let dev = sock.get_link(netlink::LinkID::Name(if_name)).wrap(format!( "get container interface {}", &info.per_network_opts.interface_name ))?; let container_mac_address = core_utils::get_mac_address(dev.attributes)?; release_dhcp_lease( &info.network.network_interface.clone().unwrap_or_default(), &info.per_network_opts.interface_name, info.netns_path, &container_mac_address, )? } Ok(()) } containers-netavark-68510f4/src/network/driver.rs000066400000000000000000000050421475240420400220730ustar00rootroot00000000000000use crate::{ dns::aardvark::AardvarkEntry, error::{NetavarkError, NetavarkResult}, firewall::FirewallDriver, }; use std::{ffi::OsString, net::IpAddr, os::fd::BorrowedFd, path::Path}; use super::{ bridge::Bridge, constants, netlink, plugin::PluginDriver, types::{Network, PerNetworkOptions, PortMapping, StatusBlock}, vlan::Vlan, }; use std::os::unix::fs::PermissionsExt; pub struct DriverInfo<'a> { pub firewall: &'a dyn FirewallDriver, pub container_id: &'a String, pub container_name: &'a String, pub container_dns_servers: &'a Option>, pub netns_host: BorrowedFd<'a>, pub netns_container: BorrowedFd<'a>, pub netns_path: &'a str, pub network: &'a Network, pub per_network_opts: &'a PerNetworkOptions, pub port_mappings: &'a Option>, pub dns_port: u16, pub config_dir: &'a Path, pub rootless: bool, pub container_hostname: &'a Option, } pub trait NetworkDriver { /// validate the driver options fn validate(&mut self) -> NetavarkResult<()>; /// setup the network interfaces/firewall rules for this driver fn setup( &self, netlink_sockets: (&mut netlink::Socket, &mut netlink::Socket), ) -> NetavarkResult<(StatusBlock, Option)>; /// teardown the network interfaces/firewall rules for this driver fn teardown( &self, netlink_sockets: (&mut netlink::Socket, &mut netlink::Socket), ) -> NetavarkResult<()>; /// return the network name fn network_name(&self) -> String; } pub fn get_network_driver<'a>( info: DriverInfo<'a>, plugins_directories: &Option>, ) -> NetavarkResult> { match info.network.driver.as_str() { constants::DRIVER_BRIDGE => Ok(Box::new(Bridge::new(info))), constants::DRIVER_IPVLAN | constants::DRIVER_MACVLAN => Ok(Box::new(Vlan::new(info))), name => { if let Some(dirs) = plugins_directories { for path in dirs.iter() { let path = Path::new(path).join(name); if let Ok(meta) = path.metadata() { if meta.is_file() && meta.permissions().mode() & 0o111 != 0 { return Ok(Box::new(PluginDriver::new(path, info))); } } } } Err(NetavarkError::Message(format!( "unknown network driver \"{}\"", info.network.driver ))) } } } containers-netavark-68510f4/src/network/internal_types.rs000066400000000000000000000103151475240420400236370ustar00rootroot00000000000000use super::netlink; use crate::network::types; use std::net::IpAddr; /// Teardown contains options for tearing down behind a container #[derive(Debug)] pub struct TeardownPortForward<'a> { pub config: PortForwardConfig<'a>, /// remove network related information pub complete_teardown: bool, } /// SetupNetwork contains options for setting up a container #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct SetupNetwork { /// subnets used for this network pub subnets: Option>, /// bridge interface name pub bridge_name: String, /// id for the network #[serde(default)] pub network_id: String, /// hash id for the network pub network_hash_name: String, /// isolation determines whether the network can communicate with others outside of its interface pub isolation: IsolateOption, /// port used for the dns server pub dns_port: u16, } #[derive(Debug)] pub struct TearDownNetwork { pub config: SetupNetwork, pub complete_teardown: bool, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct PortForwardConfigGeneric { /// id of container pub container_id: String, /// id of the network #[serde(default)] pub network_id: String, /// port mappings pub port_mappings: Ports, /// name of network pub network_name: String, /// hash id for the network pub network_hash_name: String, /// ipv4 address of the container to bind to. /// If multiple v4 addresses are present, use the first one for this. /// At least one of container_ip_v6 and container_ip_v6 must be set. Both can /// be set at the same time as well. pub container_ip_v4: Option, /// subnet associated with the IPv4 address. /// Must be set if v4 address is set. pub subnet_v4: Option, /// ipv6 address of the container. /// If multiple v6 addresses are present, use the first one for this. /// At least one of container_ip_v6 and container_ip_v6 must be set. Both can /// be set at the same time as well. pub container_ip_v6: Option, /// subnet associated with the ipv6 address. /// Must be set if the v6 address is set. pub subnet_v6: Option, /// port used by DNS that should create forwarding rules /// forwarding is not setup if this is 53. pub dns_port: u16, /// dns servers IPs where forwarding rule to port 53 from dns_port are necessary pub dns_server_ips: IpAddresses, } // Some trickery to define two struct one with references and one with owned data, // basically the reference version should be used everywhere and the owned version // is only needed to deserialize the json data. pub type PortForwardConfigOwned = PortForwardConfigGeneric>, Vec>; pub type PortForwardConfig<'a> = PortForwardConfigGeneric<&'a Option>, &'a Vec>; impl<'a> From<&'a PortForwardConfigOwned> for PortForwardConfig<'a> { fn from(p: &'a PortForwardConfigOwned) -> PortForwardConfig<'a> { Self { container_id: p.container_id.clone(), network_id: p.network_id.clone(), port_mappings: &p.port_mappings, network_name: p.network_name.clone(), network_hash_name: p.network_hash_name.clone(), container_ip_v4: p.container_ip_v4, subnet_v4: p.subnet_v4, container_ip_v6: p.container_ip_v6, subnet_v6: p.subnet_v6, dns_port: p.dns_port, dns_server_ips: &p.dns_server_ips, } } } /// IPAMAddresses is used to pass ipam information around pub struct IPAMAddresses { // ip addresses for netlink pub container_addresses: Vec, // if using macvlan and dhcp, then true pub dhcp_enabled: bool, pub gateway_addresses: Vec, pub routes: Vec, pub ipv6_enabled: bool, // result for podman pub net_addresses: Vec, pub nameservers: Vec, } // IsolateOption is used to select isolate option value #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum IsolateOption { Strict, Normal, Never, } containers-netavark-68510f4/src/network/mod.rs000066400000000000000000000014711475240420400213610ustar00rootroot00000000000000pub mod types; pub mod validation; use std::{ ffi::OsString, fs::File, io::{self, BufReader}, }; use crate::{ error::{NetavarkError, NetavarkResult}, wrap, }; pub mod bridge; pub mod constants; pub mod core_utils; mod dhcp; pub mod driver; pub mod internal_types; pub mod netlink; pub mod plugin; pub mod vlan; impl types::NetworkOptions { pub fn load(path: Option) -> NetavarkResult { wrap!(Self::load_inner(path), "failed to load network options") } fn load_inner(path: Option) -> Result { let opts = match path { Some(path) => serde_json::from_reader(BufReader::new(File::open(path)?)), None => serde_json::from_reader(io::stdin()), }?; Ok(opts) } } containers-netavark-68510f4/src/network/netlink.rs000066400000000000000000000474531475240420400222600ustar00rootroot00000000000000use std::{ net::{Ipv4Addr, Ipv6Addr}, os::fd::{AsFd, AsRawFd, BorrowedFd}, }; use crate::{ error::{ErrorWrap, NetavarkError, NetavarkResult}, network::constants, wrap, }; use log::{info, trace}; use netlink_packet_core::{ NetlinkHeader, NetlinkMessage, NetlinkPayload, NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_EXCL, NLM_F_REQUEST, }; use netlink_packet_route::{ address::AddressMessage, link::{ AfSpecBridge, BridgeVlanInfo, BridgeVlanInfoFlags, InfoBridge, InfoData, InfoKind, LinkAttribute, LinkFlags, LinkInfo, LinkMessage, }, route::{RouteAddress, RouteMessage, RouteProtocol, RouteScope, RouteType}, AddressFamily, RouteNetlinkMessage, }; use netlink_sys::{protocols::NETLINK_ROUTE, SocketAddr}; pub struct Socket { socket: netlink_sys::Socket, sequence_number: u32, /// buffer size for reading netlink messages, see NLMSG_GOODSIZE in the kernel buffer: [u8; 8192], } #[derive(Clone)] pub struct CreateLinkOptions<'fd> { pub name: String, kind: InfoKind, pub info_data: Option, pub mtu: u32, pub primary_index: u32, pub link: u32, pub mac: Vec, pub netns: Option>, } pub enum LinkID { ID(u32), Name(String), } pub enum Route { Ipv4 { dest: ipnet::Ipv4Net, gw: Ipv4Addr, metric: Option, }, Ipv6 { dest: ipnet::Ipv6Net, gw: Ipv6Addr, metric: Option, }, } impl std::fmt::Display for Route { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let (dest, gw, metric) = match self { Route::Ipv4 { dest, gw, metric } => ( dest.to_string(), gw.to_string(), metric.unwrap_or(constants::DEFAULT_METRIC), ), Route::Ipv6 { dest, gw, metric } => ( dest.to_string(), gw.to_string(), metric.unwrap_or(constants::DEFAULT_METRIC), ), }; write!(f, "(dest: {dest} ,gw: {gw}, metric {metric})") } } macro_rules! expect_netlink_result { ($result:expr, $count:expr) => { if $result.len() != $count { return Err(NetavarkError::msg(format!( "{}: unexpected netlink result (got {} result(s), want {})", function!(), $result.len(), $count ))); } }; } /// get the function name of the currently executed function /// taken from https://stackoverflow.com/a/63904992 macro_rules! function { () => {{ fn f() {} fn type_name_of(_: T) -> &'static str { std::any::type_name::() } let name = type_name_of(f); // Find and cut the rest of the path match &name[..name.len() - 3].rfind(':') { Some(pos) => &name[pos + 1..name.len() - 3], None => &name[..name.len() - 3], } }}; } impl Socket { pub fn new() -> NetavarkResult { let mut socket = wrap!(netlink_sys::Socket::new(NETLINK_ROUTE), "open")?; let addr = &SocketAddr::new(0, 0); wrap!(socket.bind(addr), "bind")?; wrap!(socket.connect(addr), "connect")?; Ok(Socket { socket, sequence_number: 0, buffer: [0; 8192], }) } pub fn get_link(&mut self, id: LinkID) -> NetavarkResult { let mut msg = LinkMessage::default(); match id { LinkID::ID(id) => msg.header.index = id, LinkID::Name(name) => msg.attributes.push(LinkAttribute::IfName(name)), } let mut result = self.make_netlink_request(RouteNetlinkMessage::GetLink(msg), 0)?; expect_netlink_result!(result, 1); match result.remove(0) { RouteNetlinkMessage::NewLink(m) => Ok(m), m => Err(NetavarkError::Message(format!( "unexpected netlink message type: {}", m.message_type() ))), } } pub fn create_link(&mut self, options: CreateLinkOptions) -> NetavarkResult<()> { let mut msg = LinkMessage::default(); parse_create_link_options(&mut msg, options); let result = self.make_netlink_request( RouteNetlinkMessage::NewLink(msg), NLM_F_ACK | NLM_F_EXCL | NLM_F_CREATE, )?; expect_netlink_result!(result, 0); Ok(()) } pub fn set_link_name(&mut self, id: u32, name: String) -> NetavarkResult<()> { let mut msg = LinkMessage::default(); msg.header.index = id; msg.attributes.push(LinkAttribute::IfName(name)); let result = self.make_netlink_request(RouteNetlinkMessage::SetLink(msg), NLM_F_ACK)?; expect_netlink_result!(result, 0); Ok(()) } pub fn del_link(&mut self, id: LinkID) -> NetavarkResult<()> { let mut msg = LinkMessage::default(); match id { LinkID::ID(id) => msg.header.index = id, LinkID::Name(name) => msg.attributes.push(LinkAttribute::IfName(name)), } let result = self.make_netlink_request(RouteNetlinkMessage::DelLink(msg), NLM_F_ACK)?; expect_netlink_result!(result, 0); Ok(()) } pub fn set_link_ns(&mut self, link_id: u32, netns: Fd) -> NetavarkResult<()> { let mut msg = LinkMessage::default(); msg.header.index = link_id; msg.attributes .push(LinkAttribute::NetNsFd(netns.as_fd().as_raw_fd())); let result = self.make_netlink_request(RouteNetlinkMessage::SetLink(msg), NLM_F_ACK)?; expect_netlink_result!(result, 0); Ok(()) } /// set the vlan_filtering attribute on a bridge pub fn set_vlan_filtering(&mut self, link_id: u32, vlan_filtering: bool) -> NetavarkResult<()> { let mut msg = LinkMessage::default(); msg.header.index = link_id; msg.attributes.push(LinkAttribute::LinkInfo(vec![ LinkInfo::Kind(InfoKind::Bridge), LinkInfo::Data(InfoData::Bridge(vec![InfoBridge::VlanFiltering( vlan_filtering, )])), ])); // Now idea why this must use NewLink not SetLink, I strace'd ip route // and they use newlink and which setlink here it does not error but also does not set the setting. let result = self.make_netlink_request(RouteNetlinkMessage::NewLink(msg), NLM_F_ACK)?; expect_netlink_result!(result, 0); Ok(()) } /// set the vlan id for an interface which is attached to the bridge with vlan_filtering /// Performs the equivalent of "bridge vlan add dev test vid [flags]" pub fn set_vlan_id( &mut self, link_id: u32, // vlan id vid: u16, // flags for the vlan config flags: BridgeVlanInfoFlags, ) -> NetavarkResult<()> { let mut msg = LinkMessage::default(); msg.header.interface_family = AddressFamily::Bridge; // msg.header.link_layer_type = LinkLayerType::Netrom; msg.header.index = link_id; msg.attributes .push(LinkAttribute::AfSpecBridge(vec![AfSpecBridge::VlanInfo( BridgeVlanInfo { flags, vid }, )])); let result = self.make_netlink_request(RouteNetlinkMessage::SetLink(msg), NLM_F_ACK)?; expect_netlink_result!(result, 0); Ok(()) } fn create_addr_msg(link_id: u32, addr: &ipnet::IpNet) -> AddressMessage { let mut msg = AddressMessage::default(); msg.header.index = link_id; match addr { ipnet::IpNet::V4(v4) => { msg.header.family = AddressFamily::Inet; msg.attributes .push(netlink_packet_route::address::AddressAttribute::Broadcast( v4.broadcast(), )); } ipnet::IpNet::V6(_) => { msg.header.family = AddressFamily::Inet6; } }; msg.header.prefix_len = addr.prefix_len(); msg.attributes .push(netlink_packet_route::address::AddressAttribute::Local( addr.addr(), )); msg } pub fn add_addr(&mut self, link_id: u32, addr: &ipnet::IpNet) -> NetavarkResult<()> { let msg = Self::create_addr_msg(link_id, addr); let result = match self.make_netlink_request( RouteNetlinkMessage::NewAddress(msg), NLM_F_ACK | NLM_F_EXCL | NLM_F_CREATE, ) { Ok(result) => result, Err(err) => match err { // kernel returns EACCES when we try to add an ipv6 but ipv6 is disabled in the kernel NetavarkError::Netlink(ref e) if -e.raw_code() == libc::EACCES => match addr { ipnet::IpNet::V6(_) => { return Err(NetavarkError::wrap( "failed to add ipv6 address, is ipv6 enabled in the kernel?", err, )); } _ => return Err(err), }, err => return Err(err), }, }; expect_netlink_result!(result, 0); Ok(()) } pub fn del_addr(&mut self, link_id: u32, addr: &ipnet::IpNet) -> NetavarkResult<()> { let msg = Self::create_addr_msg(link_id, addr); let result = self.make_netlink_request(RouteNetlinkMessage::DelAddress(msg), NLM_F_ACK)?; expect_netlink_result!(result, 0); Ok(()) } fn create_route_msg(route: &Route) -> RouteMessage { let mut msg = RouteMessage::default(); msg.header.table = libc::RT_TABLE_MAIN; msg.header.protocol = RouteProtocol::Static; msg.header.scope = RouteScope::Universe; msg.header.kind = RouteType::Unicast; let (dest, dest_prefix, gateway, final_metric) = match route { Route::Ipv4 { dest, gw, metric } => { msg.header.address_family = AddressFamily::Inet; ( RouteAddress::Inet(dest.addr()), dest.prefix_len(), RouteAddress::Inet(*gw), metric.unwrap_or(constants::DEFAULT_METRIC), ) } Route::Ipv6 { dest, gw, metric } => { msg.header.address_family = AddressFamily::Inet6; ( RouteAddress::Inet6(dest.addr()), dest.prefix_len(), RouteAddress::Inet6(*gw), metric.unwrap_or(constants::DEFAULT_METRIC), ) } }; msg.header.destination_prefix_length = dest_prefix; msg.attributes .push(netlink_packet_route::route::RouteAttribute::Destination( dest, )); msg.attributes .push(netlink_packet_route::route::RouteAttribute::Gateway( gateway, )); msg.attributes .push(netlink_packet_route::route::RouteAttribute::Priority( final_metric, )); msg } pub fn add_route(&mut self, route: &Route) -> NetavarkResult<()> { let msg = Self::create_route_msg(route); info!("Adding route {}", route); let result = self .make_netlink_request(RouteNetlinkMessage::NewRoute(msg), NLM_F_ACK | NLM_F_CREATE)?; expect_netlink_result!(result, 0); Ok(()) } pub fn del_route(&mut self, route: &Route) -> NetavarkResult<()> { let msg = Self::create_route_msg(route); info!("Deleting route {}", route); let result = self.make_netlink_request(RouteNetlinkMessage::DelRoute(msg), NLM_F_ACK)?; expect_netlink_result!(result, 0); Ok(()) } pub fn dump_routes(&mut self) -> NetavarkResult> { let mut msg = RouteMessage::default(); msg.header.table = libc::RT_TABLE_MAIN; msg.header.protocol = RouteProtocol::Unspec; msg.header.scope = RouteScope::Universe; msg.header.kind = RouteType::Unicast; let results = self.make_netlink_request(RouteNetlinkMessage::GetRoute(msg), NLM_F_DUMP | NLM_F_ACK)?; let mut routes = Vec::with_capacity(results.len()); for res in results { match res { RouteNetlinkMessage::NewRoute(m) => routes.push(m), m => { return Err(NetavarkError::Message(format!( "unexpected netlink message type: {}", m.message_type() ))) } }; } Ok(routes) } pub fn dump_links( &mut self, nlas: &mut Vec, ) -> NetavarkResult> { let mut msg = LinkMessage::default(); msg.attributes.append(nlas); let results = self.make_netlink_request(RouteNetlinkMessage::GetLink(msg), NLM_F_DUMP | NLM_F_ACK)?; let mut links = Vec::with_capacity(results.len()); for res in results { match res { RouteNetlinkMessage::NewLink(m) => links.push(m), m => { return Err(NetavarkError::Message(format!( "unexpected netlink message type: {}", m.message_type() ))) } }; } Ok(links) } pub fn dump_addresses(&mut self) -> NetavarkResult> { let msg = AddressMessage::default(); let results = self .make_netlink_request(RouteNetlinkMessage::GetAddress(msg), NLM_F_DUMP | NLM_F_ACK)?; let mut addresses = Vec::with_capacity(results.len()); for res in results { match res { RouteNetlinkMessage::NewAddress(m) => addresses.push(m), m => { return Err(NetavarkError::Message(format!( "unexpected netlink message type: {}", m.message_type() ))) } }; } Ok(addresses) } pub fn set_up(&mut self, id: LinkID) -> NetavarkResult<()> { let mut msg = LinkMessage::default(); match id { LinkID::ID(id) => msg.header.index = id, LinkID::Name(name) => msg.attributes.push(LinkAttribute::IfName(name)), } msg.header.flags = LinkFlags::Up; msg.header.change_mask = LinkFlags::Up; let result = self.make_netlink_request( RouteNetlinkMessage::SetLink(msg), NLM_F_ACK | NLM_F_EXCL | NLM_F_CREATE, )?; expect_netlink_result!(result, 0); Ok(()) } pub fn set_mac_address(&mut self, id: LinkID, mac: Vec) -> NetavarkResult<()> { let mut msg = LinkMessage::default(); match id { LinkID::ID(id) => msg.header.index = id, LinkID::Name(name) => msg.attributes.push(LinkAttribute::IfName(name)), } msg.attributes.push(LinkAttribute::Address(mac)); let result = self.make_netlink_request(RouteNetlinkMessage::SetLink(msg), NLM_F_ACK)?; expect_netlink_result!(result, 0); Ok(()) } fn make_netlink_request( &mut self, msg: RouteNetlinkMessage, flags: u16, ) -> NetavarkResult> { self.send(msg, flags).wrap("send to netlink")?; self.recv(flags & NLM_F_DUMP == NLM_F_DUMP) } fn send(&mut self, msg: RouteNetlinkMessage, flags: u16) -> NetavarkResult<()> { let mut packet = NetlinkMessage::new(NetlinkHeader::default(), NetlinkPayload::from(msg)); packet.header.flags = NLM_F_REQUEST | flags; packet.header.sequence_number = { self.sequence_number += 1; self.sequence_number }; packet.finalize(); packet.serialize(&mut self.buffer[..]); trace!("send netlink packet: {:?}", packet); self.socket.send(&self.buffer[..packet.buffer_len()], 0)?; Ok(()) } fn recv(&mut self, multi: bool) -> NetavarkResult> { let mut offset = 0; let mut result = Vec::new(); // if multi is set we expect a multi part message loop { let size = wrap!( self.socket.recv(&mut &mut self.buffer[..], 0), "recv from netlink" )?; loop { let bytes = &self.buffer[offset..]; let rx_packet: NetlinkMessage = NetlinkMessage::deserialize(bytes).map_err(|e| { NetavarkError::Message(format!( "failed to deserialize netlink message: {e}", )) })?; trace!("read netlink packet: {:?}", rx_packet); if rx_packet.header.sequence_number != self.sequence_number { return Err(NetavarkError::msg(format!( "netlink: sequence_number out of sync (got {}, want {})", rx_packet.header.sequence_number, self.sequence_number, ))); } match rx_packet.payload { NetlinkPayload::Done(_) => return Ok(result), NetlinkPayload::Error(e) => { if e.code.is_some() { return Err(e.into()); } return Ok(result); } NetlinkPayload::Noop => { return Err(NetavarkError::msg( "unimplemented netlink message type NOOP", )) } NetlinkPayload::Overrun(_) => { return Err(NetavarkError::msg( "unimplemented netlink message type OVERRUN", )) } NetlinkPayload::InnerMessage(msg) => { result.push(msg); if !multi { return Ok(result); } } _ => {} }; offset += rx_packet.header.length as usize; if offset == size || rx_packet.header.length == 0 { offset = 0; break; } } } } } impl CreateLinkOptions<'_> { pub fn new(name: String, kind: InfoKind) -> Self { CreateLinkOptions { name, kind, info_data: None, mtu: 0, primary_index: 0, link: 0, mac: vec![], netns: None, } } } pub fn parse_create_link_options(msg: &mut LinkMessage, options: CreateLinkOptions) { // add link specific data let mut link_info_nlas = vec![LinkInfo::Kind(options.kind)]; if let Some(data) = options.info_data { link_info_nlas.push(LinkInfo::Data(data)); } msg.attributes.push(LinkAttribute::LinkInfo(link_info_nlas)); // add name if !options.name.is_empty() { msg.attributes.push(LinkAttribute::IfName(options.name)); } // add mtu if options.mtu != 0 { msg.attributes.push(LinkAttribute::Mtu(options.mtu)); } // add mac address if !options.mac.is_empty() { msg.attributes.push(LinkAttribute::Address(options.mac)); } // add primary device if options.primary_index != 0 { msg.attributes .push(LinkAttribute::Controller(options.primary_index)); } // add link device if options.link != 0 { msg.attributes.push(LinkAttribute::Link(options.link)); } // add netnsfd if let Some(netns) = options.netns { msg.attributes .push(LinkAttribute::NetNsFd(netns.as_raw_fd())); } } containers-netavark-68510f4/src/network/plugin.rs000066400000000000000000000110471475240420400221000ustar00rootroot00000000000000use std::{ io::Read, path::PathBuf, process::{Command, Stdio}, }; use crate::{ dns::aardvark::AardvarkEntry, error::{ErrorWrap, JsonError, NetavarkError, NetavarkResult}, wrap, }; use super::{ driver::{DriverInfo, NetworkDriver}, types, }; pub struct PluginDriver<'a> { path: PathBuf, info: DriverInfo<'a>, } impl<'a> PluginDriver<'a> { pub fn new(path: PathBuf, info: DriverInfo<'a>) -> Self { PluginDriver { path, info } } } impl NetworkDriver for PluginDriver<'_> { fn validate(&mut self) -> NetavarkResult<()> { // Note the the plugin API does not implement validate(). // This would just add an extra fork()/exec() overhead which seems // undesirable since most times it will work without errors. Ok(()) } fn setup( &self, _netlink_sockets: (&mut super::netlink::Socket, &mut super::netlink::Socket), ) -> NetavarkResult<(types::StatusBlock, Option)> { let result = self.exec_plugin(true, self.info.netns_path).wrap(format!( "plugin {:?} failed", &self.path.file_name().unwrap_or_default() ))?; // The unwrap should be safe, only if the exec_plugin has a bug this // could fail, in which case the test should catch it. Ok((result.unwrap(), None)) } fn teardown( &self, _netlink_sockets: (&mut super::netlink::Socket, &mut super::netlink::Socket), ) -> NetavarkResult<()> { self.exec_plugin(false, self.info.netns_path).wrap(format!( "plugin {:?} failed", &self.path.file_name().unwrap_or_default() ))?; Ok(()) } fn network_name(&self) -> String { self.info.network.name.clone() } } impl PluginDriver<'_> { fn exec_plugin(&self, setup: bool, netns: &str) -> NetavarkResult> { // problem we always need to clone since you can only deserialize owned data, // it is not a problem here but for the plugin it is required. // If performance becomes a concern we could use two types for it but the // maintenance overhead does not seem worth right now. let input = types::NetworkPluginExec { container_name: self.info.container_name.clone(), container_id: self.info.container_id.clone(), port_mappings: self.info.port_mappings.clone(), network: self.info.network.clone(), network_options: self.info.per_network_opts.clone(), }; let mut child = Command::new(&self.path) .arg(if setup { "setup" } else { "teardown" }) .arg(netns) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn()?; let stdin = child.stdin.take().unwrap(); serde_json::to_writer(&stdin, &input)?; // Close stdin here to avoid that the plugin waits forever for an EOF. // And then we would wait for the child to exit which would cause a hang. drop(stdin); // Note: We need to buffer the output and then deserialize into the correct type after // the plugin exits, Since the plugin can return two different json types depending on // the exit code. let mut buffer: Vec = Vec::new(); let mut stdout = child.stdout.take().unwrap(); // Do not handle error here, we have to wait for the child first. let result = stdout.read_to_end(&mut buffer); let exit_status = wrap!(child.wait(), "wait for plugin to exit")?; if let Some(rc) = exit_status.code() { // make sure the buffer is correct wrap!(result, "read into buffer")?; if rc == 0 { // read status block and setup if setup { let status = serde_json::from_slice(&buffer)?; return Ok(Some(status)); } else { return Ok(None); } } else { // exit code not 0 => error let err: JsonError = serde_json::from_slice(&buffer)?; return Err(NetavarkError::msg(format!( "exit code {}, message: {}", rc, err.error ))); } } // If we could not get the exit code then the process was killed by a signal. // I don't think it is necessary to read and return the signal so we just return a generic error. Err(NetavarkError::msg("plugin killed by signal")) } } containers-netavark-68510f4/src/network/types.rs000066400000000000000000000217431475240420400217520ustar00rootroot00000000000000// Crate contains the types which are accepted by netavark. use ipnet::IpNet; use std::collections::HashMap; use std::net::IpAddr; // Network describes the Network attributes. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Network { /// Set up dns for this network #[serde(rename = "dns_enabled")] pub dns_enabled: bool, /// Driver for this Network, e.g. bridge, macvlan... #[serde(rename = "driver")] pub driver: String, /// ID of the Network. #[serde(rename = "id")] pub id: String, /// Internal is whether the Network should not have external routes /// to public or other Networks. #[serde(rename = "internal")] pub internal: bool, /// This network contains at least one ipv6 subnet. #[serde(rename = "ipv6_enabled")] pub ipv6_enabled: bool, /// Name of the Network. #[serde(rename = "name")] pub name: String, /// NetworkInterface is the network interface name on the host. #[serde(rename = "network_interface")] pub network_interface: Option, /// Options is a set of key-value options that have been applied to /// the Network. #[serde(rename = "options")] pub options: Option>, /// IPAM options is a set of key-value options that have been applied to /// the Network. #[serde(rename = "ipam_options")] pub ipam_options: Option>, /// Subnets to use for this network. #[serde(rename = "subnets")] pub subnets: Option>, /// Static routes to use for this network. #[serde(rename = "routes")] pub routes: Option>, /// Network DNS servers for aardvark-dns. #[serde(rename = "network_dns_servers")] pub network_dns_servers: Option>, } /// NetworkOptions for a given container. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NetworkOptions { /// The container id, used for iptables comments and ipam allocation. #[serde(rename = "container_id")] pub container_id: String, /// The container name. #[serde(rename = "container_name")] pub container_name: String, /// The container hostname. #[serde(rename = "container_hostname")] pub container_hostname: Option, /// The options used to create the interfaces with. /// The networks listed in "network_info" have to match this, /// both use the network name as key for the map. #[serde(rename = "networks")] pub networks: HashMap, /// The networks which are needed to run this. /// It has to match the networks listed in "networks", /// both use the network name as key for the map. #[serde(rename = "network_info")] pub network_info: HashMap, /// The port mappings for this container. #[serde(rename = "port_mappings")] pub port_mappings: Option>, /// Custom DNS servers for aardvark-dns. #[serde(rename = "dns_servers")] pub dns_servers: Option>, } /// PerNetworkOptions are options which should be set on a per network basis #[derive(Clone, Debug, Serialize, Deserialize)] pub struct PerNetworkOptions { /// Aliases contains a list of names which the dns server should resolve /// to this container. Should only be set when DNSEnabled is true on the Network. /// If aliases are set but there is no dns support for this network the /// network interface implementation should ignore this and NOT error. #[serde(rename = "aliases")] pub aliases: Option>, /// InterfaceName for this container. Required. #[serde(rename = "interface_name")] pub interface_name: String, /// StaticIPs for this container. #[serde(rename = "static_ips")] pub static_ips: Option>, /// MAC address for the container interface. #[serde(rename = "static_mac")] pub static_mac: Option, /// Driver-specific options for this container. #[serde(rename = "options")] pub options: Option>, } /// PortMapping is one or more ports that will be mapped into the container. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct PortMapping { /// ContainerPort is the port number that will be exposed from the /// container. #[serde(rename = "container_port")] pub container_port: u16, /// HostIP is the IP that we will bind to on the host. /// If unset, assumed to be 0.0.0.0 (all interfaces). #[serde(rename = "host_ip")] pub host_ip: String, /// HostPort is the port number that will be forwarded from the host into /// the container. #[serde(rename = "host_port")] pub host_port: u16, /// Protocol is the protocol forward. /// Must be either "tcp", "udp", and "sctp", or some combination of these /// separated by commas. /// If unset, assumed to be TCP. #[serde(rename = "protocol")] pub protocol: String, /// Range is the number of ports that will be forwarded, starting at /// HostPort and ContainerPort and counting up. /// This is 1-indexed, so 1 is assumed to be a single port (only the /// Hostport:Containerport mapping will be added), 2 is two ports (both /// Hostport:Containerport and Hostport+1:Containerport+1), etc. /// If unset, assumed to be 1 (a single port). /// Both hostport + range and containerport + range must be less than /// 65536. #[serde(rename = "range")] pub range: u16, } /// StatusBlock contains the network information about a container /// connected to one Network. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct StatusBlock { /// Aardvark supports resolving queries with /// having fewer than ndots dots. So we dont /// need this as of now. /// DNS search domains for /etc/resolv.conf #[serde(rename = "dns_search_domains")] pub dns_search_domains: Option>, /// DNS nameservers /etc/resolv.conf will be populated by these #[serde(rename = "dns_server_ips")] pub dns_server_ips: Option>, /// Interfaces contains the created network interface in the container. /// The map key is the interface name. #[serde(rename = "interfaces")] pub interfaces: Option>, } /// NetInterface contains the settings for a given network interface. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NetInterface { /// MacAddress for this Interface. #[serde(rename = "mac_address")] pub mac_address: String, /// Subnets list of assigned subnets with their gateway. #[serde(rename = "subnets")] pub subnets: Option>, } /// NetAddress contains the ip address, subnet and gateway. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NetAddress { /// Gateway for the network. This can be empty if there is no gateway, e.g. internal network. #[serde(rename = "gateway")] pub gateway: Option, /// IPNet of this NetAddress. Note that this is a subnet but it has to contain the /// actual ip of the network interface and not the network address. #[serde(rename = "ipnet")] pub ipnet: IpNet, } /// Subnet for a network. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Subnet { /// Gateway IP for this Network. #[serde(rename = "gateway")] pub gateway: Option, /// LeaseRange contains the range where IP are leased. Optional. #[serde(rename = "lease_range")] pub lease_range: Option, /// Subnet for this Network in CIDR form. #[serde(rename = "subnet")] pub subnet: IpNet, } /// Static routes for a network. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Route { /// Gateway IP for this route. #[serde(rename = "gateway")] pub gateway: IpAddr, /// Destination for this route in CIDR form. #[serde(rename = "destination")] pub destination: IpNet, /// Route Metric #[serde(rename = "metric")] pub metric: Option, } /// LeaseRange contains the range where IP are leased. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LeaseRange { /// EndIP last IP in the subnet which should be used to assign ips. #[serde(rename = "end_ip")] pub end_ip: Option, /// StartIP first IP in the subnet which should be used to assign ips. #[serde(rename = "start_ip")] pub start_ip: Option, } /// Type used for the plugin setup and teardown command #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NetworkPluginExec { /// The id for the container #[serde(rename = "container_id")] pub container_id: String, /// The name for the container #[serde(rename = "container_name")] pub container_name: String, /// The port mappings for this container. Optional #[serde(rename = "port_mappings")] pub port_mappings: Option>, /// The network config for this network pub network: Network, /// The special network options for this specific container pub network_options: PerNetworkOptions, } containers-netavark-68510f4/src/network/validation.rs000066400000000000000000000004011475240420400227240ustar00rootroot00000000000000use crate::error::NetavarkResult; use log::debug; use std::fs::File; pub fn ns_checks(file: &str) -> NetavarkResult<()> { debug!("Validating network namespace..."); // TODO check for FS_MAGIC let _ = File::open(file)?.metadata()?; Ok(()) } containers-netavark-68510f4/src/network/vlan.rs000066400000000000000000000334041475240420400215430ustar00rootroot00000000000000use log::{debug, error}; use std::os::fd::BorrowedFd; use std::{collections::HashMap, net::IpAddr}; use netlink_packet_route::link::{ InfoData, InfoIpVlan, InfoKind, InfoMacVlan, IpVlanMode, LinkAttribute, MacVlanMode, }; use rand::distr::{Alphanumeric, SampleString}; use crate::network::dhcp::{dhcp_teardown, get_dhcp_lease}; use crate::{ dns::aardvark::AardvarkEntry, error::{ErrorWrap, NetavarkError, NetavarkResult}, exec_netns, network::core_utils::{disable_ipv6_autoconf, join_netns}, }; use super::{ constants::{ NO_CONTAINER_INTERFACE_ERROR, OPTION_BCLIM, OPTION_METRIC, OPTION_MODE, OPTION_MTU, OPTION_NO_DEFAULT_ROUTE, }, core_utils::{self, get_ipam_addresses, get_mac_address, parse_option, CoreUtils}, driver::{self, DriverInfo}, internal_types::IPAMAddresses, netlink::{self, CreateLinkOptions}, types::{NetInterface, StatusBlock}, }; enum KindData { MacVlan { /// static mac address mac_address: Option>, /// macvlan mode mode: MacVlanMode, // IFLA_MACVLAN_BC_CUTOFF option if set bclim: Option, }, IpVlan { /// ipvlan mode mode: IpVlanMode, }, } impl core::fmt::Display for KindData { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { f.write_str(match self { Self::MacVlan { .. } => "macvlan", Self::IpVlan { .. } => "ipvlan", }) } } struct InternalData { /// interface name inside the container container_interface_name: String, /// interface name on the host host_interface_name: String, /// ip addresses ipam: IPAMAddresses, /// mtu for the network interfaces (0 if default) mtu: u32, /// Route metric for default routes added to the network metric: Option, /// kind-specific data kind: KindData, /// if set, no default gateway will be added no_default_route: bool, // TODO: add vlan } pub struct Vlan<'a> { info: DriverInfo<'a>, data: Option, } impl<'a> Vlan<'a> { pub fn new(info: DriverInfo<'a>) -> Self { Self { info, data: None::, } } } impl driver::NetworkDriver for Vlan<'_> { fn network_name(&self) -> String { self.info.network.name.clone() } fn validate(&mut self) -> NetavarkResult<()> { if self.info.per_network_opts.interface_name.is_empty() { return Err(NetavarkError::msg(NO_CONTAINER_INTERFACE_ERROR)); } let mode: Option = parse_option(&self.info.network.options, OPTION_MODE)?; let mut ipam = get_ipam_addresses(self.info.per_network_opts, self.info.network)?; let mtu = parse_option(&self.info.network.options, OPTION_MTU)?.unwrap_or(0); let metric = parse_option(&self.info.network.options, OPTION_METRIC)?.unwrap_or(100); let no_default_route: bool = parse_option(&self.info.network.options, OPTION_NO_DEFAULT_ROUTE)?.unwrap_or(false); // Remove gateways when marked as internal network if self.info.network.internal { ipam.gateway_addresses = Vec::new(); } self.data = Some(InternalData { container_interface_name: self.info.per_network_opts.interface_name.clone(), host_interface_name: self .info .network .network_interface .clone() .unwrap_or_default(), ipam, mtu, metric: Some(metric), kind: match self.info.network.driver.as_str() { super::constants::DRIVER_IPVLAN => KindData::IpVlan { mode: CoreUtils::get_ipvlan_mode_from_string(mode.as_deref())?, }, super::constants::DRIVER_MACVLAN => { let bclim = parse_option(&self.info.network.options, OPTION_BCLIM)?; KindData::MacVlan { mode: CoreUtils::get_macvlan_mode_from_string(mode.as_deref())?, mac_address: match &self.info.per_network_opts.static_mac { Some(mac) => Some(CoreUtils::decode_address_from_hex(mac)?), None => None, }, bclim, } } other => return Err(NetavarkError::msg(format!("unsupported VLAN type {other}"))), }, no_default_route, }); Ok(()) } fn setup( &self, netlink_sockets: (&mut netlink::Socket, &mut netlink::Socket), ) -> Result<(StatusBlock, Option), NetavarkError> { let data = match &self.data { Some(d) => d, None => return Err(NetavarkError::msg("must call validate() before setup()")), }; debug!("Setup network {}", self.info.network.name); debug!( "Container interface name: {} with IP addresses {:?}", self.info.per_network_opts.interface_name, data.ipam.container_addresses ); let (host_sock, netns_sock) = netlink_sockets; let container_vlan_mac = setup( host_sock, netns_sock, &self.info.per_network_opts.interface_name, data, self.info.netns_host, self.info.netns_container, &data.kind, )?; // StatusBlock response is what we return at the end // of all of this let mut response = StatusBlock { dns_server_ips: Some(Vec::::new()), dns_search_domains: Some(Vec::::new()), interfaces: Some(HashMap::new()), }; // interfaces map, but we only ever expect one, for response let mut interfaces: HashMap = HashMap::new(); // if dhcp is enabled, we need to call the dhcp proxy to perform // a dhcp lease. it will also perform the IP address assignment // to the macvlan interface. let subnets = if data.ipam.dhcp_enabled { let (subnets, dns_servers, domain_name) = get_dhcp_lease( &data.host_interface_name, &data.container_interface_name, self.info.netns_path, &container_vlan_mac, self.info.container_hostname.as_deref().unwrap_or(""), self.info.container_id, )?; // do not overwrite dns servers set by dns podman flag if !self.info.container_dns_servers.is_some() { response.dns_server_ips = dns_servers; } if domain_name.is_some() { response.dns_search_domains = domain_name; } subnets } else { data.ipam.net_addresses.clone() }; let interface = NetInterface { mac_address: container_vlan_mac, subnets: Option::from(subnets), }; // Add interface to interfaces (part of StatusBlock) interfaces.insert(self.info.per_network_opts.interface_name.clone(), interface); let _ = response.interfaces.insert(interfaces); Ok((response, None)) } fn teardown( &self, netlink_sockets: (&mut netlink::Socket, &mut netlink::Socket), ) -> NetavarkResult<()> { dhcp_teardown(&self.info, netlink_sockets.1)?; let routes = core_utils::create_route_list(&self.info.network.routes)?; for route in routes.iter() { netlink_sockets.1.del_route(route)?; } netlink_sockets.1.del_link(netlink::LinkID::Name( self.info.per_network_opts.interface_name.to_string(), ))?; Ok(()) } } fn setup( host: &mut netlink::Socket, netns: &mut netlink::Socket, if_name: &str, data: &InternalData, hostns_fd: BorrowedFd<'_>, netns_fd: BorrowedFd<'_>, kind_data: &KindData, ) -> NetavarkResult { let primary_ifname = match data.host_interface_name.as_ref() { "" => get_default_route_interface(host)?, host_name => host_name.to_string(), }; let link = host.get_link(netlink::LinkID::Name(primary_ifname))?; let opts = match kind_data { KindData::IpVlan { mode } => { let mut opts = CreateLinkOptions::new(if_name.to_string(), InfoKind::IpVlan); opts.mtu = data.mtu; opts.netns = Some(netns_fd); opts.link = link.header.index; opts.info_data = Some(InfoData::IpVlan(vec![InfoIpVlan::Mode(*mode)])); opts } KindData::MacVlan { mode, mac_address, bclim, } => { let mut opts = CreateLinkOptions::new(if_name.to_string(), InfoKind::MacVlan); opts.mac = mac_address.clone().unwrap_or_default(); opts.mtu = data.mtu; opts.netns = Some(netns_fd); opts.link = link.header.index; let mut mv_opts = vec![InfoMacVlan::Mode(*mode)]; if let Some(bclim) = bclim { debug!("setting macvlan bclim to {bclim}"); mv_opts.push(InfoMacVlan::BcCutoff(*bclim)) } opts.info_data = Some(InfoData::MacVlan(mv_opts)); opts } }; let mut result = host.create_link(opts.clone()); // Sigh, the kernel creates the interface first in the hostns before moving it into the netns. // Therefore it can fail with EEXIST if the name is already used on the host. Create the link // with tmp name, then rename it in the netns. // If you change the iterations here make sure to match the number in early return case below as well. for i in 0..3 { match result { // no error we can break Ok(_) => break, Err(err) => match err { NetavarkError::Netlink(ref e) if -e.raw_code() == libc::EEXIST => { let random = Alphanumeric.sample_string(&mut rand::rng(), 10); let tmp_name = "mv-".to_string() + &random; let mut opts = opts.clone(); opts.name.clone_from(&tmp_name); result = host.create_link(opts); if let Err(ref e) = result { // if last element return directly if i == 2 { return Err(NetavarkError::msg(format!( "create {kind_data} interface: {e}" ))); } // retry, error could EEXIST again because we pick a random name continue; } let link = netns .get_link(netlink::LinkID::Name(tmp_name.clone())) .wrap(format!("get tmp {kind_data} interface"))?; netns .set_link_name(link.header.index, if_name.to_string()) .wrap(format!("rename tmp {kind_data} interface")) .inspect_err(|_| { // If there is an error here most likely the name in the netns is already used, // make sure to delete the tmp interface. if let Err(err) = netns.del_link(netlink::LinkID::ID(link.header.index)) { error!( "failed to delete tmp {} link {}: {}", kind_data, tmp_name, err ); }; })?; // successful run, break out of loop break; } err => return Err(err).wrap(format!("create {kind_data} interface"))?, }, } } exec_netns!(hostns_fd, netns_fd, res, { disable_ipv6_autoconf(if_name) }); res?; // return autoconf sysctl error let dev = netns .get_link(netlink::LinkID::Name(if_name.to_string())) .wrap(format!("get {kind_data} interface"))?; for addr in &data.ipam.container_addresses { netns .add_addr(dev.header.index, addr) .wrap(format!("add ip addr to {kind_data}"))?; } netns .set_up(netlink::LinkID::ID(dev.header.index)) .wrap(format!("set {kind_data} up"))?; if !data.no_default_route { core_utils::add_default_routes(netns, &data.ipam.gateway_addresses, data.metric)?; } // add static routes for route in data.ipam.routes.iter() { netns.add_route(route)? } get_mac_address(dev.attributes) } fn get_default_route_interface(host: &mut netlink::Socket) -> NetavarkResult { let routes = host.dump_routes().wrap("dump routes")?; for route in routes { let mut dest = false; let mut out_if = 0; for nla in route.attributes { if let netlink_packet_route::route::RouteAttribute::Destination(_) = nla { dest = true; } if let netlink_packet_route::route::RouteAttribute::Oif(oif) = nla { out_if = oif; } } // if there is no dest we have a default route // return the output interface for this route if !dest && out_if > 0 { let link = host.get_link(netlink::LinkID::ID(out_if))?; let name = link.attributes.iter().find_map(|nla| { if let LinkAttribute::IfName(name) = nla { Some(name) } else { None } }); if let Some(name) = name { return Ok(name.to_owned()); } } } Err(NetavarkError::msg("failed to get default route interface")) } containers-netavark-68510f4/src/plugin.rs000066400000000000000000000073641475240420400204160ustar00rootroot00000000000000use std::{collections::HashMap, env, error::Error, io}; use serde::Serialize; use crate::{error, network::types}; pub const API_VERSION: &str = "1.0.0"; // create new boxed error with string error message, also accepts format!() style arguments #[macro_export] macro_rules! new_error { ($msg:ident) => { Box::new(std::io::Error::new(std::io::ErrorKind::Other, $msg)) }; ($($arg:tt)*) => {{ Box::new(std::io::Error::new(std::io::ErrorKind::Other, format!($($arg)*))) }}; } /// Contains info about this plugin #[derive(Serialize)] pub struct Info { /// The version of this plugin. version: String, // The api version for the netavark plugin API. api_version: String, /// Optional fields you want to be displayed for the info command #[serde(flatten)] extra_info: Option>, } impl Info { pub fn new( version: String, api_version: String, extra_info: Option>, ) -> Self { Self { version, api_version, extra_info, } } } /// Define the plugin functions pub trait Plugin { // create a network config fn create(&self, network: types::Network) -> Result>; /// set up the network configuration fn setup( &self, netns: String, opts: types::NetworkPluginExec, ) -> Result>; /// tear down the network configuration fn teardown(&self, netns: String, opts: types::NetworkPluginExec) -> Result<(), Box>; } pub struct PluginExec { plugin: P, info: Info, } impl PluginExec

{ pub fn new(plugin: P, info: Info) -> Self { PluginExec { plugin, info } } pub fn exec(&self) { match self.inner_exec() { Ok(_) => {} Err(err) => { let e = error::JsonError { error: err.to_string(), }; serde_json::to_writer(io::stdout(), &e) .unwrap_or_else(|e| println!("failed to write json error: {e}: {err}")); std::process::exit(1); } }; } fn inner_exec(&self) -> Result<(), Box> { let mut args = env::args(); args.next() .ok_or_else(|| new_error!("zero arguments given"))?; // match subcommand match args.next().as_deref() { Some("create") => { let mut network = serde_json::from_reader(io::stdin())?; network = self.plugin.create(network)?; serde_json::to_writer(io::stdout(), &network)?; } Some("setup") => { let netns = args .next() .ok_or_else(|| new_error!("netns path argument is missing"))?; let opts = serde_json::from_reader(io::stdin())?; let status_block = self.plugin.setup(netns, opts)?; serde_json::to_writer(io::stdout(), &status_block)?; } Some("teardown") => { let netns = args .next() .ok_or_else(|| new_error!("netns path argument is missing"))?; let opts = serde_json::from_reader(io::stdin())?; self.plugin.teardown(netns, opts)?; } Some("info") => self.print_info()?, Some(unknown) => { return Err(new_error!("unknown subcommand: {}", unknown)); } None => self.print_info()?, }; Ok(()) } fn print_info(&self) -> Result<(), Box> { serde_json::to_writer(io::stdout(), &self.info)?; Ok(()) } } containers-netavark-68510f4/src/proto/000077500000000000000000000000001475240420400177035ustar00rootroot00000000000000containers-netavark-68510f4/src/proto/proxy.proto000066400000000000000000000025511475240420400221540ustar00rootroot00000000000000syntax = "proto3"; package netavark_proxy; service NetavarkProxy { // Client side streaming to detect client disconnection rpc Setup(NetworkConfig) returns (Lease) {} rpc Teardown(NetworkConfig) returns (Lease) {} rpc Clean(Empty) returns (OperationResponse) {} } // Netavark sends the proxy the Network Configuration that it wants to setup message NetworkConfig { string host_iface = 1; string container_iface = 2; string container_mac_addr = 3; string domain_name = 4; string host_name = 5; Version version = 6; string ns_path = 7; string container_id = 8; } // Lease can either contain a IPv4 or IPv6 DHCP lease, and the common IP information message Lease { uint32 t1 = 1; uint32 t2 = 2; uint32 lease_time = 3; uint32 mtu = 4; string domain_name = 5; string mac_address= 6; bool isV6 = 10; string siaddr = 11; string yiaddr = 12; string srv_id = 16; string subnet_mask = 17; string broadcast_addr = 18; repeated string dns_servers = 19; repeated string gateways = 20; repeated string ntp_servers = 21; string host_name = 22; } // Empty Message to send when calling for a shutdown message Empty{} // Response to netavark on successful teardown message OperationResponse { bool success = 1; } enum Version { V4 = 0; V6 = 1; } message NvIpv4Addr { bytes octets = 1; } message NvIpv6Addr { bytes octets = 1; } containers-netavark-68510f4/src/test/000077500000000000000000000000001475240420400175175ustar00rootroot00000000000000containers-netavark-68510f4/src/test/config/000077500000000000000000000000001475240420400207645ustar00rootroot00000000000000containers-netavark-68510f4/src/test/config/portmapping.json000066400000000000000000000017321475240420400242220ustar00rootroot00000000000000{ "container_id": "ad1df727792c3041dc82c5b7791365debce6f5ada7c7a0320a2d1368e1635d30", "container_name": "ecstatic_lamarr", "port_mappings": [ { "host_ip": "", "container_port": 80, "host_port": 8080, "range": 1, "protocol": "tcp" } ], "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "aliases": [ "ad1df727792c" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "bridge", "network_interface": "podman0", "created": "2021-11-18T01:58:22.148419519Z", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/src/test/config/setupopts.test.json000066400000000000000000000015051475240420400247040ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "port_mappings": [ { "host_ip": "127.0.0.1", "container_port": 5000, "host_port": 5001, "range": 3, "protocol": "tcp" } ], "networks": { "defaultNetwork": { "interface_name": "eth0" } }, "network_info": { "defaultNetwork": { "dns_enabled": true, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": true, "name": "defaultNetwork", "network_interface": "podman0", "subnets": [ { "gateway": "192.168.43.1", "subnet": "192.168.43.0/24" } ] } } } containers-netavark-68510f4/src/test/config/setupopts2.test.json000066400000000000000000000014571475240420400247740ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "interface_name": "ethsomething0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "bridge", "network_interface": "podman0", "created": "2021-10-26T16:42:48.769170534+02:00", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/src/test/config/setupoptsmacvlan.test.json000066400000000000000000000014621475240420400262500ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "interface_name": "ethsomething0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "macvlan", "network_interface": "wlp0s20f3", "created": "2021-10-26T16:42:48.769170534+02:00", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/src/test/config/twoNetworks.json000066400000000000000000000032611475240420400242270ustar00rootroot00000000000000{ "container_id": "2d0fe608dc86d7ba65dcec724a335b618328f08daa62afd2ee7fa9d41f74e5a9", "container_name": "", "networks": { "podman1": { "static_ips": [ "10.0.0.2" ], "interface_name": "eth0" }, "podman2": { "static_ips": [ "10.1.0.2" ], "interface_name": "eth1" } }, "network_info": { "podman1": { "name": "podman1", "id": "4937f73ac0df011d4f2848d5f83f5c20b707e71a8d98789bbe80d8f64a815e79", "driver": "bridge", "network_interface": "podman1", "created": "2021-11-04T19:08:39.124321192+01:00", "subnets": [ { "subnet": "10.0.0.0/24", "gateway": "10.0.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } }, "podman2": { "name": "podman2", "id": "488a7d9be4fa72a5b80811bd847aac1d99d1a09060739b4e08687949c957cda8", "driver": "bridge", "network_interface": "podman2", "created": "2021-11-04T19:08:39.124800596+01:00", "subnets": [ { "subnet": "10.1.0.0/24", "gateway": "10.1.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/src/test/mod.rs000066400000000000000000000000371475240420400206440ustar00rootroot00000000000000pub mod netlink; pub mod test; containers-netavark-68510f4/src/test/netlink.rs000066400000000000000000000156171475240420400215430ustar00rootroot00000000000000#[cfg(test)] mod tests { use std::net::{IpAddr, Ipv4Addr}; use netavark::network::netlink::*; use netlink_packet_route::{address, link::InfoKind}; macro_rules! test_setup { () => { if !nix::unistd::getuid().is_root() { // there is no actual way to mark a test as skipped // https://internals.rust-lang.org/t/pre-rfc-skippable-tests/14611 eprintln!("test skipped, requires root"); return; } nix::sched::unshare(nix::sched::CloneFlags::CLONE_NEWNET) .expect("unshare(CLONE_NEWNET)"); }; } macro_rules! run_command { ($command:expr $(, $args:expr)*) => { std::process::Command::new($command).args([$($args),*]).output() .expect("failed to run command") }; } #[test] fn test_socket_new() { test_setup!(); assert!(Socket::new().is_ok(), "Netlink Socket::new() should work"); } #[test] fn test_add_link() { test_setup!(); let mut sock = Socket::new().expect("Socket::new()"); let name = String::from("test1"); sock.create_link(CreateLinkOptions::new(name.clone(), InfoKind::Dummy)) .expect("create link failed"); let out = String::from_utf8(run_command!("ip", "link", "show", &name).stdout) .expect("convert to string failed"); assert!(out.contains(&name), "link test1 does not exists"); } #[test] fn test_add_addr() { test_setup!(); let mut sock = Socket::new().expect("Socket::new()"); let out = run_command!("ip", "link", "add", "test1", "type", "dummy"); eprintln!("{}", String::from_utf8(out.stderr).unwrap()); assert!(out.status.success(), "failed to add link via ip"); let link = sock .get_link(LinkID::Name("test1".into())) .expect("get_link failed"); let net = "10.0.0.2/24"; sock.add_addr(link.header.index, &net.parse().unwrap()) .expect("add_addr failed"); let out = String::from_utf8(run_command!("ip", "addr", "show", "test1").stdout) .expect("convert to string failed"); assert!(out.contains(net), "addr does not exists"); } #[test] fn test_del_addr() { test_setup!(); let mut sock = Socket::new().expect("Socket::new()"); let out = run_command!("ip", "link", "add", "test1", "type", "dummy"); eprintln!("{}", String::from_utf8(out.stderr).unwrap()); assert!(out.status.success(), "failed to add link via ip"); let net = "10.0.0.2/24"; let out = run_command!("ip", "addr", "add", net, "dev", "test1"); eprintln!("{}", String::from_utf8(out.stderr).unwrap()); assert!(out.status.success(), "failed to add addr via ip"); let link = sock .get_link(LinkID::Name("test1".into())) .expect("get_link failed"); sock.del_addr(link.header.index, &net.parse().unwrap()) .expect("del_addr failed"); let out = run_command!("ip", "addr", "show", "test1"); let stdout = String::from_utf8(out.stdout).unwrap(); eprintln!("{stdout}"); assert!(out.status.success(), "failed to show addr via ip"); assert!(!stdout.contains(net), "addr does exist"); } /// This test fails because we do not have actual functioning routes in the test netns /// For some reason the kernel expects us to set different options to make it work but /// I do not want to expose them just for a test. /// With these option you could get it to work but it will not work in the actual use case /// msg.header.protocol = RTPROT_UNSPEC; /// msg.header.scope = RT_SCOPE_NOWHERE; /// msg.header.kind = RTN_UNSPEC; #[test] #[ignore] fn test_del_route() { test_setup!(); let mut sock = Socket::new().expect("Socket::new()"); let out = run_command!("ip", "link", "add", "test1", "type", "dummy"); eprintln!("{}", String::from_utf8(out.stderr).unwrap()); assert!(out.status.success(), "failed to add link via ip"); let net = "10.0.0.2/24"; let out = run_command!("ip", "addr", "add", net, "dev", "test1"); eprintln!("{}", String::from_utf8(out.stderr).unwrap()); assert!(out.status.success(), "failed to add addr via ip"); // route requires that the link is up! let out = run_command!("ip", "link", "set", "dev", "test1", "up"); eprintln!("{}", String::from_utf8(out.stderr).unwrap()); assert!(out.status.success(), "failed to set test1 up via ip"); let net = "10.0.1.0/24"; let gw = "10.0.0.2"; let out = run_command!("ip", "route", "add", net, "via", gw); eprintln!("{}", String::from_utf8(out.stderr).unwrap()); assert!(out.status.success(), "failed to add route via ip"); let out = run_command!("ip", "route", "show"); let stdout = String::from_utf8(out.stdout).unwrap(); eprintln!("{stdout}"); assert!(out.status.success(), "failed to show addr via ip"); assert!(stdout.contains(net), "route should exist"); sock.del_route(&Route::Ipv4 { dest: net.parse().unwrap(), gw: gw.parse().unwrap(), metric: None, }) .expect("del_route failed"); let out = run_command!("ip", "route", "show"); let stdout = String::from_utf8(out.stdout).unwrap(); eprintln!("{stdout}"); assert!(out.status.success(), "failed to show addr via ip"); assert!(!stdout.contains(net), "route should not exist"); } #[test] fn test_dump_addr() { test_setup!(); let mut sock = Socket::new().expect("Socket::new()"); let out = run_command!("ip", "link", "add", "test1", "type", "dummy"); eprintln!("{}", String::from_utf8(out.stderr).unwrap()); assert!(out.status.success(), "failed to add link via ip"); let net = "10.0.0.2/24"; let out = run_command!("ip", "addr", "add", net, "dev", "test1"); eprintln!("{}", String::from_utf8(out.stderr).unwrap()); assert!(out.status.success(), "failed to add addr via ip"); let out = run_command!("ip", "link", "set", "up", "lo"); eprintln!("{}", String::from_utf8(out.stderr).unwrap()); assert!(out.status.success(), "failed to set up lo via ip"); let addresses = sock.dump_addresses().expect("dump_addresses failed"); for nla in addresses[0].attributes.iter() { if let address::AddressAttribute::Address(ip) = nla { assert_eq!(ip, &IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))) } } for nla in addresses[1].attributes.iter() { if let address::AddressAttribute::Address(ip) = nla { assert_eq!(ip, &IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2))) } } } } containers-netavark-68510f4/src/test/test.rs000066400000000000000000000030661475240420400210510ustar00rootroot00000000000000//use super::*; #[cfg(test)] mod tests { use std::ffi::OsString; use netavark::network; #[test] // Test setup options loader fn test_setup_opts_load() { match network::types::NetworkOptions::load(Some(OsString::from( "src/test/config/setupopts.test.json", ))) { Ok(_) => {} Err(e) => panic!("{}", e), } } // Test if we can deserialize values correctly #[test] fn test_setup_opts_assert() { match network::types::NetworkOptions::load(Some(OsString::from( "src/test/config/setupopts.test.json", ))) { Ok(setupopts) => { assert_eq!(setupopts.container_name, "testcontainer") } Err(e) => panic!("{}", e), } } // Deserialize values correctly // Try mutating deserialized struct #[test] fn test_setup_opts_mutability() { match network::types::NetworkOptions::load(Some(OsString::from( "src/test/config/setupopts.test.json", ))) { Ok(mut setupopts) => { assert_eq!(setupopts.container_name, "testcontainer"); setupopts.container_name = "mutatedcontainername".to_string(); assert_eq!(setupopts.container_name, "mutatedcontainername"); } Err(e) => panic!("{}", e), } } // Test commands::setup::ns_checks works correctly #[test] fn test_ns_checks() { assert!(network::validation::ns_checks("src/test/config/setupopts.test.json").is_ok()); } } containers-netavark-68510f4/test-dhcp/000077500000000000000000000000001475240420400176445ustar00rootroot00000000000000containers-netavark-68510f4/test-dhcp/001-basic.bats000066400000000000000000000005461475240420400221030ustar00rootroot00000000000000#!/usr/bin/env bats -*- bats -*- # # basic netavark tests # load helpers # One might think this is a NOP and does nothing, so do I. But apparently # something really weird is going with that in CI. If we delete this file # the basic setup test will fail in CI. No idea why and I tried for to # long to make any sense of this. @test "NOP setup" { : } containers-netavark-68510f4/test-dhcp/002-setup.bats000066400000000000000000000065331475240420400221650ustar00rootroot00000000000000#!/usr/bin/env bats -*- bats -*- # # basic netavark tests # load helpers @test "basic setup" { read -r -d '\0' input_config < setup|teardown foo ``` containers-netavark-68510f4/test-dhcp/README.md000066400000000000000000000004231475240420400211220ustar00rootroot00000000000000# netavark-dhcp-proxy integration test with bats ## Running tests To run the tests locally in your sandbox, you can use one of these methods: * bats ./test/001-basic.bats # runs just the specified test * bats ./test/ # runs all ## Requirements - bats - jq containers-netavark-68510f4/test-dhcp/configfiles/000077500000000000000000000000001475240420400221345ustar00rootroot00000000000000containers-netavark-68510f4/test-dhcp/configfiles/bad_interface.json000066400000000000000000000002231475240420400255720ustar00rootroot00000000000000{ "host_iface": "badinterfacename", "mac_addr": "3c:e1:a1:c1:7a:3f", "domain_name": "example.com", "host_name": "foobar", "version": 0 } containers-netavark-68510f4/test-dhcp/configfiles/bad_mac.json000066400000000000000000000002031475240420400243700ustar00rootroot00000000000000{ "host_iface": "veth0", "mac_addr": "3c:e1:a1a:3f", "domain_name": "example.com", "host_name": "foobar", "version": 0 } containers-netavark-68510f4/test-dhcp/configfiles/basic.json000066400000000000000000000003221475240420400241050ustar00rootroot00000000000000{ "host_iface": "veth1", "container_iface": "veth0", "container_mac_addr": "66:c0:05:74:df:d0", "domain_name": "example.com", "host_name": "foobar", "version": 0, "ns_path": "/run/netns/foobar" } containers-netavark-68510f4/test-dhcp/configfiles/no_mac.json000066400000000000000000000001671475240420400242670ustar00rootroot00000000000000{ "host_iface": "veth0", "mac_addr": "", "domain_name": "example.com", "host_name": "foobar", "version": 0 } containers-netavark-68510f4/test-dhcp/dnsmasqfiles/000077500000000000000000000000001475240420400223355ustar00rootroot00000000000000containers-netavark-68510f4/test-dhcp/dnsmasqfiles/sample.conf000066400000000000000000000015721475240420400244720ustar00rootroot00000000000000# Set the interface on which dnsmasq operates. # If not set, all the interfaces is used. interface=brtest # To disable dnsmasq's DNS server functionality. port=0 # To enable dnsmasq's DHCP server functionality. dhcp-range=172.172.1.50,172.172.1.150,255.255.255.0,12h #dhcp-range=192.168.0.50,192.168.0.150,12h # Set static IPs of other PCs and the Router. dhcp-host=90:9f:44:d8:16:fc,iptime,192.168.0.1,infinite # Router dhcp-host=31:25:99:36:c2:bb,server-right,192.168.0.3,infinite # PC1 dhcp-host=ac:97:0e:f2:6f:ab,yul-x230,192.168.0.13,infinite # PC2 # Set gateway as Router. Following two lines are identical. #dhcp-option=option:router,192.168.0.1 dhcp-option=3,172.172.1.1 # Set DNS server as Router. dhcp-option=6,172.172.1.1 # Logging. log-facility=/var/log/dnsmasq.log # logfile path. log-async log-queries # log queries. log-dhcp # log dhcp related messages. containers-netavark-68510f4/test-dhcp/helpers.bash000066400000000000000000000337341475240420400221570ustar00rootroot00000000000000# -*- bash -*- CONTAINER_MAC= DNSMASQ_PIDFILE= NS_NAME= NS_PATH= PROXY_PID= SUBNET_CIDR= TMP_TESTDIR= # Netavark binary to run NETAVARK=${NETAVARK:-./bin/netavark} TESTSDIR=${TESTSDIR:-$(dirname ${BASH_SOURCE})} # export RUST_BACKTRACE so that we get a helpful stack trace export RUST_BACKTRACE=full #### Functions below are taken from podman and buildah and adapted to netavark. ################ # run_helper # Invoke args, with timeout, using BATS 'run' ################ # # Second, we use 'timeout' to abort (with a diagnostic) if something # takes too long; this is preferable to a CI hang. # # Third, we log the command run and its output. This doesn't normally # appear in BATS output, but it will if there's an error. # # Next, we check exit status. Since the normal desired code is 0, # that's the default; but the expected_rc var can override: # # expected_rc=125 run_helper nonexistent-subcommand # expected_rc=? run_helper some-other-command # let our caller check status # # Since we use the BATS 'run' mechanism, $output and $status will be # defined for our caller. # function run_helper() { # expected_rc if unset set default to 0 expected_rc="${expected_rc-0}" if [ "$expected_rc" == "?" ]; then expected_rc= fi # Remember command args, for possible use in later diagnostic messages MOST_RECENT_COMMAND="$*" # stdout is only emitted upon error; this echo is to help a debugger echo "$_LOG_PROMPT $*" # BATS hangs if a subprocess remains and keeps FD 3 open; this happens # if a process crashes unexpectedly without cleaning up subprocesses. run timeout --foreground -v --kill=10 10 "$@" 3>/dev/null # without "quotes", multiple lines are glommed together into one if [ -n "$output" ]; then echo "$output" fi if [ "$status" -ne 0 ]; then echo -n "[ rc=$status " if [ -n "$expected_rc" ]; then if [ "$status" -eq "$expected_rc" ]; then echo -n "(expected) " else echo -n "(** EXPECTED $expected_rc **) " fi fi echo "]" fi if [ "$status" -eq 124 ]; then if expr "$output" : ".*timeout: sending" >/dev/null; then # It's possible for a subtest to _want_ a timeout if [[ "$expected_rc" != "124" ]]; then echo "*** TIMED OUT ***" false fi fi fi if [ -n "$expected_rc" ]; then if [ "$status" -ne "$expected_rc" ]; then die "exit code is $status; expected $expected_rc" fi fi # unset unset expected_rc } ################ # run_in_container_netns # Run args in container netns ################ # function run_in_container_netns() { run_helper ip netns exec "${NS_NAME}" "$@" } ######### # die # Abort with helpful message ######### function die() { # FIXME: handle multi-line output echo "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" >&2 echo "#| FAIL: $*" >&2 echo "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" >&2 false } ############ # assert # Compare actual vs expected string; fail if mismatch ############ # # Compares string (default: $output) against the given string argument. # By default we do an exact-match comparison against $output, but there # are two different ways to invoke us, each with an optional description: # # xpect "EXPECT" [DESCRIPTION] # xpect "RESULT" "OP" "EXPECT" [DESCRIPTION] # # The first form (one or two arguments) does an exact-match comparison # of "$output" against "EXPECT". The second (three or four args) compares # the first parameter against EXPECT, using the given OPerator. If present, # DESCRIPTION will be displayed on test failure. # # Examples: # # xpect "this is exactly what we expect" # xpect "${lines[0]}" =~ "^abc" "first line begins with abc" # function assert() { local actual_string="$output" local operator='==' local expect_string="$1" local testname="$2" case "${#*}" in 0) die "Internal error: 'assert' requires one or more arguments" ;; 1 | 2) ;; 3 | 4) actual_string="$1" operator="$2" expect_string="$3" testname="$4" ;; *) die "Internal error: too many arguments to 'assert'" ;; esac # Comparisons. # Special case: there is no !~ operator, so fake it via '! x =~ y' local not= local actual_op="$operator" if [[ $operator == '!~' ]]; then not='!' actual_op='=~' fi if [[ $operator == '=' || $operator == '==' ]]; then # Special case: we can't use '=' or '==' inside [[ ... ]] because # the right-hand side is treated as a pattern... and '[xy]' will # not compare literally. There seems to be no way to turn that off. if [ "$actual_string" = "$expect_string" ]; then return fi elif [[ $operator == '!=' ]]; then # Same special case as above if [ "$actual_string" != "$expect_string" ]; then return fi else if eval "[[ $not \$actual_string $actual_op \$expect_string ]]"; then return elif [ $? -gt 1 ]; then die "Internal error: could not process 'actual' $operator 'expect'" fi fi # Test has failed. Get a descriptive test name. if [ -z "$testname" ]; then testname="${MOST_RECENT_BUILDAH_COMMAND:-[no test name given]}" fi # Display optimization: the typical case for 'expect' is an # exact match ('='), but there are also '=~' or '!~' or '-ge' # and the like. Omit the '=' but show the others; and always # align subsequent output lines for ease of comparison. local op='' local ws='' if [ "$operator" != '==' ]; then op="$operator " ws=$(printf "%*s" ${#op} "") fi # This is a multi-line message, which may in turn contain multi-line # output, so let's format it ourself, readably local actual_split IFS=$'\n' read -rd '' -a actual_split <<<"$actual_string" || true printf "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" >&2 printf "#| FAIL: %s\n" "$testname" >&2 printf "#| expected: %s'%s'\n" "$op" "$expect_string" >&2 printf "#| actual: %s'%s'\n" "$ws" "${actual_split[0]}" >&2 local line for line in "${actual_split[@]:1}"; do printf "#| > %s'%s'\n" "$ws" "$line" >&2 done printf "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" >&2 false } ################# # assert_json # Compare actual json vs expected string; fail if mismatch ################# # assert_json works like assert except that it accepts one extra parameter, # the jq query string. # There are two different ways to invoke us, each with an optional description: # # xpect "JQ_QUERY" "EXPECT" [DESCRIPTION] # xpect "JSON_STRING" "JQ_QUERY" "OP" "EXPECT" [DESCRIPTION] # Important this function will overwrite $output, so if you need to use the value # more than once you need to safe it in another variable. function assert_json() { local actual_json="$output" local operator='==' local jq_query="$1" local expect_string="$2" local testname="$3" case "${#*}" in 0 | 1) die "Internal error: 'assert_json' requires two or more arguments" ;; 2 | 3) ;; 4 | 5) actual_json="$1" jq_query="$2" operator="$3" expect_string="$4" testname="$5" ;; *) die "Internal error: too many arguments to 'assert_json'" ;; esac run_helper jq -r "$jq_query" <<<"$actual_json" assert "$output" "$operator" "$expect_string" "$testname" } function setup() { echo "### Setup ###" NS_PATH="/var/run/netns/$(random_string)" NS_NAME=$(basename "$NS_PATH") ip netns add "${NS_NAME}" basic_setup } function teardown() { echo "### Teardown ###" basic_teardown ip netns delete "${NS_NAME}" } function basic_teardown(){ # TODO # Make dynamic stop_proxy remove_veth "veth0" "br0" remove_bridge "br0" stop_dhcp "$DNSMASQ_PID" run_in_container_netns ip link set lo down rm -rf "$TMP_TESTDIR" } function basic_setup() { SUBNET_CIDR=$(random_subnet) set_tmpdir add_bridge "br0" add_veth "veth0" "br0" run_in_container_netns ip -j link show veth0 CONTAINER_MAC=$(echo "$output" | jq -r .[0].address) add_veth "veth1" "br0" run_in_container_netns ip link set lo up run_dhcp "$TESTSDIR/dnsmasqfiles" start_proxy } # # add_bridge # function add_bridge() { local bridge_name="$1" br_cidr=$(gateway_from_subnet "$SUBNET_CIDR") run_in_container_netns ip link add $bridge_name type bridge run_in_container_netns ip addr add $br_cidr dev $bridge_name run_in_container_netns ip link set $bridge_name up } # # remove_bridge # function remove_bridge() { local bridge_name="$1" run_in_container_netns ip link set "$bridge_name" down # shellcheck disable=SC2086 run_in_container_netns ip link del $bridge_name } # # remove_veth veth0 br0 # function remove_veth() { local veth_name="$1" local bridge_name="$2" local veth_br_name="${veth_name}br" run_in_container_netns ip link del "$veth_br_name" } # # add_veth veth0 br0 # function add_veth() { local veth_name="$1" local bridge_name="$2" local veth_br_name="${veth_name}br" run_in_container_netns ip link add "$veth_br_name" type veth peer name "$veth_name" run_in_container_netns ip link set "$veth_br_name" master "$bridge_name" run_in_container_netns ip link set "$veth_br_name" up run_in_container_netns ip link set "$veth_name" up } # # run_dhcp /var/tmp/conf # function run_dhcp() { gw=$(gateway_from_subnet "$SUBNET_CIDR") stripped_subnet=$(strip_last_octet_from_subnet) read -r -d '\0' dnsmasq_config < "$dnsmasq_testdir/test.conf" ip netns exec "${NS_NAME}" dnsmasq --log-debug --log-dhcp --no-daemon --conf-dir "${dnsmasq_testdir}" &>>"$TMP_TESTDIR/dnsmasq.log" & DNSMASQ_PID=$! } # # stop_dhcp 27231 # function stop_dhcp() { echo "dnsmasq log:" cat "${TMP_TESTDIR}/dnsmasq.log" kill -9 "$DNSMASQ_PID" } function start_proxy() { RUST_LOG=info ip netns exec "$NS_NAME" $NETAVARK dhcp-proxy --dir "$TMP_TESTDIR" --uds "$TMP_TESTDIR" &>"$TMP_TESTDIR/proxy.log" & PROXY_PID=$! } function stop_proxy(){ echo "proxy log:" cat "$TMP_TESTDIR/proxy.log" kill -9 $PROXY_PID } function run_setup(){ local conf=$1 NS_PATH=$(echo "${conf}" | jq -r .ns_path) NS_NAME=$(basename "$NS_PATH") echo "$conf" > "$TMP_TESTDIR/setup.json" run_client "setup" "${TMP_TESTDIR}/setup.json" } function run_teardown(){ local conf=$1 echo "$conf" > "$TMP_TESTDIR/teardown.json" run_client "teardown" "${TMP_TESTDIR}/teardown.json" } # The first arg is the incoming config from "netavark" ################### # run_client # use test client ################### function run_client(){ local verb=$1 local conf=$2 run_in_container_netns "./bin/netavark-dhcp-proxy-client" --uds "$TMP_TESTDIR/nv-proxy.sock" -f "${conf}" "${verb}" } ################### # random_subnet # generate a random private subnet ################### # # by default it will return a 10.x.x.0/24 ipv4 subnet # if "6" is given as first argument it will return a "fdx:x:x:x::/64" ipv6 subnet function random_subnet() { if [[ "$1" == "6" ]]; then printf "fd%x:%x:%x:%x::/64" $((RANDOM % 256)) $((RANDOM % 65535)) $((RANDOM % 65535)) $((RANDOM % 65535)) else printf "10.%d.%d.0/24" $((RANDOM % 256)) $((RANDOM % 256)) fi } ######################### # random_ip_in_subnet # get a random from a given subnet ######################### # the first arg must be an subnet created by random_subnet # otherwise this function might return an invalid ip function random_ip_in_subnet() { # first trim subnet local net_ip=${1%/*} local num= # if ip has colon it is ipv6 if [[ "$net_ip" == *":"* ]]; then # make sure to not get 0 or 1 num=$(printf "%x" $((RANDOM % 65533 + 2))) else # if ipv4 we have to trim the final 0 net_ip=${net_ip%0} # make sure to not get 0, 1 or 255 num=$(printf "%d" $((RANDOM % 252 + 2))) fi printf "$net_ip%s" $num } ######################### # gateway_from_subnet # get the first ip from a given subnet ######################### # the first arg must be an subnet created by random_subnet # otherwise this function might return an invalid ip function gateway_from_subnet() { local num=1 net_ip=$(strip_last_octet_from_subnet "$SUBNET_CIDR") printf "$net_ip%s" $num } function strip_last_octet_from_subnet() { # first trim subnet local net_ip=${SUBNET_CIDR%/*} # set first ip in network as gateway # if ip has dor it is ipv4 if [[ "$net_ip" == *"."* ]]; then # if ipv4 we have to trim the final 0 net_ip=${net_ip%0} fi printf "$net_ip" } ######################### # generate_mac # random generated mac address ######################### # No args required function generate_mac(){ openssl rand -hex 6 | sed 's/\(..\)/\1:/g; s/.$//' } function set_tmpdir(){ TMP_TESTDIR=$(mktemp -d /tmp/nv-proxyXXX) } ################### # random_string # Pseudorandom alphanumeric string of given length ################### function random_string() { local length=${1:-10} head /dev/urandom | tr -dc a-zA-Z0-9 | head -c$length } function has_ip() { local container_ip=$1 local interface=$2 run_in_container_netns ip -j address show $interface addr_info=$(jq '.[0].addr_info' <<<"$output") assert "$addr_info" =~ "$container_ip" "ip not set on interface $interface" } containers-netavark-68510f4/test-dhcp/setup.sh000077500000000000000000000007651475240420400213530ustar00rootroot00000000000000ip netns add new ip link add dev outside type veth peer name outsidebr ip link add dev inside type veth peer name insidebr ip link add brtest type bridge ip addr add 172.172.1.1/24 dev brtest ip link set outsidebr master brtest ip link set insidebr master brtest ip link set brtest up ip link set inside netns new ip link set outsidebr up ip link set insidebr up ip addr add 172.172.1.2/24 dev outside ip link set outside up ip netns exec new ip link set lo up ip netns exec new ip link set inside up containers-netavark-68510f4/test/000077500000000000000000000000001475240420400167305ustar00rootroot00000000000000containers-netavark-68510f4/test/001-basic.bats000066400000000000000000000025221475240420400211630ustar00rootroot00000000000000#!/usr/bin/env bats -*- bats -*- # # basic netavark tests # load helpers @test "netavark version" { run_netavark --version assert "$output" =~ "netavark 1\.[0-9]+\.[0-9]+(-rc|-dev)?" "expected version" run_netavark version json="$output" assert_json "$json" ".version" =~ "^1\.[0-9]+\.[0-9]+(-rc[0-9]|-dev)?" "correct version" assert_json "$json" ".commit" =~ "[0-9a-f]{40}" "shows commit sha" assert_json "$json" ".build_time" =~ "20.*" "show build date" assert_json "$json" ".target" =~ ".*" "contains target string" } @test "netavark error - invalid ns path" { expected_rc=1 run_netavark -f ${TESTSDIR}/testfiles/simplebridge.json setup /test/1 assert_json ".error" "invalid namespace path: IO error: No such file or directory (os error 2)" "Namespace path does not exists" } @test "netavark error - invalid config path" { expected_rc=1 run_netavark -f /test/1 setup $(get_container_netns_path) assert_json ".error" "failed to load network options: IO error: No such file or directory (os error 2)" "Config file does not exists" } @test "netavark - check non utf-8 paths" { # do not use run_netavark here as it sets --config run_helper $NETAVARK --config $'/tmp/\xff.test' version json="$output" assert_json "$json" ".version" =~ "^1\.[0-9]+\.[0-9]+(-rc[0-9]|-dev)?" "correct version" } containers-netavark-68510f4/test/100-bridge-iptables.bats000066400000000000000000001403051475240420400231410ustar00rootroot00000000000000#!/usr/bin/env bats -*- bats -*- # # bridge driver tests with iptables firewall driver # load helpers fw_driver=iptables @test "check iptables driver is in use" { RUST_LOG=netavark=info run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) assert "${lines[0]}" "==" "[INFO netavark::firewall] Using iptables firewall driver" "iptables driver is in use" } @test "$fw_driver - internal network" { run_in_host_netns iptables -t nat -nvL before="$output" run_netavark --file ${TESTSDIR}/testfiles/internal.json setup $(get_container_netns_path) run_in_host_netns iptables -t nat -nvL assert "$output" == "$before" "make sure tables have not changed" run_in_container_netns ip route show assert "$output" "!~" "default" "No default route for internal networks" run_in_container_netns ping -c 1 10.88.0.1 run_netavark --file ${TESTSDIR}/testfiles/internal.json teardown $(get_container_netns_path) } @test "$fw_driver - simple bridge" { run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) result="$output" assert_json "$result" 'has("podman")' == "true" "object key exists" mac=$(jq -r '.podman.interfaces.eth0.mac_address' <<<"$result") # check that interface exists run_in_container_netns ip -j --details link show eth0 link_info="$output" assert_json "$link_info" ".[].address" == "$mac" "MAC matches container mac" assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Container interface is up" assert_json "$link_info" ".[].linkinfo.info_kind" == "veth" "Container interface is a veth device" ipaddr="10.88.0.2/16" run_in_container_netns ip addr show eth0 assert "$output" =~ "$ipaddr" "IP address matches container address" assert_json "$result" ".podman.interfaces.eth0.subnets[0].ipnet" == "$ipaddr" "Result contains correct IP address" run_in_host_netns ip -j --details link show podman0 link_info="$output" assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Host bridge interface is up" assert_json "$link_info" ".[].linkinfo.info_kind" == "bridge" "The bridge interface is actually a bridge" bridge_mac=$(jq -r '.[].address' <<<"$link_info") run_in_host_netns ip -j link show veth0 veth_info="$output" assert_json "$veth_info" ".[].address" != "$bridge_mac" "Bridge and Veth must have different mac address" ipaddr="10.88.0.1" run_in_host_netns ip addr show podman0 assert "$output" =~ "$ipaddr" "IP address matches bridge gateway address" assert_json "$result" ".podman.interfaces.eth0.subnets[0].gateway" == "$ipaddr" "Result contains gateway address" # check that the loopback adapter is up run_in_container_netns ip addr show lo assert "$output" =~ "127.0.0.1" "Loopback adapter is up (has address)" run_in_host_netns ping -c 1 10.88.0.2 check_simple_bridge_iptables run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json teardown $(get_container_netns_path) # now check that iptables rules are gone run_in_host_netns iptables -S # check FORWARD rules run_in_host_netns iptables -S FORWARD assert "${lines[1]}" == "-A FORWARD -m comment --comment \"netavark firewall rules\" -j NETAVARK_FORWARD" "FORWARD rule" assert "${#lines[@]}" = 2 "too many FORWARD rules after teardown" # rule 1 should be DROP for any existing networks run_in_host_netns iptables -S NETAVARK_FORWARD assert "${lines[1]}" == "-A NETAVARK_FORWARD -m conntrack --ctstate INVALID -j DROP" "NETAVARK_FORWARD rule 1" assert "${#lines[@]}" = 2 "too many NETAVARK_FORWARD rules after teardown" # check POSTROUTING nat rules run_in_host_netns iptables -S POSTROUTING -t nat assert "${lines[1]}" =~ "-A POSTROUTING -j NETAVARK-HOSTPORT-MASQ" "POSTROUTING HOSTPORT-MASQ rule" assert "${#lines[@]}" = 2 "too many POSTROUTING rules after teardown" # NETAVARK-1D8721804F16F chain should not exists expected_rc=1 run_in_host_netns iptables -nvL NETAVARK-1D8721804F16F -t nat # bridge should be removed on teardown expected_rc=1 run_in_host_netns ip addr show podman0 } @test "$fw_driver - bridge with static routes" { # add second interface and routes through that interface to test proper teardown run_in_container_netns ip link add type dummy run_in_container_netns ip a add 10.91.0.10/24 dev dummy0 run_in_container_netns ip link set dummy0 up run_netavark --file ${TESTSDIR}/testfiles/bridge-staticroutes.json setup $(get_container_netns_path) # check static routes run_in_container_netns ip r assert "$output" "=~" "10.89.0.0/24 via 10.88.0.2" "static route not set" assert "$output" "=~" "10.90.0.0/24 via 10.88.0.3" "static route not set" assert "$output" "=~" "10.92.0.0/24 via 10.91.0.1" "static route not set" run_netavark --file ${TESTSDIR}/testfiles/bridge-staticroutes.json teardown $(get_container_netns_path) # check static routes get removed assert "$output" "!~" "10.89.0.0/24 via 10.88.0.2" "static route not set" assert "$output" "!~" "10.90.0.0/24 via 10.88.0.3" "static route not set" assert "$output" "!~" "10.92.0.0/24 via 10.91.0.1" "static route not removed" } @test "$fw_driver - bridge with no default route" { run_netavark --file ${TESTSDIR}/testfiles/bridge-nodefaultroute.json setup $(get_container_netns_path) run_in_container_netns ip r assert "$output" "!~" "default" "default route exists" run_in_container_netns ip -6 r assert "$output" "!~" "default" "default route exists" run_netavark --file ${TESTSDIR}/testfiles/bridge-nodefaultroute.json teardown $(get_container_netns_path) assert "" "no errors" } @test "$fw_driver - bridge driver must generate config for aardvark with multiple custom dns server with network dns servers and perform update" { # get a random port directly to avoid low ports e.g. 53 would not create iptables dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-network-container-dns-server.json \ setup $(get_container_netns_path) # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1 127.0.0.1,3.3.3.3" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8,1.1.1.1$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p $dns_port run" "aardvark not running or bad options" # Use run_helper instead of run_netavark here to check network namespace detection logic. # See https://github.com/containers/netavark/issues/911 for details. NETAVARK_DNS_PORT="$dns_port" run_helper $NETAVARK --config "$NETAVARK_TMPDIR/config" --rootless "$rootless" --file ${TESTSDIR}/testfiles/dualstack-bridge-network-container-dns-server.json \ update podman1 --network-dns-servers 8.8.8.8 assert "$output" = "" # after update the pid should never change aardvark_pid2=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$aardvark_pid2" == "$aardvark_pid" "aardvark-dns pid after nv update" # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1 8.8.8.8" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8,1.1.1.1$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" # remove network and check running and verify if aardvark config has no nameserver NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-network-container-dns-server.json \ update podman1 --network-dns-servers "" # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" == "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8,1.1.1.1$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" } # netavark must do no-op on upates when no aardvark config is there @test "run netavark update - no-op" { # get a random port directly to avoid low ports e.g. 53 would not create iptables dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-network-container-dns-server.json \ update podman1 --network-dns-servers 8.8.8.8 } @test "$fw_driver - ipv6 bridge" { run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge.json setup $(get_container_netns_path) result="$output" assert_json "$result" 'has("podman1")' == "true" "object key exists" mac=$(jq -r '.podman1.interfaces.eth0.mac_address' <<<"$result") # check that interface exists run_in_container_netns ip -j --details link show eth0 link_info="$output" assert_json "$link_info" ".[].address" == "$mac" "MAC matches container mac" assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Container interface is up" assert_json "$link_info" ".[].linkinfo.info_kind" == "veth" "Container interface is a veth device" ipaddr="fd10:88:a::2/64" run_in_container_netns ip addr show eth0 assert "$output" =~ "$ipaddr" "IP address matches container address" assert_json "$result" ".podman1.interfaces.eth0.subnets[0].ipnet" == "$ipaddr" "Result contains correct IP address" run_in_host_netns ip -j --details link show podman1 link_info="$output" assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Host bridge interface is up" assert_json "$link_info" ".[].linkinfo.info_kind" == "bridge" "The bridge interface is actually a bridge" ipaddr="fd10:88:a::1" run_in_host_netns ip addr show podman1 assert "$output" =~ "$ipaddr" "IP address matches bridge gateway address" assert_json "$result" ".podman1.interfaces.eth0.subnets[0].gateway" == "$ipaddr" "Result contains gateway address" # check that the loopback adapter is up run_in_container_netns ip addr show lo assert "$output" =~ "127.0.0.1" "Loopback adapter is up (has address)" run_in_host_netns ping6 -c 1 fd10:88:a::2 run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge.json teardown $(get_container_netns_path) } @test "$fw_driver - ipv6 bridge with static routes" { # add second interface and routes through that interface to test proper teardown run_in_container_netns ip link add type dummy run_in_container_netns ip a add fd10:49:b::2/64 dev dummy0 run_in_container_netns ip link set dummy0 up run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge-staticroutes.json setup $(get_container_netns_path) # check static routes run_in_container_netns ip -6 -br r assert "$output" "=~" "fd10:89:b::/64 via fd10:88:a::ac02" "static route not set" assert "$output" "=~" "fd10:89:c::/64 via fd10:88:a::ac03" "static route not set" assert "$output" "=~" "fd10:51:b::/64 via fd10:49:b::30" "static route not set" run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge-staticroutes.json teardown $(get_container_netns_path) # check static routes get removed run_in_container_netns ip -6 -br r assert "$output" "!~" "fd10:89:b::/64 via fd10:88:a::ac02" "static route not removed" assert "$output" "!~" "fd10:89:c::/64 via fd10:88:a::ac03" "static route not removed" assert "$output" "!~" "fd10:51:b::/64 via fd10:49:b::30" "static route not removed" run_in_container_netns ip link delete dummy0 } @test "$fw_driver - bridge driver must generate config for aardvark with custom dns server" { run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-custom-dns-server.json \ setup $(get_container_netns_path) run_in_host_netns iptables -S NETAVARK_INPUT assert "${lines[1]}" == "-A NETAVARK_INPUT -s 10.89.3.0/24 -p udp -m udp --dport 53 -j ACCEPT" "ipv4 dns udp accept rule" assert "${lines[2]}" == "-A NETAVARK_INPUT -s 10.89.3.0/24 -p tcp -m tcp --dport 53 -j ACCEPT" "ipv4 dns tcp accept rule" run_in_host_netns ip6tables -S NETAVARK_INPUT assert "${lines[1]}" == "-A NETAVARK_INPUT -s fd10:88:a::/64 -p udp -m udp --dport 53 -j ACCEPT" "ipv6 dns udp accept rule" assert "${lines[2]}" == "-A NETAVARK_INPUT -s fd10:88:a::/64 -p tcp -m tcp --dport 53 -j ACCEPT" "ipv6 dns tcp accept rule" # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p 53 run" "aardvark not running or bad options" } @test "$fw_driver - bridge driver must generate config for aardvark with multiple custom dns server" { # get a random port directly to avoid low ports e.g. 53 would not create iptables dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-multiple-custom-dns-server.json \ setup $(get_container_netns_path) # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8,1.1.1.1$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p $dns_port run" "aardvark not running or bad options" } @test "$fw_driver - bridge driver must generate config for aardvark with multiple custom dns server with network dns servers" { # get a random port directly to avoid low ports e.g. 53 would not create iptables dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-network-container-dns-server.json \ setup $(get_container_netns_path) # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1 127.0.0.1,3.3.3.3" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8,1.1.1.1$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p $dns_port run" "aardvark not running or bad options" } @test "$fw_driver - dual stack dns with alt port" { # get a random port directly to avoid low ports e.g. 53 would not create iptables dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge.json \ setup $(get_container_netns_path) # check iptables run_in_host_netns iptables -t nat -S NETAVARK-HOSTPORT-DNAT assert "${lines[2]}" == "-A NETAVARK-HOSTPORT-DNAT -d 10.89.3.1/32 -p udp -m udp --dport 53 -j DNAT --to-destination 10.89.3.1:$dns_port" "ipv4 dns forward rule" assert "${lines[1]}" == "-A NETAVARK-HOSTPORT-DNAT -d 10.89.3.1/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 10.89.3.1:$dns_port" "ipv4 dns tcp forward rule" run_in_host_netns ip6tables -t nat -S NETAVARK-HOSTPORT-DNAT assert "${lines[2]}" == "-A NETAVARK-HOSTPORT-DNAT -d fd10:88:a::1/128 -p udp -m udp --dport 53 -j DNAT --to-destination [fd10:88:a::1]:$dns_port" "ipv6 dns forward rule" assert "${lines[1]}" == "-A NETAVARK-HOSTPORT-DNAT -d fd10:88:a::1/128 -p tcp -m tcp --dport 53 -j DNAT --to-destination [fd10:88:a::1]:$dns_port" "ipv6 dns tcp forward rule" # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p $dns_port run" "aardvark not running or bad options" # test redirection actually works run_in_container_netns dig +short "somename.dns.podman" @10.89.3.1 A "somename.dns.podman" @10.89.3.1 AAAA assert "${lines[0]}" =~ "10.89.3.2" "ipv4 dns resolution works 1/2" assert "${lines[1]}" =~ "fd10:88:a::2" "ipv6 dns resolution works 2/2" run_in_container_netns dig +short "somename.dns.podman" @fd10:88:a::1 assert "${lines[0]}" =~ "10.89.3.2" "ipv6 dns resolution works" NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge.json \ teardown $(get_container_netns_path) # check iptables got removed run_in_host_netns iptables -t nat -S NETAVARK-HOSTPORT-DNAT assert "${#lines[@]}" = 1 "too many v4 NETAVARK_HOSTPORT-DNAT rules after teardown" run_in_host_netns ip6tables -t nat -S NETAVARK-HOSTPORT-DNAT assert "${#lines[@]}" = 1 "too many v6 NETAVARK_HOSTPORT-DNAT rules after teardown" # check aardvark config got cleared, process killed expected_rc=2 run_helper ls "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" expected_rc=1 run_helper ps "$aardvark_pid" } @test "$fw_driver - dns with default drop policy" { run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge.json \ setup $(get_container_netns_path) run_in_host_netns iptables -P INPUT DROP run_in_host_netns iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" # test redirection actually works run_in_container_netns dig +short "somename.dns.podman" @10.89.3.1 A "somename.dns.podman" @10.89.3.1 AAAA assert "${lines[0]}" =~ "10.89.3.2" "ipv4 dns resolution works 1/2" assert "${lines[1]}" =~ "fd10:88:a::2" "ipv6 dns resolution works 2/2" run_in_container_netns dig +short "somename.dns.podman" @fd10:88:a::1 assert "${lines[0]}" =~ "10.89.3.2" "ipv6 dns resolution works" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge.json \ teardown $(get_container_netns_path) # check iptables got removed run_in_host_netns iptables -t nat -S NETAVARK-HOSTPORT-DNAT assert "${#lines[@]}" = 1 "too many v4 NETAVARK_HOSTPORT-DNAT rules after teardown" run_in_host_netns ip6tables -t nat -S NETAVARK-HOSTPORT-DNAT assert "${#lines[@]}" = 1 "too many v6 NETAVARK_HOSTPORT-DNAT rules after teardown" # check aardvark config got cleared, process killed expected_rc=2 run_helper ls "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" expected_rc=1 run_helper ps "$aardvark_pid" } @test "$fw_driver - dns with default drop policy with non-default dns port" { # get a random port dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge.json \ setup $(get_container_netns_path) # check iptables run_in_host_netns iptables -t filter -S NETAVARK_INPUT assert "${lines[1]}" == "-A NETAVARK_INPUT -s 10.89.3.0/24 -p udp -m udp --dport $dns_port -j ACCEPT" "ipv4 dns forward rule" run_in_host_netns ip6tables -t filter -S NETAVARK_INPUT assert "${lines[1]}" == "-A NETAVARK_INPUT -s fd10:88:a::/64 -p udp -m udp --dport $dns_port -j ACCEPT" "ipv6 dns forward rule" run_in_host_netns iptables -P INPUT DROP run_in_host_netns iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" # test redirection actually works run_in_container_netns dig +short "somename.dns.podman" @10.89.3.1 A "somename.dns.podman" @10.89.3.1 AAAA assert "${lines[0]}" =~ "10.89.3.2" "ipv4 dns resolution works 1/2" assert "${lines[1]}" =~ "fd10:88:a::2" "ipv6 dns resolution works 2/2" run_in_container_netns dig +short "somename.dns.podman" @fd10:88:a::1 assert "${lines[0]}" =~ "10.89.3.2" "ipv6 dns resolution works" NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge.json \ teardown $(get_container_netns_path) # check iptables got removed run_in_host_netns iptables -t nat -S NETAVARK-HOSTPORT-DNAT assert "${#lines[@]}" = 1 "too many v4 NETAVARK_HOSTPORT-DNAT rules after teardown" run_in_host_netns ip6tables -t nat -S NETAVARK-HOSTPORT-DNAT assert "${#lines[@]}" = 1 "too many v6 NETAVARK_HOSTPORT-DNAT rules after teardown" # check aardvark config got cleared, process killed expected_rc=2 run_helper ls "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" expected_rc=1 run_helper ps "$aardvark_pid" } @test "$fw_driver - check error message from netns thread" { # create interface in netns to force error run_in_container_netns ip link add eth0 type dummy expected_rc=1 run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) assert_json ".error" "create veth pair: interface eth0 already exists on container namespace: Netlink error: File exists (os error 17)" "interface exists on netns" } @test "$fw_driver - port forwarding ipv4 - tcp" { test_port_fw } @test "$fw_driver - port forwarding ipv6 - tcp" { test_port_fw ip=6 } @test "$fw_driver - port forwarding dualstack - tcp" { test_port_fw ip=dual } @test "$fw_driver - port forwarding ipv4 - udp" { test_port_fw proto=udp } @test "$fw_driver - port forwarding ipv6 - udp" { test_port_fw ip=6 proto=udp } @test "$fw_driver - port forwarding dualstack - udp" { test_port_fw ip=dual proto=udp } @test "$fw_driver - port forwarding ipv4 - sctp" { setup_sctp_kernel_module test_port_fw proto=sctp } @test "$fw_driver - port forwarding ipv6 - sctp" { setup_sctp_kernel_module test_port_fw ip=6 proto=sctp } @test "$fw_driver - port forwarding dualstack - sctp" { setup_sctp_kernel_module test_port_fw ip=dual proto=sctp } @test "$fw_driver - port range forwarding ipv4 - tcp" { test_port_fw range=3 } @test "$fw_driver - port range forwarding ipv6 - tcp" { test_port_fw ip=6 range=3 } @test "$fw_driver - port range forwarding ipv4 - udp" { test_port_fw proto=udp range=3 } @test "$fw_driver - port range forwarding ipv6 - udp" { test_port_fw ip=6 proto=udp range=3 } @test "$fw_driver - port range forwarding dual - udp" { test_port_fw ip=dual proto=udp range=3 } @test "$fw_driver - port range forwarding dual - tcp" { test_port_fw ip=dual proto=tcp range=3 } @test "$fw_driver - port forwarding with hostip ipv4 - tcp" { add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw hostip="172.16.0.1" } @test "$fw_driver - port forwarding with hostip ipv4 dual stack - tcp" { add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw ip=dual hostip="172.16.0.1" } @test "$fw_driver - port forwarding with hostip ipv6 - tcp" { add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=6 hostip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with hostip ipv6 dual stack - tcp" { add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=dual hostip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with wildcard hostip ipv4 - tcp" { add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw hostip="0.0.0.0" connectip="172.16.0.1" } @test "$fw_driver - port forwarding with wildcard hostip ipv4 dual stack - tcp" { add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw ip=dual hostip="0.0.0.0" connectip="172.16.0.1" } @test "$fw_driver - port forwarding with wildcard hostip ipv6 - tcp" { add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=6 hostip="::" connectip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with wildcard hostip ipv6 dual stack - tcp" { add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=dual hostip="::" connectip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with hostip ipv4 - udp" { add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw proto=udp hostip="172.16.0.1" } @test "$fw_driver - port forwarding with hostip ipv6 - udp" { add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=6 proto=udp hostip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with localhost - tcp" { test_port_fw hostip="127.0.0.1" } # Test that port forwarding works with strict Reverse Path Forwarding enabled on the host @test "$fw_driver - port forwarding with two networks and RPF - tcp" { # First, enable strict RPF on host/container ns. run_in_host_netns sysctl -w net.ipv4.conf.all.rp_filter=1 run_in_host_netns sysctl -w net.ipv4.conf.default.rp_filter=1 run_in_container_netns sysctl -w net.ipv4.conf.all.rp_filter=1 run_in_container_netns sysctl -w net.ipv4.conf.default.rp_filter=1 # We need a dummy interface with a host ip, # if we connect directly to the bridge ip it doesn't reproduce. add_dummy_interface_on_host dummy0 "10.0.0.1/24" run_netavark --file ${TESTSDIR}/testfiles/two-networks.json setup $(get_container_netns_path) result="$output" run_in_host_netns cat /proc/sys/net/ipv4/conf/podman2/rp_filter assert "2" "rp_filter podman2 bridge" run_in_host_netns cat /proc/sys/net/ipv4/conf/podman3/rp_filter assert "2" "rp_filter podman3 bridge" run_in_container_netns cat /proc/sys/net/ipv4/conf/eth0/rp_filter assert "2" "rp_filter eth0 interface" run_in_container_netns cat /proc/sys/net/ipv4/conf/eth1/rp_filter assert "2" "rp_filter eth1 interface" # Important: Use the "host" ip here and not localhost or bridge ip. run_nc_test "0" "tcp" 8080 "10.0.0.1" 8080 } @test "bridge ipam none" { read -r -d '\0' config < /proc/sys/net/ipv4/ip_forward" run_in_container_netns sh -c "echo 1 > /proc/sys/net/ipv4/conf/default/arp_notify" run_in_host_netns sh -c "echo 2 > /proc/sys/net/ipv4/conf/default/rp_filter" run_in_container_netns sh -c "echo 2 > /proc/sys/net/ipv4/conf/default/rp_filter" run_in_host_netns mount -t proc -o ro,nosuid,nodev,noexec proc /proc run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json teardown $(get_container_netns_path) run_in_host_netns mount -t proc -o remount,rw /proc run_in_host_netns sh -c "echo 0 > /proc/sys/net/ipv4/ip_forward" run_in_host_netns mount -t proc -o remount,ro /proc expected_rc=1 run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) assert_json ".error" "Sysctl error: IO Error: Read-only file system (os error 30)" "Sysctl error because fs is read only" } @test "$fw_driver - bridge static mac" { mac="aa:bb:cc:dd:ee:ff" read -r -d '\0' config < /proc/sys/net/ipv6/conf/default/accept_dad" #run_in_container_netns sh -c "echo 0 > /proc/sys/net/ipv6/conf/default/accept_dad" # run_in_host_netns sh -c "echo 0 > /proc/sys/net/ipv6/conf/default/accept_ra" run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge.json setup $(get_container_netns_path) result="$output" assert_json "$result" 'has("podman1")' == "true" "object key exists" mac=$(jq -r '.podman1.interfaces.eth0.mac_address' <<<"$result") # check that interface exists run_in_container_netns ip -j --details link show eth0 link_info="$output" assert_json "$link_info" ".[].address" == "$mac" "MAC matches container mac" assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Container interface is up" assert_json "$link_info" ".[].linkinfo.info_kind" == "veth" "Container interface is a veth device" ipaddr="fd10:88:a::2/64" run_in_container_netns ip addr show eth0 assert "$output" =~ "$ipaddr" "IP address matches container address" assert_json "$result" ".podman1.interfaces.eth0.subnets[0].ipnet" == "$ipaddr" "Result contains correct IP address" run_in_host_netns ip -j --details link show podman1 link_info="$output" assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Host bridge interface is up" assert_json "$link_info" ".[].linkinfo.info_kind" == "bridge" "The bridge interface is actually a bridge" ipaddr="fd10:88:a::1" run_in_host_netns ip addr show podman1 assert "$output" =~ "$ipaddr" "IP address matches bridge gateway address" assert_json "$result" ".podman1.interfaces.eth0.subnets[0].gateway" == "$ipaddr" "Result contains gateway address" # check that the loopback adapter is up run_in_container_netns ip addr show lo assert "$output" =~ "127.0.0.1" "Loopback adapter is up (has address)" run_in_host_netns ping6 -c 1 fd10:88:a::2 } @test "$fw_driver - ipv6 bridge with static routes" { # add second interface and routes through that interface to test proper teardown run_in_container_netns ip link add type dummy run_in_container_netns ip a add fd10:49:b::2/64 dev dummy0 run_in_container_netns ip link set dummy0 up run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge-staticroutes.json setup $(get_container_netns_path) # check static routes run_in_container_netns ip -6 -br r assert "$output" "=~" "fd10:89:b::/64 via fd10:88:a::ac02" "static route not set" assert "$output" "=~" "fd10:89:c::/64 via fd10:88:a::ac03" "static route not set" assert "$output" "=~" "fd10:51:b::/64 via fd10:49:b::30" "static route not set" run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge-staticroutes.json teardown $(get_container_netns_path) # check static routes get removed run_in_container_netns ip -6 -br r assert "$output" "!~" "fd10:89:b::/64 via fd10:88:a::ac02" "static route not removed" assert "$output" "!~" "fd10:89:c::/64 via fd10:88:a::ac03" "static route not removed" assert "$output" "!~" "fd10:51:b::/64 via fd10:49:b::30" "static route not removed" run_in_container_netns ip link delete dummy0 } @test "$fw_driver - dual stack dns with alt port" { # get a random port directly to avoid low ports e.g. 53 would not create iptables dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" \ run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge.json \ setup $(get_container_netns_path) # check iptables # firewall-cmd --list-rich-rules does not guarantee order, use sort run_in_host_netns sh -c 'firewall-cmd --policy netavark_portfwd --list-rich-rules | sort' assert "${lines[0]}" =~ "rule family=\"ipv4\" destination address=\"10.89.3.1\" forward-port port=\"53\" protocol=\"udp\" to-port=\"$dns_port\" to-addr=\"10.89.3.1\"" "ipv4 dns redirection" assert "${lines[1]}" =~ "rule family=\"ipv6\" destination address=\"fd10:88:a::1\" forward-port port=\"53\" protocol=\"udp\" to-port=\"$dns_port\" to-addr=\"fd10:88:a::1\"" "ipv6 dns redirection" assert "${#lines[@]}" = 2 "too many rich rules" # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p $dns_port run" "aardvark not running or bad options" # test redirection actually works run_in_container_netns dig +short "somename.dns.podman" @10.89.3.1 A "somename.dns.podman" @10.89.3.1 AAAA assert "${lines[0]}" =~ "10.89.3.2" "ipv4 dns resolution works 1/2" assert "${lines[1]}" =~ "fd10:88:a::2" "ipv6 dns resolution works 2/2" run_in_container_netns dig +short "somename.dns.podman" @fd10:88:a::1 assert "${lines[0]}" =~ "10.89.3.2" "ipv6 dns resolution works" NETAVARK_DNS_PORT="$dns_port" \ run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge.json \ teardown $(get_container_netns_path) # check iptables got removed run_in_host_netns firewall-cmd --policy netavark_portfwd --list-rich-rules assert "${#lines[@]}" = 0 "rich rules did not get removed on teardown" # check aardvark config got cleared, process killed expected_rc=2 run_helper ls "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" expected_rc=1 run_helper ps "$aardvark_pid" } @test "$fw_driver - check error message from netns thread" { # create interface in netns to force error run_in_container_netns ip link add eth0 type dummy expected_rc=1 run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) assert_json ".error" "create veth pair: interface eth0 already exists on container namespace: Netlink error: File exists (os error 17)" "interface exists on netns" } @test "$fw_driver - port forwarding ipv4 - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw } @test "$fw_driver - port forwarding ipv6 - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw ip=6 } @test "$fw_driver - port forwarding dualstack - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw ip=dual } @test "$fw_driver - port forwarding ipv4 - udp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw proto=udp } @test "$fw_driver - port forwarding ipv6 - udp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw ip=6 proto=udp } @test "$fw_driver - port forwarding dualstack - udp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw ip=dual proto=udp } @test "$fw_driver - port forwarding ipv4 - sctp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" setup_sctp_kernel_module test_port_fw proto=sctp } @test "$fw_driver - port forwarding ipv6 - sctp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" setup_sctp_kernel_module test_port_fw ip=6 proto=sctp } @test "$fw_driver - port forwarding dualstack - sctp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" setup_sctp_kernel_module test_port_fw ip=dual proto=sctp } @test "$fw_driver - port range forwarding ipv4 - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw range=3 } @test "$fw_driver - port range forwarding ipv6 - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw ip=6 range=3 } @test "$fw_driver - port range forwarding ipv4 - udp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw proto=udp range=3 } @test "$fw_driver - port range forwarding ipv6 - udp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw ip=6 proto=udp range=3 } @test "$fw_driver - port range forwarding dual - udp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw ip=dual proto=udp range=3 } @test "$fw_driver - port range forwarding dual - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" test_port_fw ip=dual proto=tcp range=3 } @test "$fw_driver - port forwarding with hostip ipv4 - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw hostip="172.16.0.1" } @test "$fw_driver - port forwarding with hostip ipv4 dual stack - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw ip=dual hostip="172.16.0.1" } @test "$fw_driver - port forwarding with hostip ipv6 - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=6 hostip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with hostip ipv6 dual stack - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=dual hostip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with hostip ipv4 - udp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw proto=udp hostip="172.16.0.1" } @test "$fw_driver - port forwarding with hostip ipv6 - udp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=6 proto=udp hostip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with wildcard hostip ipv4 - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw hostip="0.0.0.0" connectip="172.16.0.1" } @test "$fw_driver - port forwarding with wildcard hostip ipv4 dual stack - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw ip=dual hostip="0.0.0.0" connectip="172.16.0.1" } @test "$fw_driver - port forwarding with wildcard hostip ipv6 - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=6 hostip="::" connectip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with wildcard hostip ipv6 dual stack - tcp" { skip "test requires firewalld same-machine port forwarding for non-localhost IP" add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=dual hostip="::" connectip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with localhost - tcp" { test_port_fw hostip="127.0.0.1" } @test "netavark error - invalid host_ip in port mappings" { expected_rc=1 run_netavark -f ${TESTSDIR}/testfiles/invalid-port.json setup $(get_container_netns_path) assert_json ".error" "invalid host ip \"abcd\" provided for port 8080" "host ip error" } containers-netavark-68510f4/test/250-bridge-nftables.bats000066400000000000000000001327331475240420400231500ustar00rootroot00000000000000#!/usr/bin/env bats -*- bats -*- # # bridge driver tests with nftables firewall driver # load helpers fw_driver=nftables export NETAVARK_FW=nftables @test "check nftables driver is in use" { RUST_LOG=netavark=info run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) assert "${lines[0]}" "==" "[INFO netavark::firewall] Using nftables firewall driver" "nftables driver is in use" } @test "$fw_driver - internal network" { # Table doesn't exist at this point otherwise run_in_host_netns nft add table inet netavark run_in_host_netns nft list table inet netavark before="$output" run_netavark --file ${TESTSDIR}/testfiles/internal.json setup $(get_container_netns_path) run_in_host_netns nft list table inet netavark assert "$output" == "$before" "make sure tables have not changed" run_in_container_netns ip route show assert "$output" "!~" "default" "No default route for internal networks" run_in_container_netns ping -c 1 10.88.0.1 run_netavark --file ${TESTSDIR}/testfiles/internal.json teardown $(get_container_netns_path) } @test "$fw_driver - simple bridge" { run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) result="$output" assert_json "$result" 'has("podman")' == "true" "object key exists" mac=$(jq -r '.podman.interfaces.eth0.mac_address' <<<"$result") # check that interface exists run_in_container_netns ip -j --details link show eth0 link_info="$output" assert_json "$link_info" ".[].address" == "$mac" "MAC matches container mac" assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Container interface is up" assert_json "$link_info" ".[].linkinfo.info_kind" == "veth" "Container interface is a veth device" ipaddr="10.88.0.2/16" run_in_container_netns ip addr show eth0 assert "$output" =~ "$ipaddr" "IP address matches container address" assert_json "$result" ".podman.interfaces.eth0.subnets[0].ipnet" == "$ipaddr" "Result contains correct IP address" run_in_host_netns ip -j --details link show podman0 link_info="$output" assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Host bridge interface is up" assert_json "$link_info" ".[].linkinfo.info_kind" == "bridge" "The bridge interface is actually a bridge" bridge_mac=$(jq -r '.[].address' <<<"$link_info") run_in_host_netns ip -j link show veth0 veth_info="$output" assert_json "$veth_info" ".[].address" != "$bridge_mac" "Bridge and Veth must have different mac address" ipaddr="10.88.0.1" run_in_host_netns ip addr show podman0 assert "$output" =~ "$ipaddr" "IP address matches bridge gateway address" assert_json "$result" ".podman.interfaces.eth0.subnets[0].gateway" == "$ipaddr" "Result contains gateway address" # check that the loopback adapter is up run_in_container_netns ip addr show lo assert "$output" =~ "127.0.0.1" "Loopback adapter is up (has address)" run_in_host_netns ping -c 1 10.88.0.2 check_simple_bridge_nftables run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json teardown $(get_container_netns_path) # now check that nftables rules are gone # check FORWARD rules run_in_host_netns nft list chain inet netavark FORWARD assert "${lines[3]}" =~ "ct state invalid drop" "CT state invalid rule" assert "${#lines[@]}" = 7 "too many FORWARD rules after teardown" # check POSTROUTING rules run_in_host_netns nft list chain inet netavark POSTROUTING assert "${lines[3]}" =~ "meta mark & 0x00002000 == 0x00002000 masquerade" "Mark-masquerade rule" assert "${#lines[@]}" = 6 "too many POSTROUTING rules after teardown" # nv_10_88_0_0_nm16 chain should not exists expected_rc=1 run_in_host_netns nft list chain inet netavark nv_10_88_0_0_nm16 # bridge should be removed on teardown expected_rc=1 run_in_host_netns ip addr show podman0 } @test "$fw_driver - bridge with static routes" { # add second interface and routes through that interface to test proper teardown run_in_container_netns ip link add type dummy run_in_container_netns ip a add 10.91.0.10/24 dev dummy0 run_in_container_netns ip link set dummy0 up run_netavark --file ${TESTSDIR}/testfiles/bridge-staticroutes.json setup $(get_container_netns_path) # check static routes run_in_container_netns ip r assert "$output" "=~" "10.89.0.0/24 via 10.88.0.2" "static route not set" assert "$output" "=~" "10.90.0.0/24 via 10.88.0.3" "static route not set" assert "$output" "=~" "10.92.0.0/24 via 10.91.0.1" "static route not set" run_netavark --file ${TESTSDIR}/testfiles/bridge-staticroutes.json teardown $(get_container_netns_path) # check static routes get removed assert "$output" "!~" "10.89.0.0/24 via 10.88.0.2" "static route not set" assert "$output" "!~" "10.90.0.0/24 via 10.88.0.3" "static route not set" assert "$output" "!~" "10.92.0.0/24 via 10.91.0.1" "static route not removed" } @test "$fw_driver - bridge with no default route" { run_netavark --file ${TESTSDIR}/testfiles/bridge-nodefaultroute.json setup $(get_container_netns_path) run_in_container_netns ip r assert "$output" "!~" "default" "default route exists" run_in_container_netns ip -6 r assert "$output" "!~" "default" "default route exists" run_netavark --file ${TESTSDIR}/testfiles/bridge-nodefaultroute.json teardown $(get_container_netns_path) assert "" "no errors" } @test "$fw_driver - bridge driver must generate config for aardvark with multiple custom dns server with network dns servers and perform update" { # get a random port directly to avoid low ports e.g. 53 would not create nftables rules dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-network-container-dns-server.json \ setup $(get_container_netns_path) # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1 127.0.0.1,3.3.3.3" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8,1.1.1.1$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p $dns_port run" "aardvark not running or bad options" NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-network-container-dns-server.json \ update podman1 --network-dns-servers 8.8.8.8 # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1 8.8.8.8" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8,1.1.1.1$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" # remove network and check running and verify if aardvark config has no nameserver NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-network-container-dns-server.json \ update podman1 --network-dns-servers "" # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" == "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8,1.1.1.1$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" } # netavark must do no-op on upates when no aardvark config is there @test "run netavark update - no-op" { # get a random port directly to avoid low ports e.g. 53 would not create nftables rules dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-network-container-dns-server.json \ update podman1 --network-dns-servers 8.8.8.8 } @test "$fw_driver - ipv6 bridge" { run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge.json setup $(get_container_netns_path) result="$output" assert_json "$result" 'has("podman1")' == "true" "object key exists" mac=$(jq -r '.podman1.interfaces.eth0.mac_address' <<<"$result") # check that interface exists run_in_container_netns ip -j --details link show eth0 link_info="$output" assert_json "$link_info" ".[].address" == "$mac" "MAC matches container mac" assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Container interface is up" assert_json "$link_info" ".[].linkinfo.info_kind" == "veth" "Container interface is a veth device" ipaddr="fd10:88:a::2/64" run_in_container_netns ip addr show eth0 assert "$output" =~ "$ipaddr" "IP address matches container address" assert_json "$result" ".podman1.interfaces.eth0.subnets[0].ipnet" == "$ipaddr" "Result contains correct IP address" run_in_host_netns ip -j --details link show podman1 link_info="$output" assert_json "$link_info" '.[].flags[] | select(.=="UP")' == "UP" "Host bridge interface is up" assert_json "$link_info" ".[].linkinfo.info_kind" == "bridge" "The bridge interface is actually a bridge" ipaddr="fd10:88:a::1" run_in_host_netns ip addr show podman1 assert "$output" =~ "$ipaddr" "IP address matches bridge gateway address" assert_json "$result" ".podman1.interfaces.eth0.subnets[0].gateway" == "$ipaddr" "Result contains gateway address" # check that the loopback adapter is up run_in_container_netns ip addr show lo assert "$output" =~ "127.0.0.1" "Loopback adapter is up (has address)" run_in_host_netns ping6 -c 1 fd10:88:a::2 run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge.json teardown $(get_container_netns_path) } @test "$fw_driver - ipv6 bridge with static routes" { # add second interface and routes through that interface to test proper teardown run_in_container_netns ip link add type dummy run_in_container_netns ip a add fd10:49:b::2/64 dev dummy0 run_in_container_netns ip link set dummy0 up run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge-staticroutes.json setup $(get_container_netns_path) # check static routes run_in_container_netns ip -6 -br r assert "$output" "=~" "fd10:89:b::/64 via fd10:88:a::ac02" "static route not set" assert "$output" "=~" "fd10:89:c::/64 via fd10:88:a::ac03" "static route not set" assert "$output" "=~" "fd10:51:b::/64 via fd10:49:b::30" "static route not set" run_netavark --file ${TESTSDIR}/testfiles/ipv6-bridge-staticroutes.json teardown $(get_container_netns_path) # check static routes get removed run_in_container_netns ip -6 -br r assert "$output" "!~" "fd10:89:b::/64 via fd10:88:a::ac02" "static route not removed" assert "$output" "!~" "fd10:89:c::/64 via fd10:88:a::ac03" "static route not removed" assert "$output" "!~" "fd10:51:b::/64 via fd10:49:b::30" "static route not removed" run_in_container_netns ip link delete dummy0 } @test "$fw_driver - bridge driver must generate config for aardvark with custom dns server" { run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-custom-dns-server.json \ setup $(get_container_netns_path) # check nftables run_in_host_netns nft list chain inet netavark INPUT assert "${lines[3]}" =~ "ip saddr 10.89.3.0/24 meta l4proto \{ tcp, udp \} th dport 53 accept" "DNS accept rule" # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p 53 run" "aardvark not running or bad options" } @test "$fw_driver - aardvark-dns entries after startup failure" { # force failure with invalid aardvark-dns binary expected_rc=1 run_netavark --aardvark-binary ${TESTSDIR} --file ${TESTSDIR}/testfiles/dualstack-bridge-custom-dns-server.json \ setup $(get_container_netns_path) assert "$output" =~ "aardvark-dns failed to start: Failed to find executable" "netavark error" # check aardvark config must not exists after error run_helper ls "$NETAVARK_TMPDIR/config/aardvark-dns" assert "$output" == "" "No aardvark entries" } @test "$fw_driver - bridge driver must generate config for aardvark with multiple custom dns server" { # get a random port directly to avoid low ports e.g. 53 would not create nftables dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-multiple-custom-dns-server.json \ setup $(get_container_netns_path) # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8,1.1.1.1$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p $dns_port run" "aardvark not running or bad options" } @test "$fw_driver - bridge driver must generate config for aardvark with multiple custom dns server with network dns servers" { # get a random port directly to avoid low ports e.g. 53 would not create nftables rules dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge-network-container-dns-server.json \ setup $(get_container_netns_path) # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1 127.0.0.1,3.3.3.3" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename 8.8.8.8,1.1.1.1$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p $dns_port run" "aardvark not running or bad options" } @test "$fw_driver - dual stack dns with alt port" { # get a random port directly to avoid low ports e.g. 53 would not create nftables rules dns_port=$((RANDOM+10000)) NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge.json \ setup $(get_container_netns_path) # check nftables run_in_host_netns nft list chain inet netavark NETAVARK-HOSTPORT-DNAT assert "${lines[2]}" =~ "ip6 daddr fd10:88:a::1 meta l4proto \{ tcp, udp \} th dport 53 dnat ip6 to \[fd10:88:a::1\]:$dns_port" "DNS forward rule ip6" assert "${lines[3]}" =~ "ip daddr 10.89.3.1 meta l4proto \{ tcp, udp \} th dport 53 dnat ip to 10.89.3.1:$dns_port" "DNS forward rule ip4" # check aardvark config and running run_helper cat "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" assert "${lines[0]}" =~ "10.89.3.1,fd10:88:a::1" "aardvark set to listen to all IPs" assert "${lines[1]}" =~ "^[0-9a-f]{64} 10.89.3.2 fd10:88:a::2 somename$" "aardvark config's container" assert "${#lines[@]}" = 2 "too many lines in aardvark config" aardvark_pid=$(cat "$NETAVARK_TMPDIR/config/aardvark-dns/aardvark.pid") assert "$ardvark_pid" =~ "[0-9]*" "aardvark pid not found" run_helper ps "$aardvark_pid" assert "${lines[1]}" =~ ".*aardvark-dns --config $NETAVARK_TMPDIR/config/aardvark-dns -p $dns_port run" "aardvark not running or bad options" # test redirection actually works run_in_container_netns dig +short "somename.dns.podman" @10.89.3.1 A "somename.dns.podman" @10.89.3.1 AAAA assert "${lines[0]}" =~ "10.89.3.2" "ipv4 dns resolution works 1/2" assert "${lines[1]}" =~ "fd10:88:a::2" "ipv6 dns resolution works 2/2" run_in_container_netns dig +short "somename.dns.podman" @fd10:88:a::1 assert "${lines[0]}" =~ "10.89.3.2" "ipv6 dns resolution works" NETAVARK_DNS_PORT="$dns_port" run_netavark --file ${TESTSDIR}/testfiles/dualstack-bridge.json \ teardown $(get_container_netns_path) # check nftables rules were removed run_in_host_netns nft list chain inet netavark NETAVARK-HOSTPORT-DNAT assert "${#lines[@]}" = 4 "too many v4 NETAVARK_HOSTPORT-DNAT rules after teardown" # check aardvark config got cleared, process killed expected_rc=2 run_helper ls "$NETAVARK_TMPDIR/config/aardvark-dns/podman1" expected_rc=1 run_helper ps "$aardvark_pid" } @test "$fw_driver - check error message from netns thread" { # create interface in netns to force error run_in_container_netns ip link add eth0 type dummy expected_rc=1 run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) assert_json ".error" "create veth pair: interface eth0 already exists on container namespace: Netlink error: File exists (os error 17)" "interface exists on netns" } @test "$fw_driver - port forwarding ipv4 - tcp" { test_port_fw } @test "$fw_driver - port forwarding ipv6 - tcp" { test_port_fw ip=6 } @test "$fw_driver - port forwarding dualstack - tcp" { test_port_fw ip=dual } @test "$fw_driver - port forwarding ipv4 - udp" { test_port_fw proto=udp } @test "$fw_driver - port forwarding ipv6 - udp" { test_port_fw ip=6 proto=udp } @test "$fw_driver - port forwarding dualstack - udp" { test_port_fw ip=dual proto=udp } @test "$fw_driver - port forwarding ipv4 - sctp" { setup_sctp_kernel_module test_port_fw proto=sctp } @test "$fw_driver - port forwarding ipv6 - sctp" { setup_sctp_kernel_module test_port_fw ip=6 proto=sctp } @test "$fw_driver - port forwarding dualstack - sctp" { setup_sctp_kernel_module test_port_fw ip=dual proto=sctp } @test "$fw_driver - port range forwarding ipv4 - tcp" { test_port_fw range=3 } @test "$fw_driver - port range forwarding ipv6 - tcp" { test_port_fw ip=6 range=3 } @test "$fw_driver - port range forwarding ipv4 - udp" { test_port_fw proto=udp range=3 } @test "$fw_driver - port range forwarding ipv6 - udp" { test_port_fw ip=6 proto=udp range=3 } @test "$fw_driver - port range forwarding dual - udp" { test_port_fw ip=dual proto=udp range=3 } @test "$fw_driver - port range forwarding dual - tcp" { test_port_fw ip=dual proto=tcp range=3 } @test "$fw_driver - port forwarding with hostip ipv4 - tcp" { add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw hostip="172.16.0.1" } @test "$fw_driver - port forwarding with hostip ipv4 dual stack - tcp" { add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw ip=dual hostip="172.16.0.1" } @test "$fw_driver - port forwarding with hostip ipv6 - tcp" { add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=6 hostip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with hostip ipv6 dual stack - tcp" { add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=dual hostip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with wildcard hostip ipv4 - tcp" { add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw hostip="0.0.0.0" connectip="172.16.0.1" } @test "$fw_driver - port forwarding with wildcard hostip ipv4 dual stack - tcp" { add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw ip=dual hostip="0.0.0.0" connectip="172.16.0.1" } @test "$fw_driver - port forwarding with wildcard hostip ipv6 - tcp" { add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=6 hostip="::" connectip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with wildcard hostip ipv6 dual stack - tcp" { add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=dual hostip="::" connectip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with hostip ipv4 - udp" { add_dummy_interface_on_host dummy0 "172.16.0.1/24" test_port_fw proto=udp hostip="172.16.0.1" } @test "$fw_driver - port forwarding with hostip ipv6 - udp" { add_dummy_interface_on_host dummy0 "fd65:8371:648b:0c06::1/64" test_port_fw ip=6 proto=udp hostip="fd65:8371:648b:0c06::1" } @test "$fw_driver - port forwarding with localhost - tcp" { test_port_fw hostip="127.0.0.1" } @test "bridge ipam none" { read -r -d '\0' config < /proc/sys/net/ipv4/ip_forward" run_in_container_netns sh -c "echo 1 > /proc/sys/net/ipv4/conf/default/arp_notify" run_in_host_netns sh -c "echo 2 > /proc/sys/net/ipv4/conf/default/rp_filter" run_in_container_netns sh -c "echo 2 > /proc/sys/net/ipv4/conf/default/rp_filter" run_in_host_netns mount -t proc -o ro,nosuid,nodev,noexec proc /proc run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json teardown $(get_container_netns_path) run_in_host_netns mount -t proc -o remount,rw /proc run_in_host_netns sh -c "echo 0 > /proc/sys/net/ipv4/ip_forward" run_in_host_netns mount -t proc -o remount,ro /proc expected_rc=1 run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path) assert_json ".error" "Sysctl error: IO Error: Read-only file system (os error 30)" "Sysctl error because fs is read only" } @test "$fw_driver - bridge static mac" { mac="aa:bb:cc:dd:ee:ff" read -r -d '\0' config <"$NETAVARK_TMPDIR/firewalld.log" & FIREWALLD_PID=$! echo "firewalld pid: $FIREWALLD_PID" # wait for firewalld to become ready timeout=5 while [ $timeout -gt 0 ]; do # query firewalld with firewall-cmd expected_rc="?" run_in_host_netns firewall-cmd --state if [ "$status" -eq 0 ]; then break fi sleep 1 timeout=$(($timeout - 1)) if [ $timeout -eq 0 ]; then cat "$NETAVARK_TMPDIR/firewalld.log" die "failed to start firewalld - timeout" fi done } function teardown_firewalld() { if [ -n "${NETAVARK_FIREWALLD_RELOAD_PID}" ]; then kill -9 $NETAVARK_FIREWALLD_RELOAD_PID fi if [ -n "${FIREWALLD_PID}" ]; then kill -9 $FIREWALLD_PID fi if [ -n "${DBUS_PID}" ]; then kill -9 $DBUS_PID fi unset DBUS_SYSTEM_BUS_ADDRESS } # Provide the above as default methods. function setup() { basic_setup } function teardown() { basic_teardown } function create_netns() { # create a new netns and mountns and run a sleep process to keep it alive # we have to redirect stdout/err to /dev/null otherwise bats will hang unshare -nm sleep inf &>/dev/null & echo $! } function get_container_netns_path() { local which="0" if [[ $# -eq 1 ]]; then which=$1 fi echo /proc/"${CONTAINER_NS_PIDS[$which]}"/ns/net } ################ # run_netavark # Invoke $NETAVARK, with timeout, using BATS 'run' ################ # # This is the preferred mechanism for invoking netavark: first, it # it joins the test network namespace before it invokes $NETAVARK, # which may be 'netavark' or '/some/path/netavark'. function run_netavark() { run_in_host_netns $NETAVARK --rootless "$rootless" \ --config "$NETAVARK_TMPDIR/config" "$@" } function run_netavark_firewalld_reload() { # need to use nsetner as this will be run in the background nsenter -n -t $HOST_NS_PID $NETAVARK --config "$NETAVARK_TMPDIR/config" firewalld-reload & NETAVARK_FIREWALLD_RELOAD_PID=$! } ################ # run_in_container_netns # Run args in container netns ################ # function run_in_container_netns() { local i="0" isnum='^[0-9]+$' if [[ $1 =~ $isnum ]]; then i=$1 shift 1 fi run_helper nsenter -n -m -w -t "${CONTAINER_NS_PIDS[$i]}" "$@" } ################ # run_in_host_netns # Run args in host netns ################ function run_in_host_netns() { run_helper nsenter -n -m -w -t $HOST_NS_PID "$@" } #### Functions below are taken from podman and buildah and adapted to netavark. ################ # run_helper # Invoke args, with timeout, using BATS 'run' ################ # # Second, we use 'timeout' to abort (with a diagnostic) if something # takes too long; this is preferable to a CI hang. # # Third, we log the command run and its output. This doesn't normally # appear in BATS output, but it will if there's an error. # # Next, we check exit status. Since the normal desired code is 0, # that's the default; but the expected_rc var can override: # # expected_rc=125 run_helper nonexistent-subcommand # expected_rc=? run_helper some-other-command # let our caller check status # # Since we use the BATS 'run' mechanism, $output and $status will be # defined for our caller. # function run_helper() { # expected_rc if unset set default to 0 expected_rc="${expected_rc-0}" if [ "$expected_rc" == "?" ]; then expected_rc= fi # Remember command args, for possible use in later diagnostic messages MOST_RECENT_COMMAND="$*" # stdout is only emitted upon error; this echo is to help a debugger echo "$_LOG_PROMPT $*" # BATS hangs if a subprocess remains and keeps FD 3 open; this happens # if a process crashes unexpectedly without cleaning up subprocesses. run timeout --foreground -v --kill=10 10 "$@" 3>/dev/null # without "quotes", multiple lines are glommed together into one if [ -n "$output" ]; then echo "$output" fi if [ "$status" -ne 0 ]; then echo -n "[ rc=$status " if [ -n "$expected_rc" ]; then if [ "$status" -eq "$expected_rc" ]; then echo -n "(expected) " else echo -n "(** EXPECTED $expected_rc **) " fi fi echo "]" fi if [ "$status" -eq 124 ]; then if expr "$output" : ".*timeout: sending" >/dev/null; then # It's possible for a subtest to _want_ a timeout if [[ "$expected_rc" != "124" ]]; then echo "*** TIMED OUT ***" false fi fi fi if [ -n "$expected_rc" ]; then if [ "$status" -ne "$expected_rc" ]; then die "exit code is $status; expected $expected_rc" fi fi # unset unset expected_rc } ######### # die # Abort with helpful message ######### function die() { # FIXME: handle multi-line output echo "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" >&2 echo "#| FAIL: $*" >&2 echo "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" >&2 false } ############ # assert # Compare actual vs expected string; fail if mismatch ############ # # Compares string (default: $output) against the given string argument. # By default we do an exact-match comparison against $output, but there # are two different ways to invoke us, each with an optional description: # # xpect "EXPECT" [DESCRIPTION] # xpect "RESULT" "OP" "EXPECT" [DESCRIPTION] # # The first form (one or two arguments) does an exact-match comparison # of "$output" against "EXPECT". The second (three or four args) compares # the first parameter against EXPECT, using the given OPerator. If present, # DESCRIPTION will be displayed on test failure. # # Examples: # # xpect "this is exactly what we expect" # xpect "${lines[0]}" =~ "^abc" "first line begins with abc" # function assert() { local actual_string="$output" local operator='==' local expect_string="$1" local testname="$2" case "${#*}" in 0) die "Internal error: 'assert' requires one or more arguments" ;; 1 | 2) ;; 3 | 4) actual_string="$1" operator="$2" expect_string="$3" testname="$4" ;; *) die "Internal error: too many arguments to 'assert'" ;; esac # Comparisons. # Special case: there is no !~ operator, so fake it via '! x =~ y' local not= local actual_op="$operator" if [[ $operator == '!~' ]]; then not='!' actual_op='=~' fi if [[ $operator == '=' || $operator == '==' ]]; then # Special case: we can't use '=' or '==' inside [[ ... ]] because # the right-hand side is treated as a pattern... and '[xy]' will # not compare literally. There seems to be no way to turn that off. if [ "$actual_string" = "$expect_string" ]; then return fi elif [[ $operator == '!=' ]]; then # Same special case as above if [ "$actual_string" != "$expect_string" ]; then return fi else if eval "[[ $not \$actual_string $actual_op \$expect_string ]]"; then return elif [ $? -gt 1 ]; then die "Internal error: could not process 'actual' $operator 'expect'" fi fi # Test has failed. Get a descriptive test name. if [ -z "$testname" ]; then testname="${MOST_RECENT_BUILDAH_COMMAND:-[no test name given]}" fi # Display optimization: the typical case for 'expect' is an # exact match ('='), but there are also '=~' or '!~' or '-ge' # and the like. Omit the '=' but show the others; and always # align subsequent output lines for ease of comparison. local op='' local ws='' if [ "$operator" != '==' ]; then op="$operator " ws=$(printf "%*s" ${#op} "") fi # This is a multi-line message, which may in turn contain multi-line # output, so let's format it ourself, readably local actual_split IFS=$'\n' read -rd '' -a actual_split <<<"$actual_string" || true printf "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" >&2 printf "#| FAIL: %s\n" "$testname" >&2 printf "#| expected: %s'%s'\n" "$op" "$expect_string" >&2 printf "#| actual: %s'%s'\n" "$ws" "${actual_split[0]}" >&2 local line for line in "${actual_split[@]:1}"; do printf "#| > %s'%s'\n" "$ws" "$line" >&2 done printf "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" >&2 false } ################# # assert_json # Compare actual json vs expected string; fail if mismatch ################# # assert_json works like assert except that it accepts one extra parameter, # the jq query string. # There are two different ways to invoke us, each with an optional description: # # xpect "JQ_QUERY" "EXPECT" [DESCRIPTION] # xpect "JSON_STRING" "JQ_QUERY" "OP" "EXPECT" [DESCRIPTION] # Important this function will overwrite $output, so if you need to use the value # more than once you need to safe it in another variable. function assert_json() { local actual_json="$output" local operator='==' local jq_query="$1" local expect_string="$2" local testname="$3" case "${#*}" in 0 | 1) die "Internal error: 'assert_json' requires two or more arguments" ;; 2 | 3) ;; 4 | 5) actual_json="$1" jq_query="$2" operator="$3" expect_string="$4" testname="$5" ;; *) die "Internal error: too many arguments to 'assert_json'" ;; esac run_helper jq -r "$jq_query" <<<"$actual_json" assert "$output" "$operator" "$expect_string" "$testname" } ################## # test_port_fw # test port forwarding ################## # test port forwarding # by default this will create a ipv4 config with tcp as protocol # # The following arguments are supported, the order does not matter: # ip={4, 6, dual} # proto={tcp,udp,sctp} or some comma separated list of the protocols # hostip=$ip the ip which is used for binding on the host # hostport=$port the port which is binded on the host # containerport=$port the port which is binded in the container # range=$num >=1 specify a port range which will forward hostport+range ports # connectip=$ip the ip which is used to connect to in the ncat test # firewalld_reload={false,true} call firewall-cmd --reload to check for port rules # function test_port_fw() { local ipv4=true local ipv6=false local proto=tcp local host_ip="" local host_port="" local container_port="" local range=1 local connect_ip="" local firewalld_reload=false # parse arguments while [[ "$#" -gt 0 ]]; do IFS='=' read -r arg value <<<"$1" case "$arg" in ip) case "$value" in 4) ipv4=true ;; 6) ipv6=true ipv4=false ;; dual) ipv6=true ;; *) die "unknown argument '$value' for ip=" ;; esac ;; proto) proto="$value" ;; hostip) host_ip="$value" ;; connectip) connect_ip="$value" ;; hostport) host_port="$value" ;; containerport) container_port="$value" ;; range) range="$value" ;; firewalld_reload) firewalld_reload="$value" ;; *) die "unknown argument for '$arg' test_port_fw" ;; esac shift done if [ -z "$host_port" ]; then host_port=$(random_port) fi if [ -z "$container_port" ]; then container_port=$(random_port) fi local container_id=$(random_string 64) local container_name="name-$(random_string 10)" local static_ips="" local subnets="" if [ $ipv4 = true ]; then ipv4_subnet=$(random_subnet) ipv4_gateway=$(gateway_from_subnet $ipv4_subnet) ipv4_container_ip=$(random_ip_in_subnet $ipv4_subnet) static_ips="\"$ipv4_container_ip\"" subnets="{\"subnet\":\"$ipv4_subnet\",\"gateway\":\"$ipv4_gateway\"}" fi if [ $ipv6 = true ]; then ipv6_subnet=$(random_subnet 6) ipv6_gateway=$(gateway_from_subnet $ipv6_subnet) ipv6_container_ip=$(random_ip_in_subnet $ipv6_subnet) if [ $ipv4 = true ]; then # add comma for the json static_ips="$static_ips, " subnets="$subnets, " fi static_ips="$static_ips\"$ipv6_container_ip\"" subnets="$subnets {\"subnet\":\"$ipv6_subnet\",\"gateway\":\"$ipv6_gateway\"}" fi read -r -d '\0' config </dev/null case $proto in tcp) ;; # nothing to do (default) udp) nc_common_args=--udp ;; sctp) nc_common_args=--sctp # For some reason we have to attach a empty STDIN (not /dev/null and not something with data in it) # to the server only for the sctp proto otherwise it will just exit for weird reasons. # As such create a empty anonymous pipe to work around that. # https://github.com/nmap/nmap/issues/2829 exec {stdin}<> <(:) ;; *) die "unknown port proto '$proto'" ;; esac if is_ipv4 "$connect_ip"; then nc_common_args="-4 $nc_common_args" fi if is_ipv6 "$connect_ip"; then nc_common_args="-6 $nc_common_args" fi nsenter -n -t "${CONTAINER_NS_PIDS[$container_ns]}" timeout --foreground -v --kill=10 5 \ nc $nc_common_args -l -p $container_port &>"$NETAVARK_TMPDIR/nc-out" <&$stdin & # make sure to wait until port is bound otherwise test can flake # https://github.com/containers/netavark/issues/433 if [ "$proto" = "tcp" ] || [ "$proto" = "udp" ]; then wait_for_port "${CONTAINER_NS_PIDS[$container_ns]}" $container_port $proto else # TODO add support for sctp port reading from /proc/net/sctp/eps, # for now just sleep sleep 0.5 fi data=$(random_string) run_in_host_netns nc $nc_common_args $connect_ip $host_port <<<"$data" got=$(cat "$NETAVARK_TMPDIR/nc-out") assert "$got" == "$data" "ncat received data" # close the fd exec {stdin}>&- } ################# # random_port # get a random port number between 1-32768 ################# function random_port() { printf $(($RANDOM + 1)) } ################### # random_string # Pseudorandom alphanumeric string of given length ################### function random_string() { local length=${1:-10} head /dev/urandom | tr -dc a-zA-Z0-9 | head -c$length } ################### # random_subnet # generate a random private subnet ################### # # by default it will return a 10.x.x.0/24 ipv4 subnet # if "6" is given as first argument it will return a "fdx:x:x:x::/64" ipv6 subnet function random_subnet() { if [[ "$1" == "6" ]]; then printf "fd%x:%x:%x:%x::/64" $((RANDOM % 256)) $((RANDOM % 65535)) $((RANDOM % 65535)) $((RANDOM % 65535)) else printf "10.%d.%d.0/24" $((RANDOM % 256)) $((RANDOM % 256)) fi } ######################### # random_ip_in_subnet # get a random from a given subnet ######################### # the first arg must be an subnet created by random_subnet # otherwise this function might return an invalid ip function random_ip_in_subnet() { # first trim subnet local net_ip=${1%/*} local num= # if ip has colon it is ipv6 if [[ "$net_ip" == *":"* ]]; then # make sure to not get 0 or 1 num=$(printf "%x" $((RANDOM % 65533 + 2))) else # if ipv4 we have to trim the final 0 net_ip=${net_ip%0} # make sure to not get 0, 1 or 255 num=$(printf "%d" $((RANDOM % 252 + 2))) fi printf "$net_ip%s" $num } ######################### # random_ip_in_subnet # get the first ip from a given subnet ######################### # the first arg must be an subnet created by random_subnet # otherwise this function might return an invalid ip function gateway_from_subnet() { # first trim subnet local net_ip=${1%/*} # set first ip in network as gateway local num=1 # if ip has dor it is ipv4 if [[ "$net_ip" == *"."* ]]; then # if ipv4 we have to trim the final 0 net_ip=${net_ip%0} fi printf "$net_ip%s" $num } ############################## # setup_sctp_kernel_module # ############################## # tries to load the sctp kernel module if possible # otherwise it will skip the test function setup_sctp_kernel_module() { modprobe sctp || skip "cannot load sctp kernel module" } ################################# # add_dummy_interface_on_host # ################################# # create a dummy interface with the given name and subnet # the first arg is the name # the second arg is the subnet (optional) function add_dummy_interface_on_host() { name="$1" ipaddr="$2" run_in_host_netns ip link add "$name" type dummy if [ -n "$ipaddr" ]; then run_in_host_netns ip addr add "$ipaddr" dev "$name" fi run_in_host_netns ip link set "$name" up } ### Below functions are taken from podman system tests, ### see Stefano Brivio's commit https://github.com/containers/podman/pull/16141/commits/ea4f168b3a6603991f2cbdc2dcfe6268a46bf1ba # ipv6_to_procfs() - RFC 5952 IPv6 address text representation to procfs format # $1: Address in any notation described by RFC 5952 function ipv6_to_procfs() { local addr="${1}" # Add leading zero if missing case ${addr} in "::"*) addr=0"${addr}" ;; esac # Double colon can mean any number of all-zero fields. Expand to fill # as many colons as are missing. (This will not be a valid IPv6 form, # but we don't need it for long). E.g., 0::1 -> 0:::::::1 case ${addr} in *"::"*) # All the colons in the address local colons colons=$(tr -dc : <<<$addr) # subtract those from a string of eight colons; this gives us # a string of two to six colons... local pad pad=$(sed -e "s/$colons//" <<<":::::::") # ...which we then inject in place of the double colon. addr=$(sed -e "s/::/::$pad/" <<<$addr) ;; esac # Print as a contiguous string of zero-filled 16-bit words # (The additional ":" below is needed because 'read -d x' actually # means "x is a TERMINATOR, not a delimiter") local group while read -d : group; do printf "%04X" "0x${group:-0}" done <<<"${addr}:" } # __ipv4_to_procfs() - Print bytes in hexadecimal notation reversing arguments # $@: IPv4 address as separate bytes function __ipv4_to_procfs() { printf "%02X%02X%02X%02X" ${4} ${3} ${2} ${1} } # ipv4_to_procfs() - IPv4 address representation to big-endian procfs format # $1: Text representation of IPv4 address function ipv4_to_procfs() { IFS='.' __ipv4_to_procfs ${1} } # port_is_bound() - Check if TCP or UDP port is bound for a given address # $1: Netns PID # $2: Port number # $3: Optional protocol, or optional IPv4 or IPv6 address, default: tcp # $4: Optional IPv4 or IPv6 address, or optional protocol, default: any function port_is_bound() { local pid=$1 local port=${2?Usage: port_is_bound PORT [tcp|udp] [ADDRESS]} if [ "${3}" = "tcp" ] || [ "${3}" = "udp" ]; then local address="${4}" local proto="${3}" elif [ "${4}" = "tcp" ] || [ "${4}" = "udp" ]; then local address="${3}" local proto="${4}" else local address="${3}" # Might be empty local proto="tcp" fi port=$(printf %04X ${port}) case "${address}" in *":"*) nsenter -n -t $pid grep -e "^[^:]*: $(ipv6_to_procfs "${address}"):${port} .*" \ -e "^[^:]*: $(ipv6_to_procfs "::0"):${port} .*" \ -q "/proc/net/${proto}6" ;; *"."*) nsenter -n -t $pid grep -e "^[^:]*: $(ipv4_to_procfs "${address}"):${port}" \ -e "^[^:]*: $(ipv4_to_procfs "0.0.0.0"):${port}" \ -q "/proc/net/${proto}" ;; *) # No address: check both IPv4 and IPv6, for any bound address nsenter -n -t $pid grep "^[^:]*: [^:]*:${port} .*" -q "/proc/net/${proto}6" || \ nsenter -n -t $pid grep "^[^:]*: [^:]*:${port} .*" -q "/proc/net/${proto}" ;; esac } # port_is_free() - Check if TCP or UDP port is free to bind for a given address # $1: Netns PID # $2: Port number # $3: Optional protocol, or optional IPv4 or IPv6 address, default: tcp # $4: Optional IPv4 or IPv6 address, or optional protocol, default: any function port_is_free() { ! port_is_bound ${@} } # wait_for_port() - Return when port is binded # $1: Netns PID # $2: Port number # $3: Optional protocol, or optional IPv4 or IPv6 address, default: tcp # $4: Optional IPv4 or IPv6 address, or optional protocol, default: any # $5: Optional timeout, 5 seconds if not given function wait_for_port() { local pid=$1 local port=$2 local proto=$3 local host=$4 local _timeout=${5:-5} # Wait while [ $_timeout -gt 0 ]; do port_is_bound ${pid} ${port} ${proto} ${host} && return sleep 1 _timeout=$(( $_timeout - 1 )) done die "Timed out waiting for $host:$port" } containers-netavark-68510f4/test/testfiles/000077500000000000000000000000001475240420400207325ustar00rootroot00000000000000containers-netavark-68510f4/test/testfiles/bridge-managed-dhcp.json000066400000000000000000000013311475240420400253650ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "networks": { "podman": { "interface_name": "eth0", "static_ips": [] } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "subnets": [], "options": { "mode": "managed" }, "ipam_options": { "driver": "dhcp" } } } } containers-netavark-68510f4/test/testfiles/bridge-managed.json000066400000000000000000000014761475240420400244630ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "networks": { "podman": { "interface_name": "eth0", "static_ips": [ "10.88.0.2" ] } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "subnets": [ { "gateway": "10.88.0.1", "subnet": "10.88.0.0/16" } ], "options": { "mode": "managed" } } } } containers-netavark-68510f4/test/testfiles/bridge-nodefaultroute.json000066400000000000000000000015071475240420400261220ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "networks": { "podman": { "interface_name": "eth0", "static_ips": [ "10.88.0.2" ] } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "subnets": [ { "gateway": "10.88.0.1", "subnet": "10.88.0.0/16" } ], "options": { "no_default_route": "true" } } } } containers-netavark-68510f4/test/testfiles/bridge-port-hostip.json000066400000000000000000000026121475240420400253500ustar00rootroot00000000000000{ "container_id": "f922ffdda5718b26ea585a500d5ad05191da5461b06d6f62e4d1f66ca901a253", "container_name": "sharp_gould", "port_mappings": [ { "host_ip": "192.168.188.25", "container_port": 8080, "host_port": 8080, "range": 1, "protocol": "tcp" }, { "host_ip": "192.168.188.24", "container_port": 8080, "host_port": 8080, "range": 1, "protocol": "tcp" } ], "networks": { "podman": { "static_ips": [ "10.88.0.14" ], "aliases": [ "f922ffdda571" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "bridge", "network_interface": "podman0", "created": "2024-09-05T15:00:04.45111926+02:00", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/bridge-port-tcp-udp.json000066400000000000000000000026121475240420400254160ustar00rootroot00000000000000{ "container_id": "f922ffdda5718b26ea585a500d5ad05191da5461b06d6f62e4d1f66ca901a253", "container_name": "sharp_gould", "port_mappings": [ { "host_ip": "192.168.188.25", "container_port": 8080, "host_port": 8080, "range": 1, "protocol": "tcp" }, { "host_ip": "192.168.188.25", "container_port": 8080, "host_port": 8080, "range": 1, "protocol": "udp" } ], "networks": { "podman": { "static_ips": [ "10.88.0.14" ], "aliases": [ "f922ffdda571" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "bridge", "network_interface": "podman0", "created": "2024-09-05T15:00:04.45111926+02:00", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/bridge-staticroutes.json000066400000000000000000000022431475240420400256110ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "networks": { "podman": { "interface_name": "eth0", "static_ips": [ "10.88.0.2" ] } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "subnets": [ { "gateway": "10.88.0.1", "subnet": "10.88.0.0/16" } ], "routes": [ { "destination": "10.89.0.0/24", "gateway": "10.88.0.2" }, { "destination": "10.90.0.0/24", "gateway": "10.88.0.3" }, { "destination": "10.92.0.0/24", "gateway": "10.91.0.1" } ] } } } containers-netavark-68510f4/test/testfiles/bridge-unmanaged.json000066400000000000000000000012331475240420400250150ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "networks": { "podman": { "interface_name": "eth0", "static_ips": [ "10.88.0.2" ] } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "brtest0", "options": { "mode": "unmanaged" } } } } containers-netavark-68510f4/test/testfiles/bridge-vethname-exists.json000066400000000000000000000015151475240420400262050ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "networks": { "podman": { "interface_name": "eth0", "static_ips": [ "10.88.0.2" ], "options": { "host_interface_name": "podman0" } } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "subnets": [ { "gateway": "10.88.0.1", "subnet": "10.88.0.0/16" } ] } } } containers-netavark-68510f4/test/testfiles/bridge-vethname.json000066400000000000000000000015151475240420400246700ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "networks": { "podman": { "interface_name": "eth0", "static_ips": [ "10.88.0.2" ], "options": { "host_interface_name": "my-veth" } } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "subnets": [ { "gateway": "10.88.0.1", "subnet": "10.88.0.0/16" } ] } } } containers-netavark-68510f4/test/testfiles/dualstack-bridge-custom-dns-server.json000066400000000000000000000020731475240420400304320ustar00rootroot00000000000000{ "container_id": "f031bf33eecba75d0d84952337b1ceef6a239eb8e94b48aee0993d0791345325", "container_name": "somename", "dns_servers": ["8.8.8.8"], "networks": { "podman1": { "static_ips": [ "10.89.3.2", "fd10:88:a::2" ], "interface_name": "eth0" } }, "network_info": { "podman1": { "name": "podman1", "id": "ec79dd0cad82083c8ac5cc23e9542e4ddea813dff60d68258d36e84f6393b63b", "driver": "bridge", "network_interface": "podman1", "subnets": [ { "subnet": "10.89.3.0/24", "gateway": "10.89.3.1" }, { "subnet": "fd10:88:a::/64", "gateway": "fd10:88:a::1" } ], "ipv6_enabled": true, "internal": false, "dns_enabled": true, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/dualstack-bridge-multiple-custom-dns-server.json000066400000000000000000000021061475240420400322600ustar00rootroot00000000000000{ "container_id": "f031bf33eecba75d0d84952337b1ceef6a239eb8e94b48aee0993d0791345325", "container_name": "somename", "dns_servers": ["8.8.8.8", "1.1.1.1"], "networks": { "podman1": { "static_ips": [ "10.89.3.2", "fd10:88:a::2" ], "interface_name": "eth0" } }, "network_info": { "podman1": { "name": "podman1", "id": "ec79dd0cad82083c8ac5cc23e9542e4ddea813dff60d68258d36e84f6393b63b", "driver": "bridge", "network_interface": "podman1", "subnets": [ { "subnet": "10.89.3.0/24", "gateway": "10.89.3.1" }, { "subnet": "fd10:88:a::/64", "gateway": "fd10:88:a::1" } ], "ipv6_enabled": true, "internal": false, "dns_enabled": true, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/dualstack-bridge-network-container-dns-server.json000066400000000000000000000017171475240420400325750ustar00rootroot00000000000000{ "container_id": "f031bf33eecba75d0d84952337b1ceef6a239eb8e94b48aee0993d0791345325", "container_name": "somename", "dns_servers": [ "8.8.8.8", "1.1.1.1" ], "networks": { "podman1": { "static_ips": [ "10.89.3.2", "fd10:88:a::2" ], "interface_name": "eth0" } }, "network_info": { "podman1": { "name": "podman1", "id": "ec79dd0cad82083c8ac5cc23e9542e4ddea813dff60d68258d36e84f6393b63b", "driver": "bridge", "network_interface": "podman1", "subnets": [ { "subnet": "10.89.3.0/24", "gateway": "10.89.3.1" }, { "subnet": "fd10:88:a::/64", "gateway": "fd10:88:a::1" } ], "ipv6_enabled": true, "internal": false, "dns_enabled": true, "network_dns_servers": [ "127.0.0.1", "3.3.3.3" ], "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/dualstack-bridge.json000066400000000000000000000020331475240420400250300ustar00rootroot00000000000000{ "container_id": "f031bf33eecba75d0d84952337b1ceef6a239eb8e94b48aee0993d0791345325", "container_name": "somename", "networks": { "podman1": { "static_ips": [ "10.89.3.2", "fd10:88:a::2" ], "interface_name": "eth0" } }, "network_info": { "podman1": { "name": "podman1", "id": "ec79dd0cad82083c8ac5cc23e9542e4ddea813dff60d68258d36e84f6393b63b", "driver": "bridge", "network_interface": "podman1", "subnets": [ { "subnet": "10.89.3.0/24", "gateway": "10.89.3.1" }, { "subnet": "fd10:88:a::/64", "gateway": "fd10:88:a::1" } ], "ipv6_enabled": true, "internal": false, "dns_enabled": true, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/firewalld-dbus.conf000066400000000000000000000066611475240420400245160ustar00rootroot00000000000000 EXTERNAL unix:path=/tmp/dummy containers-netavark-68510f4/test/testfiles/internal.json000066400000000000000000000013631475240420400234440ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "networks": { "podman": { "interface_name": "eth0", "static_ips": [ "10.88.0.2" ] } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": true, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "subnets": [ { "gateway": "10.88.0.1", "subnet": "10.88.0.0/16" } ] } } } containers-netavark-68510f4/test/testfiles/invalid-port.json000066400000000000000000000016661475240420400242460ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "port_mappings": [ { "host_ip": "abcd", "container_port": 8080, "host_port": 8080, "range": 1, "protocol": "tcp" } ], "networks": { "podman": { "interface_name": "eth0", "static_ips": [ "10.88.0.2" ] } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "subnets": [ { "gateway": "10.88.0.1", "subnet": "10.88.0.0/16" } ] } } } containers-netavark-68510f4/test/testfiles/ipv6-bridge-staticroutes.json000066400000000000000000000025061475240420400264750ustar00rootroot00000000000000{ "container_id": "f031bf33eecba75d0d84952337b1ceef6a239eb8e94b48aee0993d0791345325", "container_name": "somename", "networks": { "podman1": { "static_ips": [ "fd10:88:a::2" ], "interface_name": "eth0" } }, "network_info": { "podman1": { "name": "podman1", "id": "ec79dd0cad82083c8ac5cc23e9542e4ddea813dff60d68258d36e84f6393b63b", "driver": "bridge", "network_interface": "podman1", "subnets": [ { "subnet": "fd10:88:a::/64", "gateway": "fd10:88:a::1" } ], "routes": [ { "destination": "fd10:89:b::/64", "gateway": "fd10:88:a::ac02" }, { "destination": "fd10:89:c::/64", "gateway": "fd10:88:a::ac03" }, { "destination": "fd10:51:b::/64", "gateway": "fd10:49:b::30" } ], "ipv6_enabled": true, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/ipv6-bridge.json000066400000000000000000000016011475240420400237410ustar00rootroot00000000000000{ "container_id": "f031bf33eecba75d0d84952337b1ceef6a239eb8e94b48aee0993d0791345325", "container_name": "somename", "networks": { "podman1": { "static_ips": [ "fd10:88:a::2" ], "interface_name": "eth0" } }, "network_info": { "podman1": { "name": "podman1", "id": "ec79dd0cad82083c8ac5cc23e9542e4ddea813dff60d68258d36e84f6393b63b", "driver": "bridge", "network_interface": "podman1", "subnets": [ { "subnet": "fd10:88:a::/64", "gateway": "fd10:88:a::1" } ], "ipv6_enabled": true, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/ipvlan-internal.json000066400000000000000000000014101475240420400247240ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "ipvlan", "network_interface": "dummy0", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": true, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/ipvlan-mtu.json000066400000000000000000000015121475240420400237200ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "ipvlan", "network_interface": "dummy0", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" }, "options": { "mtu": "1400" } } } } containers-netavark-68510f4/test/testfiles/ipvlan-nodefaultroute.json000066400000000000000000000015231475240420400261550ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "ipvlan", "network_interface": "dummy0", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": true, "ipam_options": { "driver": "host-local" }, "options": { "no_default_route": "true" } } } } containers-netavark-68510f4/test/testfiles/ipvlan-staticroutes.json000066400000000000000000000025741475240420400256550ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2", "fd:1f1f::2" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "ipvlan", "network_interface": "dummy0", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" }, { "subnet": "fd:1f1f::/64", "gateway": "fd:1f1f::1" } ], "routes": [ { "destination": "10.89.0.0/24", "gateway": "10.88.0.2" }, { "destination": "10.90.0.0/24", "gateway": "10.88.0.3" }, { "destination": "10.92.0.0/24", "gateway": "10.91.0.1" }, { "destination": "fd:2f2f::/64", "gateway": "fd:1f1f::20" } ], "ipv6_enabled": true, "internal": false, "dns_enabled": true, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/ipvlan.json000066400000000000000000000014101475240420400231120ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "ipvlan", "network_interface": "dummy0", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": true, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/isolate1.json000066400000000000000000000017371475240420400233560ustar00rootroot00000000000000{ "container_id": "01f0b94d5f4c1", "container_name": "isolatedcontainer1", "networks": { "isolate1": { "interface_name": "eth1", "static_ips": [ "10.89.0.2", "fd90::2" ] } }, "network_info": { "isolate1": { "dns_enabled": false, "driver": "bridge", "id": "f3ac3c903b76f20e8a122b1eb2ba393ab2519ba516626be5b490b66dc96b54b7", "internal": false, "ipv6_enabled": false, "name": "isolate1", "network_interface": "isolate1", "subnets": [ { "gateway": "10.89.0.1", "subnet": "10.89.0.0/24" }, { "subnet": "fd90::/64", "gateway": "fd90::1" } ], "options": { "isolate": "true" } } } } containers-netavark-68510f4/test/testfiles/isolate2.json000066400000000000000000000017371475240420400233570ustar00rootroot00000000000000{ "container_id": "01f0b94d5f4c1", "container_name": "isolatedcontainer1", "networks": { "isolate2": { "interface_name": "eth2", "static_ips": [ "10.89.1.2", "fd99::2" ] } }, "network_info": { "isolate2": { "dns_enabled": false, "driver": "bridge", "id": "f3ac3c903b76f20e8a122b1eb2ba393ab2519ba516626be5b490b66dc96b54b7", "internal": false, "ipv6_enabled": false, "name": "isolate2", "network_interface": "isolate2", "subnets": [ { "gateway": "10.89.1.1", "subnet": "10.89.1.0/24" }, { "subnet": "fd99::/64", "gateway": "fd99::1" } ], "options": { "isolate": "true" } } } } containers-netavark-68510f4/test/testfiles/isolate3.json000066400000000000000000000017411475240420400233530ustar00rootroot00000000000000{ "container_id": "01f0b94d5f4c1", "container_name": "isolatedcontainer1", "networks": { "isolate3": { "interface_name": "eth3", "static_ips": [ "10.89.2.2", "fd92::2" ] } }, "network_info": { "isolate3": { "dns_enabled": false, "driver": "bridge", "id": "f3ac3c903b76f20e8a122b1eb2ba393ab2519ba516626be5b490b66dc96b54b7", "internal": false, "ipv6_enabled": false, "name": "isolate3", "network_interface": "isolate3", "subnets": [ { "gateway": "10.89.2.1", "subnet": "10.89.2.0/24" }, { "subnet": "fd92::/64", "gateway": "fd92::1" } ], "options": { "isolate": "strict" } } } } containers-netavark-68510f4/test/testfiles/isolate4.json000066400000000000000000000017411475240420400233540ustar00rootroot00000000000000{ "container_id": "01f0b94d5f4c1", "container_name": "isolatedcontainer1", "networks": { "isolate4": { "interface_name": "eth4", "static_ips": [ "10.89.3.2", "fd93::2" ] } }, "network_info": { "isolate4": { "dns_enabled": false, "driver": "bridge", "id": "f3ac3c903b76f20e8a122b1eb2ba393ab2519ba516626be5b490b66dc96b54b7", "internal": false, "ipv6_enabled": false, "name": "isolate4", "network_interface": "isolate4", "subnets": [ { "gateway": "10.89.3.1", "subnet": "10.89.3.0/24" }, { "subnet": "fd93::/64", "gateway": "fd93::1" } ], "options": { "isolate": "strict" } } } } containers-netavark-68510f4/test/testfiles/macvlan-dhcp.json000066400000000000000000000012021475240420400241550ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "macvlan", "network_interface": "enp9s0u2u1u2", "subnets": [ ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "dhcp" } } } } containers-netavark-68510f4/test/testfiles/macvlan-internal.json000066400000000000000000000014111475240420400250550ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "macvlan", "network_interface": "dummy0", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": true, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/macvlan-mtu.json000066400000000000000000000015131475240420400240510ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "macvlan", "network_interface": "dummy0", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" }, "options": { "mtu": "1400" } } } } containers-netavark-68510f4/test/testfiles/macvlan-nodefaultroute.json000066400000000000000000000015241475240420400263060ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "macvlan", "network_interface": "dummy0", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": true, "ipam_options": { "driver": "host-local" }, "options": { "no_default_route": "true" } } } } containers-netavark-68510f4/test/testfiles/macvlan-staticroutes.json000066400000000000000000000025751475240420400260060ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2", "fd:1f1f::2" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "macvlan", "network_interface": "dummy0", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" }, { "subnet": "fd:1f1f::/64", "gateway": "fd:1f1f::1" } ], "routes": [ { "destination": "10.89.0.0/24", "gateway": "10.88.0.2" }, { "destination": "10.90.0.0/24", "gateway": "10.88.0.3" }, { "destination": "10.92.0.0/24", "gateway": "10.91.0.1" }, { "destination": "fd:2f2f::/64", "gateway": "fd:1f1f::20" } ], "ipv6_enabled": true, "internal": false, "dns_enabled": true, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/macvlan.json000066400000000000000000000014111475240420400232430ustar00rootroot00000000000000{ "container_id": "someID", "container_name": "someName", "networks": { "podman": { "static_ips": [ "10.88.0.2" ], "interface_name": "eth0" } }, "network_info": { "podman": { "name": "podman", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", "driver": "macvlan", "network_interface": "dummy0", "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": true, "ipam_options": { "driver": "host-local" } } } } containers-netavark-68510f4/test/testfiles/metric-macvlan.json000066400000000000000000000020001475240420400245170ustar00rootroot00000000000000{ "container_id": "bc14fe7cd3633e7be338522002bb0c3ccb18150da7a6c733735ffdf8ff7e85d1", "container_name": "metrictest", "networks": { "metric": { "interface_name": "eth1", "static_ips": [ "10.89.0.2", "fde0::2" ] } }, "network_info": { "metric": { "dns_enabled": false, "driver": "macvlan", "id": "7ba44a9a709f8093621eae1a1db2ccafc2471bae19cdf9dd2ea7cf3773b9211c", "internal": false, "ipv6_enabled": true, "name": "metric", "network_interface": "dummy0", "subnets": [ { "gateway": "10.89.0.1", "subnet": "10.89.0.0/24" }, { "subnet": "fde0::/64", "gateway": "fde0::1" } ], "options": { "metric": "200" } } } } containers-netavark-68510f4/test/testfiles/metric.json000066400000000000000000000017771475240420400231240ustar00rootroot00000000000000{ "container_id": "bc14fe7cd3633e7be338522002bb0c3ccb18150da7a6c733735ffdf8ff7e85d1", "container_name": "metrictest", "networks": { "metric": { "interface_name": "eth1", "static_ips": [ "10.89.0.2", "fde0::2" ] } }, "network_info": { "metric": { "dns_enabled": false, "driver": "bridge", "id": "7ba44a9a709f8093621eae1a1db2ccafc2471bae19cdf9dd2ea7cf3773b9211c", "internal": false, "ipv6_enabled": true, "name": "metric", "network_interface": "metric", "subnets": [ { "gateway": "10.89.0.1", "subnet": "10.89.0.0/24" }, { "subnet": "fde0::/64", "gateway": "fde0::1" } ], "options": { "metric": "200" } } } } containers-netavark-68510f4/test/testfiles/simplebridge-vrf.json000066400000000000000000000015131475240420400250660ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "networks": { "podman": { "interface_name": "eth0", "static_ips": [ "10.88.0.2" ] } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "subnets": [ { "gateway": "10.88.0.1", "subnet": "10.88.0.0/16" } ], "options": { "vrf": "test-vrf" } } } } containers-netavark-68510f4/test/testfiles/simplebridge.json000066400000000000000000000013641475240420400242770ustar00rootroot00000000000000{ "container_id": "6ce776ea58b5", "container_name": "testcontainer", "networks": { "podman": { "interface_name": "eth0", "static_ips": [ "10.88.0.2" ] } }, "network_info": { "podman": { "dns_enabled": false, "driver": "bridge", "id": "53ce4390f2adb1681eb1a90ec8b48c49c015e0a8d336c197637e7f65e365fa9e", "internal": false, "ipv6_enabled": false, "name": "podman", "network_interface": "podman0", "subnets": [ { "gateway": "10.88.0.1", "subnet": "10.88.0.0/16" } ] } } } containers-netavark-68510f4/test/testfiles/two-networks.json000066400000000000000000000035531475240420400243160ustar00rootroot00000000000000{ "container_id": "a417588994662895d8b41adf8d74a83ac0cc38eb56d85d8e1268aae1e19e07e1", "container_name": "heuristic_archimedes", "port_mappings": [ { "host_ip": "", "container_port": 8080, "host_port": 8080, "range": 1, "protocol": "tcp" } ], "networks": { "t1": { "static_ips": [ "10.89.1.2" ], "interface_name": "eth0" }, "t2": { "static_ips": [ "10.89.2.2" ], "interface_name": "eth1" } }, "network_info": { "t1": { "name": "t1", "id": "fae505bba2b3ad2b9bc748f0d322864d44a3c976c642ee347e5276729dfb4d51", "driver": "bridge", "network_interface": "podman2", "created": "2022-10-18T18:34:21.701124201+02:00", "subnets": [ { "subnet": "10.89.1.0/24", "gateway": "10.89.1.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } }, "t2": { "name": "t2", "id": "d7322dfb9353cb5c1adabfc46555164654f339fa3dd0d2533d103e2a4c5240e2", "driver": "bridge", "network_interface": "podman3", "created": "2022-10-18T18:34:23.266425802+02:00", "subnets": [ { "subnet": "10.89.2.0/24", "gateway": "10.89.2.1" } ], "ipv6_enabled": false, "internal": false, "dns_enabled": false, "ipam_options": { "driver": "host-local" } } } }