├── .fmf └── version ├── src ├── test │ ├── mod.rs │ └── config │ │ ├── podman_bad_config │ │ └── podman │ │ ├── test1 │ │ ├── podman_v6_entries │ │ ├── podman_v6_entries_proper │ │ └── podman_v6_entries │ │ ├── podman2 │ │ ├── podman │ │ └── podman │ │ ├── podman_custom_dns_servers │ │ └── podman │ │ └── network_scoped_custom_dns │ │ └── podman ├── dns │ ├── mod.rs │ └── coredns.rs ├── commands │ ├── mod.rs │ ├── version.rs │ └── run.rs ├── server │ ├── mod.rs │ └── serve.rs ├── config │ ├── constants.rs │ └── mod.rs ├── lib.rs ├── main.rs ├── error.rs └── backend │ └── mod.rs ├── .gitignore ├── OWNERS ├── contrib ├── perf │ ├── nslookup.py │ └── run.sh └── cirrus │ ├── netavark_cache_groom.sh │ ├── setup.sh │ ├── lib.sh │ └── runner.sh ├── test ├── tmt │ ├── test_integration.sh │ └── main.fmf ├── dnsmasq.conf ├── 400-aliases.bats ├── 600-errors.bats ├── 500-reverse-lookups.bats ├── 200-two-networks.bats ├── 300-three-networks.bats ├── 100-basic-name-resolution.bats └── helpers.bash ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── docs └── publish-crate.md ├── hack ├── tree_status.sh └── get_ci_vm.sh ├── rpm ├── gating.yaml └── aardvark-dns.spec ├── plans └── main.fmf ├── .github ├── workflows │ ├── check_cirrus_cron.yml │ └── release.yml └── renovate.json5 ├── .packit-copr-rpm.sh ├── README.md ├── Cargo.toml ├── config.md ├── DISTRO_PACKAGE.md ├── RELEASE_NOTES.md ├── Makefile ├── .packit.yaml ├── .cirrus.yml └── LICENSE /.fmf/version: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod test; 2 | -------------------------------------------------------------------------------- /src/dns/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod coredns; 2 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod run; 2 | pub mod version; 3 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | // Serve DNS requests on the given bind addresses. 2 | pub mod serve; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | target/ 3 | targets/ 4 | *.swp 5 | netavark.1 6 | vendor/ 7 | vendor-tarball/ 8 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - baude 3 | - lsm5 4 | - Luap99 5 | - mheon 6 | reviewers: 7 | - flouthoc 8 | -------------------------------------------------------------------------------- /src/config/constants.rs: -------------------------------------------------------------------------------- 1 | pub static AARDVARK_PID_FILE: &str = "aardvark.pid"; 2 | pub static INTERNAL_SUFFIX: &str = "%int"; 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod backend; 2 | pub mod commands; 3 | pub mod config; 4 | pub mod dns; 5 | pub mod error; 6 | pub mod server; 7 | -------------------------------------------------------------------------------- /contrib/perf/nslookup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import socket 4 | import sys 5 | 6 | for i in range(0, 10_000): 7 | socket.getaddrinfo(sys.argv[1], 0) 8 | -------------------------------------------------------------------------------- /test/tmt/test_integration.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exo pipefail 4 | 5 | rpm -q aardvark-dns aardvark-dns-tests netavark 6 | 7 | cd /usr/share/aardvark-dns/ 8 | bats test/ 9 | -------------------------------------------------------------------------------- /src/test/config/podman_bad_config/podman: -------------------------------------------------------------------------------- 1 | 10.88.0.1 dfdsfds 2 | 10.88.0.2 condescendingnash 3 | 95655fb6832ba134efa66e9c80862a6c9b04f3cc6abf8adfdda8c38112c2c6fa hopefulmontalcini,testdbctr 4 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## The Aardvark-dns Project Community Code of Conduct 2 | 3 | The Aardvark-dns project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/main/CODE-OF-CONDUCT.md). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Containers/aardvark 2 | 3 | We'd love to have you join the community! [Learn how to contribute](https://github.com/containers/common/blob/main/CONTRIBUTING.md) to the Containers Group Projects. 4 | -------------------------------------------------------------------------------- /src/test/config/test1: -------------------------------------------------------------------------------- 1 | fd35:fb67:49e1:349::1 2 | 68fb291b0318b54a71f6f3636e58bd0896f084e5ba4fa311ecf36e019c5e6e43 fd35:fb67:49e1:349::2 condescending_nash 3 | 8bcc5fe0cb09bee5dfb71d61503a87688cfc82aa5f130bcedb19357a17765926 fd35:fb67:49e1:349::3 trusting_zhukovsky,ctr1,ctra 4 | -------------------------------------------------------------------------------- /src/test/config/podman_v6_entries/podman_v6_entries_proper: -------------------------------------------------------------------------------- 1 | 10.0.0.1,10.0.1.1,fdfd::1,fddd::1 2 | f35256b5e2f72ec8cb7d974d4f8841686fc8921fdfbc867285b50164e313f715 10.0.0.2,10.0.1.2 fdfd::2,fddd::2 testmulti1 3 | e5df0cdbe0136a30cc3e848d495d2cc6dada25b7dedc776b4584ce2cbba6f06f 10.0.0.3,10.0.1.3 fdfd::3,fddd::3 testmulti2 4 | -------------------------------------------------------------------------------- /src/test/config/podman2: -------------------------------------------------------------------------------- 1 | 10.88.0.1 2 | 68fb291b0318b54a71f6f3636e58bd0896f084e5ba4fa311ecf36e019c5e6e43 10.88.0.2 condescending_nash 3 | 95655fb6832ba134efa66e9c80862a6c9b04f3cc6abf8adfdda8c38112c2c6fa 10.88.0.3 hopeful_montalcini,testdbctr 4 | 8bcc5fe0cb09bee5dfb71d61503a87688cfc82aa5f130bcedb19357a17765926 10.88.0.4 trusting_zhukovsky,ctr1,ctra 5 | -------------------------------------------------------------------------------- /docs/publish-crate.md: -------------------------------------------------------------------------------- 1 | # Publishing aardvark-dns crate to crates.io 2 | ### Steps 3 | * Make sure you have already done `cargo login` on your current session with a valid token. 4 | * `cd aardvark-dns` 5 | * Git checkout the version which you want to publish. 6 | * `make crate-publish` 7 | * New version should be reflected here: https://crates.io/crates/aardvark-dns/versions 8 | -------------------------------------------------------------------------------- /hack/tree_status.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | STATUS=$(git status --porcelain) 5 | if [[ -z $STATUS ]] 6 | then 7 | echo "tree is clean" 8 | else 9 | echo "tree is dirty" 10 | echo "" 11 | echo "$STATUS" 12 | echo "" 13 | echo "---------------------- Diff below ----------------------" 14 | echo "" 15 | git --no-pager diff 16 | exit 1 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /test/dnsmasq.conf: -------------------------------------------------------------------------------- 1 | interface=lo 2 | bind-interfaces 3 | 4 | no-hosts 5 | no-resolv 6 | 7 | log-queries 8 | 9 | # aone and bone should return NXDOMAIN, by default dnsmasq returns REFUSED 10 | address=/aone/ 11 | address=/bone/ 12 | address=/testname/198.51.100.1 13 | address=/testname.local/198.51.100.2 14 | address=/example.podman.io/198.51.100.100 15 | 16 | 17 | txt-record=example.podman.io,"v=spf1 a -all" 18 | -------------------------------------------------------------------------------- /src/test/config/podman/podman: -------------------------------------------------------------------------------- 1 | 10.88.0.1 2 | 68fb291b0318b54a71f6f3636e58bd0896f084e5ba4fa311ecf36e019c5e6e43 10.88.0.2 condescendingnash 3 | 68fb291b0318b54a71f6f3636e58bd0896f084e5ba4fa311ecf36e019c5e6e48 10.88.0.5 HelloWorld 4 | 95655fb6832ba134efa66e9c80862a6c9b04f3cc6abf8adfdda8c38112c2c6fa 10.88.0.3 hopefulmontalcini,testdbctr 5 | 8bcc5fe0cb09bee5dfb71d61503a87688cfc82aa5f130bcedb19357a17765926 10.88.0.4 trustingzhukovsky,ctr1,ctra 6 | -------------------------------------------------------------------------------- /src/test/config/podman_v6_entries/podman_v6_entries: -------------------------------------------------------------------------------- 1 | 10.89.0.1 2 | 7b46c7ad93fcbcb945c35286a5ba19d6976093e2ce39d2cb38ba1eba636404ab 10.89.0.2 test1,7b46c7ad93fc 3 | 7b46c7ad93fcbcb945c35286a5ba19d6976093e2ce39d2cb38ba1eba636404ab fdfd:733b:dc3:220b::2 test1,7b46c7ad93fc 4 | 88dde8a2489780d3c8c90db54a9a97faf5dbe4f555b23e27880ca189dae0e2b0 10.89.0.3 test2,88dde8a24897 5 | 88dde8a2489780d3c8c90db54a9a97faf5dbe4f555b23e27880ca189dae0e2b0 fdfd:733b:dc3:220b::3 test2,88dde8a24897 6 | -------------------------------------------------------------------------------- /rpm/gating.yaml: -------------------------------------------------------------------------------- 1 | --- !Policy 2 | product_versions: 3 | - fedora-* 4 | decision_contexts: 5 | - bodhi_update_push_stable 6 | - bodhi_update_push_testing 7 | subject_type: koji_build 8 | rules: 9 | - !PassingTestCaseRule {test_case_name: fedora-ci.koji-build.tier0.functional} 10 | 11 | --- !Policy 12 | product_versions: 13 | - rhel-* 14 | decision_context: osci_compose_gate 15 | rules: 16 | - !PassingTestCaseRule {test_case_name: osci.brew-build.tier0.functional} 17 | -------------------------------------------------------------------------------- /src/test/config/podman_custom_dns_servers/podman: -------------------------------------------------------------------------------- 1 | 10.88.0.1 2 | 68fb291b0318b54a71f6f3636e58bd0896f084e5ba4fa311ecf36e019c5e6e43 10.88.0.2 condescendingnash 8.8.8.8 3 | 68fb291b0318b54a71f6f3636e58bd0896f084e5ba4fa311ecf36e019c5e6e48 10.88.0.5 HelloWorld 3.3.3.3,1.1.1.1,::1 4 | 95655fb6832ba134efa66e9c80862a6c9b04f3cc6abf8adfdda8c38112c2c6fa 10.88.0.3 hopefulmontalcini,testdbctr 5 | 8bcc5fe0cb09bee5dfb71d61503a87688cfc82aa5f130bcedb19357a17765926 10.88.0.4 trustingzhukovsky,ctr1,ctra 6 | -------------------------------------------------------------------------------- /src/test/config/network_scoped_custom_dns/podman: -------------------------------------------------------------------------------- 1 | 10.88.0.1 127.0.0.1,::2 2 | 68fb291b0318b54a71f6f3636e58bd0896f084e5ba4fa311ecf36e019c5e6e43 10.88.0.2 condescendingnash 8.8.8.8 3 | 68fb291b0318b54a71f6f3636e58bd0896f084e5ba4fa311ecf36e019c5e6e48 10.88.0.5 HelloWorld 3.3.3.3,1.1.1.1,::1 4 | 95655fb6832ba134efa66e9c80862a6c9b04f3cc6abf8adfdda8c38112c2c6fa 10.88.0.3 hopefulmontalcini,testdbctr 5 | 8bcc5fe0cb09bee5dfb71d61503a87688cfc82aa5f130bcedb19357a17765926 10.88.0.4 trustingzhukovsky,ctr1,ctra 6 | -------------------------------------------------------------------------------- /test/tmt/main.fmf: -------------------------------------------------------------------------------- 1 | /validate: 2 | tag: [ no-rpm, validate ] 3 | summary: Validate test 4 | test: make -C ../.. validate 5 | require: 6 | - clippy 7 | - rustfmt 8 | 9 | /unit: 10 | tag: [ no-rpm, unit ] 11 | summary: Unit tests 12 | test: make -C ../.. unit 13 | require: 14 | - cargo 15 | 16 | /integration: 17 | tag: [ rpm, integration ] 18 | summary: Integration tests 19 | test: bash test_integration.sh 20 | environment: 21 | AARDVARK: /usr/libexec/podman/aardvark-dns 22 | require: 23 | - aardvark-dns-tests 24 | adjust: 25 | duration: 10m 26 | when: arch == aarch64 27 | -------------------------------------------------------------------------------- /contrib/perf/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PODMAN=${PODMAN-podman} 4 | DIR=$(dirname -- "${BASH_SOURCE[0]}") 5 | IMAGE=docker.io/library/python 6 | JOBS=${JOBS:-$(nproc)} 7 | netname="testnet" 8 | 9 | 10 | $PODMAN rm -fa -t0 11 | $PODMAN network rm -f $netname 12 | 13 | $PODMAN network create $netname 14 | 15 | # first command to spawn aardvark-dns 16 | $PODMAN run -i -d --network $netname --name starter $IMAGE 17 | 18 | perf stat -p $(pgrep -n aardvark-dns) &> $DIR/perf.log & 19 | 20 | for i in $( seq 1 $JOBS ) 21 | do 22 | $PODMAN run -v $DIR/nslookup.py:/nslookup.py:z --name test$i --network $netname:alias=testabc$i -d $IMAGE /nslookup.py testabc$i 23 | done 24 | 25 | $PODMAN rm -f -t0 starter 26 | 27 | # wait for perf to finish 28 | # because aardvark-dns exists on its own when all containers are done this should not hang 29 | wait 30 | 31 | # 32 | $PODMAN rm -fa -t0 33 | $PODMAN network rm -f $netname 34 | -------------------------------------------------------------------------------- /contrib/cirrus/netavark_cache_groom.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script is intended to be run from Cirrus-CI to prepare the 4 | # rust targets cache for re-use during subsequent runs. This mainly 5 | # involves removing files and directories which change frequently 6 | # but are cheap/quick to regenerate - i.e. prevent "cache-flapping". 7 | # Any other use of this script is not supported and may cause harm. 8 | 9 | set -eo pipefail 10 | 11 | SCRIPT_DIRPATH=$(dirname ${BASH_SOURCE[0]}) 12 | source $SCRIPT_DIRPATH/lib.sh 13 | 14 | if [[ "$CIRRUS_CI" != true ]] || [[ -z "$NETAVARK_BRANCH" ]]; then 15 | die "Script is not intended for use outside of Cirrus-CI" 16 | fi 17 | 18 | SCRIPT_DEST=$SCRIPT_DIRPATH/cache_groom.sh 19 | showrun curl --location --silent --show-error -o $SCRIPT_DEST \ 20 | https://raw.githubusercontent.com/containers/netavark/$NETAVARK_BRANCH/contrib/cirrus/cache_groom.sh 21 | 22 | # Certain common automation library calls assume execution from this file 23 | exec bash $SCRIPT_DEST 24 | -------------------------------------------------------------------------------- /plans/main.fmf: -------------------------------------------------------------------------------- 1 | discover: 2 | how: fmf 3 | execute: 4 | how: tmt 5 | prepare: 6 | - when: distro == centos-stream or distro == rhel 7 | how: shell 8 | script: | 9 | dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-$(rpm --eval '%{?rhel}').noarch.rpm 10 | dnf -y config-manager --set-enabled epel 11 | order: 10 12 | - when: initiator == packit 13 | how: shell 14 | script: | 15 | COPR_REPO_FILE="/etc/yum.repos.d/*podman-next*.repo" 16 | if compgen -G $COPR_REPO_FILE > /dev/null; then 17 | sed -i -n '/^priority=/!p;$apriority=1' $COPR_REPO_FILE 18 | fi 19 | dnf -y upgrade --allowerasing 20 | order: 20 21 | 22 | /no-rpm: 23 | summary: Run tests independent of rpm 24 | discover+: 25 | filter: tag:no-rpm 26 | adjust+: 27 | - enabled: false 28 | when: initiator is not defined or initiator != packit 29 | 30 | /rpm: 31 | summary: Run tests on the rpm 32 | discover+: 33 | filter: tag:rpm 34 | -------------------------------------------------------------------------------- /.github/workflows/check_cirrus_cron.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # See also: 4 | # https://github.com/containers/podman/blob/main/.github/workflows/check_cirrus_cron.yml 5 | 6 | on: 7 | # Note: This only applies to the default branch. 8 | schedule: 9 | # N/B: This should correspond to a period slightly after 10 | # the last job finishes running. See job defs. at: 11 | # https://cirrus-ci.com/settings/repository/6483741884284928 12 | - cron: '03 03 * * 1-5' 13 | # Debug: Allow triggering job manually in github-actions WebUI 14 | workflow_dispatch: {} 15 | 16 | jobs: 17 | # Ref: https://docs.github.com/en/actions/using-workflows/reusing-workflows 18 | call_cron_failures: 19 | uses: containers/podman/.github/workflows/check_cirrus_cron.yml@main 20 | secrets: 21 | SECRET_CIRRUS_API_KEY: ${{secrets.SECRET_CIRRUS_API_KEY}} 22 | ACTION_MAIL_SERVER: ${{secrets.ACTION_MAIL_SERVER}} 23 | ACTION_MAIL_USERNAME: ${{secrets.ACTION_MAIL_USERNAME}} 24 | ACTION_MAIL_PASSWORD: ${{secrets.ACTION_MAIL_PASSWORD}} 25 | ACTION_MAIL_SENDER: ${{secrets.ACTION_MAIL_SENDER}} 26 | -------------------------------------------------------------------------------- /src/commands/version.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::fmt; 3 | 4 | #[derive(Parser, Debug)] 5 | pub struct Version {} 6 | 7 | #[derive(Debug)] 8 | struct Info { 9 | version: &'static str, 10 | commit: &'static str, 11 | build_time: &'static str, 12 | target: &'static str, 13 | } 14 | 15 | // since we do not need a json library here we just create the json output manually 16 | impl fmt::Display for Info { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | write!( 19 | f, 20 | "{{ 21 | \"version\": \"{}\", 22 | \"commit\": \"{}\", 23 | \"build_time\": \"{}\", 24 | \"target\": \"{}\" 25 | }}", 26 | self.version, self.commit, self.build_time, self.target 27 | ) 28 | } 29 | } 30 | 31 | impl Version { 32 | pub fn exec(&self) { 33 | let info = Info { 34 | version: env!("CARGO_PKG_VERSION"), 35 | commit: env!("GIT_COMMIT"), 36 | build_time: env!("BUILD_TIMESTAMP"), 37 | target: env!("BUILD_TARGET"), 38 | }; 39 | println!("{info}"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/400-aliases.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | 8 | @test "two containers on the same network with aliases" { 9 | # container a1 10 | subnet_a=$(random_subnet 5) 11 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" aliases='"a1", "1a"' 12 | config_a1="$config" 13 | a1_ip=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 14 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 15 | create_container "$config_a1" 16 | a1_pid=$CONTAINER_NS_PID 17 | 18 | # container a2 19 | create_config network_name="podman1" container_id=$(random_string 64) container_name="atwo" subnet="$subnet_a" aliases='"a2", "2a"' 20 | config_a2="$config" 21 | a2_ip=$(echo "$config_a2" | jq -r .networks.podman1.static_ips[0]) 22 | create_container "$config_a2" 23 | a2_pid="$CONTAINER_NS_PID" 24 | 25 | dig "$a1_pid" "a2" "$gw" 26 | assert "$a2_ip" 27 | dig "$a1_pid" "2a" "$gw" 28 | assert "$a2_ip" 29 | dig "$a2_pid" "a1" "$gw" 30 | assert "$a1_ip" 31 | dig "$a2_pid" "1a" "$gw" 32 | assert "$a1_ip" 33 | } 34 | -------------------------------------------------------------------------------- /.packit-copr-rpm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script handles any custom processing of the spec file using the `fix-spec-file` 4 | # action in .packit.yaml. These steps only work on copr builds, not on official 5 | # Fedora builds. 6 | 7 | set -exo pipefail 8 | 9 | PACKAGE=aardvark-dns 10 | SPEC_FILE=rpm/"$PACKAGE".spec 11 | 12 | # Get Version from HEAD 13 | VERSION=$(grep '^version' Cargo.toml | cut -d\" -f2) 14 | 15 | # RPM Version can't take "-" 16 | RPM_VERSION="${VERSION//-/\~}" 17 | 18 | # Generate source tarball from HEAD 19 | git-archive-all -C "$(git rev-parse --show-toplevel)" --prefix="$PACKAGE"-"$VERSION"/ rpm/"$PACKAGE"-"$VERSION".tar.gz 20 | 21 | # RPM Spec modifications 22 | 23 | # Use the Version from HEAD in rpm spec 24 | sed -i "s/^Version:.*/Version: $RPM_VERSION/" "$SPEC_FILE" 25 | 26 | # Use Packit's supplied variable in the Release field in rpm spec 27 | sed -i "s/^Release:.*/Release: $PACKIT_RPMSPEC_RELEASE%{?dist}/" "$SPEC_FILE" 28 | 29 | # Use above generated tarball as Source in rpm spec 30 | sed -i "s/^Source0:.*.tar.gz/Source0: $PACKAGE-$VERSION.tar.gz/" $SPEC_FILE 31 | 32 | # Don't need Source1 for copr builds 33 | sed -i "/^Source1/d" "$SPEC_FILE" 34 | 35 | # Update setup macro to use the correct build dir 36 | sed -i "s/^%autosetup.*/%autosetup -Sgit -n %{name}-$VERSION/" "$SPEC_FILE" 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aardvark-dns 2 | 3 | Aardvark-dns is an authoritative dns server for `A/AAAA` container records. It can forward other requests 4 | to configured resolvers. 5 | 6 | Read more about configuration in `src/backend/mod.rs`. It is mostly intended to be used with 7 | [Netavark](https://github.com/containers/netavark/) which will launch it automatically if both are 8 | installed. 9 | 10 | ```console 11 | aardvark-dns 0.1.0 12 | 13 | USAGE: 14 | aardvark-dns [OPTIONS] 15 | 16 | FLAGS: 17 | -h, --help Print help information 18 | -V, --version Print version information 19 | 20 | OPTIONS: 21 | -c, --config Path to configuration directory 22 | -p, --port Host port for aardvark servers, defaults to 5533 23 | 24 | SUBCOMMANDS: 25 | help Print this message or the help of the given subcommand(s) 26 | run Runs the aardvark dns server with the specified configuration directory 27 | ``` 28 | 29 | ### MSRV (Minimum Supported Rust Version) 30 | 31 | v1.86 32 | 33 | We test that Netavark can be build on this Rust version and on some newer versions. 34 | All newer versions should also build, and if they do not, the issue should be 35 | reported and will be fixed. Older versions are not guaranteed to build and issues 36 | will not be fixed. 37 | 38 | ### Build 39 | 40 | ```console 41 | make 42 | ``` 43 | 44 | ### Run Example 45 | 46 | ```console 47 | RUST_LOG=trace ./bin/aardvark-dns --config src/test/config/podman/ --port 5533 run 48 | ``` 49 | 50 | ### [Configuration file format](./config.md) 51 | 52 | ### [Contributing](./CONTRIBUTING.md) 53 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aardvark-dns" 3 | # This version specification right below is reused by .packit.sh to generate rpm version 4 | version = "2.0.0-dev" 5 | edition = "2018" 6 | authors = ["github.com/containers"] 7 | license = "Apache-2.0" 8 | readme = "README.md" 9 | description = "A container-focused DNS server" 10 | homepage = "https://github.com/containers/aardvark-dns" 11 | repository = "https://github.com/containers/aardvark-dns" 12 | categories = ["virtualization"] 13 | exclude = ["/.cirrus.yml", "/.github/*"] 14 | rust-version = "1.86" 15 | 16 | [package.metadata.vendor-filter] 17 | # This list is not exhaustive. 18 | platforms = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "powerpc64le-unknown-linux-gnu", 19 | "s390x-unknown-linux-gnu", "riscv64gc-unknown-linux-gnu", 20 | "x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl", 21 | ] 22 | 23 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 24 | 25 | [dependencies] 26 | clap = { version = "~4.5.53", features = ["derive"] } 27 | syslog = "^7.0.0" 28 | log = "0.4.29" 29 | hickory-server = "0.25.2" 30 | hickory-proto = { version = "0.25.2", features = ["tokio"] } 31 | hickory-client = "0.25.2" 32 | futures-util = { version = "0.3.31", default-features = false } 33 | tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "net", "signal"] } 34 | nix = { version = "0.30.1", features = ["fs", "signal", "net"] } 35 | libc = "0.2.178" 36 | arc-swap = "1.7.1" 37 | flume = "0.12.0" 38 | inotify = "0.11.0" 39 | futures = "0.3.31" 40 | 41 | [build-dependencies] 42 | chrono = "0.4.42" 43 | -------------------------------------------------------------------------------- /contrib/cirrus/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script configures the CI runtime environment. It's intended 4 | # to be used by Cirrus-CI, not humans. 5 | 6 | set -e 7 | 8 | source $(dirname $0)/lib.sh 9 | 10 | # Only do this once 11 | if [[ -r "/etc/ci_environment" ]]; then 12 | msg "It appears ${BASH_SOURCE[0]} already ran, exiting." 13 | exit 0 14 | fi 15 | trap "complete_setup" EXIT 16 | 17 | msg "************************************************************" 18 | msg "Setting up runtime environment" 19 | msg "************************************************************" 20 | show_env_vars 21 | 22 | req_env_vars NETAVARK_URL NETAVARK_BRANCH 23 | cd /usr/libexec/podman 24 | rm -vf netavark* 25 | if showrun curl --fail --location -o /tmp/netavark.zip "$NETAVARK_URL" && \ 26 | unzip -o /tmp/netavark.zip; then 27 | 28 | if [[ $(uname -m) != "x86_64" ]]; then 29 | showrun mv netavark.$(uname -m)-unknown-linux-gnu netavark 30 | fi 31 | showrun chmod a+x /usr/libexec/podman/netavark 32 | else 33 | warn "Error downloading/extracting the latest pre-compiled netavark binary from CI" 34 | showrun cargo install \ 35 | --root /usr/libexec/podman \ 36 | --git https://github.com/containers/netavark \ 37 | --branch "$NETAVARK_BRANCH" 38 | showrun mv /usr/libexec/podman/bin/netavark /usr/libexec/podman 39 | fi 40 | # show netavark commit in CI logs 41 | showrun /usr/libexec/podman/netavark version 42 | 43 | # Warning, this isn't the end. An exit-handler is installed to finalize 44 | # setup of env. vars. This is required for runner.sh to operate properly. 45 | # See complete_setup() in lib.sh for details. 46 | -------------------------------------------------------------------------------- /test/600-errors.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | 8 | 9 | SOCAT_PID= 10 | 11 | function teardown() { 12 | kill -9 $SOCAT_PID 13 | basic_teardown 14 | } 15 | 16 | # check bind error on startup 17 | @test "aardvark-dns should fail when udp port is already bound" { 18 | # bind the port to force a failure for aardvark-dns 19 | # we cannot use run_is_host_netns to run in the background 20 | nsenter -m -n -t $HOST_NS_PID socat UDP4-LISTEN:53 - 3> /dev/null & 21 | SOCAT_PID=$! 22 | 23 | # ensure socat has time to bind the port 24 | sleep 1 25 | 26 | subnet_a=$(random_subnet 5) 27 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" 28 | gw=$(echo "$config" | jq -r .network_info.podman1.subnets[0].gateway) 29 | expected_rc=1 create_container "$config" 30 | assert "$output" =~ "failed to bind udp listener on $gw:53" "bind error message" 31 | } 32 | 33 | @test "aardvark-dns should fail when tcp port is already bound" { 34 | # bind the port to force a failure for aardvark-dns 35 | # we cannot use run_is_host_netns to run in the background 36 | nsenter -m -n -t $HOST_NS_PID socat TCP4-LISTEN:53 - 3> /dev/null & 37 | SOCAT_PID=$! 38 | 39 | # ensure socat has time to bind the port 40 | sleep 1 41 | 42 | subnet_a=$(random_subnet 5) 43 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" 44 | gw=$(echo "$config" | jq -r .network_info.podman1.subnets[0].gateway) 45 | expected_rc=1 create_container "$config" 46 | assert "$output" =~ "failed to bind tcp listener on $gw:53" "bind error message" 47 | } 48 | -------------------------------------------------------------------------------- /config.md: -------------------------------------------------------------------------------- 1 | # Configuration format 2 | 3 | Aardvark-dns will read configuration files from a given directory. 4 | 5 | Inside this directory there should be at least one config file. The name of the file equals the network name. 6 | 7 | ### First line 8 | The first line in the config must contain a comma separated list of listening ips for this network, usually the bridge ips. 9 | At least one ip must be given. 10 | **Note**: An optional second column of comma delimited domain name servers can be used at the network level. All containers 11 | on that network will inherit all the specified name servers instead of using the host's resolver. 12 | 13 | ``` 14 | [comma seperated ip4,ipv6 list][(optional)[space][comma seperated DNS servers]] 15 | ``` 16 | 17 | ### Container entries 18 | All following lines must contain the dns entries in this format: 19 | ``` 20 | [containerID][space][comma sparated ipv4 list][space][comma separated ipv6 list][space][comma separated dns names][(optional)[space][comma seperated DNS servers]] 21 | ``` 22 | 23 | Aardvark-dns will reload all config files when receiving a SIGHUB signal. 24 | 25 | 26 | ## Example 27 | 28 | ``` 29 | 10.0.0.1,fdfd::1 30 | f35256b5e2f72ec8cb7d974d4f8841686fc8921fdfbc867285b50164e313f715 10.0.0.2 fdfd::2 testmulti1 8.8.8.8,1.1.1.1 31 | e5df0cdbe0136a30cc3e848d495d2cc6dada25b7dedc776b4584ce2cbba6f06f 10.0.0.3 fdfd::3 testmulti2 32 | ``` 33 | ## Example with network scoped DNS servers 34 | 35 | ``` 36 | 10.0.0.1,fdfd::1 8.8.8.8,1.1.1.1 37 | f35256b5e2f72ec8cb7d974d4f8841686fc8921fdfbc867285b50164e313f715 10.0.0.2 fdfd::2 testmulti1 8.8.8.8,1.1.1.1 38 | e5df0cdbe0136a30cc3e848d495d2cc6dada25b7dedc776b4584ce2cbba6f06f 10.0.0.3 fdfd::3 testmulti2 39 | ``` 40 | 41 | Also see [./src/test/config/](./src/test/config/) for more config examples 42 | -------------------------------------------------------------------------------- /contrib/cirrus/lib.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Library of common, shared utility functions. This file is intended 4 | # to be sourced by other scripts, not called directly. 5 | 6 | # BEGIN Global export of all variables 7 | set -a 8 | 9 | # Automation library installed at image-build time, 10 | # defining $AUTOMATION_LIB_PATH in this file. 11 | if [[ -r "/etc/automation_environment" ]]; then 12 | source /etc/automation_environment 13 | fi 14 | 15 | if [[ -n "$AUTOMATION_LIB_PATH" ]]; then 16 | source $AUTOMATION_LIB_PATH/common_lib.sh 17 | else 18 | ( 19 | echo "WARNING: It does not appear that containers/automation was installed." 20 | echo " Functionality of most of this library will be negatively impacted" 21 | echo " This ${BASH_SOURCE[0]} was loaded by ${BASH_SOURCE[1]}" 22 | ) > /dev/stderr 23 | fi 24 | 25 | # Unsafe env. vars for display 26 | SECRET_ENV_RE='(ACCOUNT)|(GC[EP]..+)|(SSH)|(PASSWORD)|(TOKEN)' 27 | 28 | # setup.sh calls make_cienv() to cache these values for the life of the VM 29 | if [[ -r "/etc/ci_environment" ]]; then 30 | source /etc/ci_environment 31 | else # set default values - see make_cienv() below 32 | # Install rust packages globally instead of per-user 33 | CARGO_HOME="${CARGO_HOME:-/usr/local/cargo}" 34 | # Ensure cargo packages can be executed 35 | PATH="$PATH:$CARGO_HOME/bin" 36 | fi 37 | 38 | # END Global export of all variables 39 | set -a 40 | 41 | # Shortcut to automation library timeout/retry function 42 | retry() { err_retry 8 1000 "" "$@"; } # just over 4 minutes max 43 | 44 | # Helper to ensure a consistent environment across multiple CI scripts 45 | # containers, and shell environments (e.g. hack/get_ci_vm.sh) 46 | make_cienv(){ 47 | local envname 48 | local envval 49 | local SETUP_ENVIRONMENT=1 50 | for envname in CARGO_HOME PATH CIRRUS_WORKING_DIR SETUP_ENVIRONMENT; do 51 | envval="${!envname}" 52 | # Properly escape values to prevent injection 53 | printf -- "$envname=%q\n" "$envval" 54 | done 55 | } 56 | 57 | complete_setup(){ 58 | set +x 59 | msg "************************************************************" 60 | msg "Completing environment setup, writing vars:" 61 | msg "************************************************************" 62 | make_cienv | tee -a /etc/ci_environment 63 | } 64 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | /* 2 | Renovate is a service similar to GitHub Dependabot, but with 3 | (fantastically) more configuration options. So many options 4 | in fact, if you're new I recommend glossing over this cheat-sheet 5 | prior to the official documentation: 6 | 7 | https://www.augmentedmind.de/2021/07/25/renovate-bot-cheat-sheet 8 | 9 | Configuration Update/Change Procedure: 10 | 1. Make changes 11 | 2. Manually validate changes (from repo-root): 12 | 13 | podman run -it \ 14 | -v ./.github/renovate.json5:/usr/src/app/renovate.json5:z \ 15 | docker.io/renovate/renovate:latest \ 16 | renovate-config-validator 17 | 3. Commit. 18 | 19 | Configuration Reference: 20 | https://docs.renovatebot.com/configuration-options/ 21 | 22 | Monitoring Dashboard: 23 | https://app.renovatebot.com/dashboard#github/containers 24 | 25 | Note: The Renovate bot will create/manage it's business on 26 | branches named 'renovate/*'. Otherwise, and by 27 | default, the only the copy of this file that matters 28 | is the one on the `main` branch. No other branches 29 | will be monitored or touched in any way. 30 | */ 31 | 32 | { 33 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 34 | 35 | /************************************************* 36 | ****** Global/general configuration options ***** 37 | *************************************************/ 38 | 39 | // Re-use predefined sets of configuration options to DRY 40 | "extends": [ 41 | // https://github.com/containers/automation/blob/main/renovate/defaults.json5 42 | "github>containers/automation//renovate/defaults.json5" 43 | ], 44 | 45 | // Permit automatic rebasing when base-branch changes by more than 46 | // one commit. 47 | "rebaseWhen": "behind-base-branch", 48 | 49 | /************************************************* 50 | *** Repository-specific configuration options *** 51 | *************************************************/ 52 | 53 | // Don't leave dep. update. PRs "hanging", assign them to people. 54 | "assignees": ["containers/netavark-maintainers"], 55 | 56 | /************************************************** 57 | ***** Manager-specific configuration options ***** 58 | **************************************************/ 59 | } 60 | -------------------------------------------------------------------------------- /contrib/cirrus/runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | # This script runs in the Cirrus CI environment, invoked from .cirrus.yml . 6 | # It can also be invoked manually in a `hack/get_ci_vm.sh` environment, 7 | # documentation of said usage is TBI. 8 | # 9 | # The principal deciding factor is the first argument. For any 10 | # given value 'xyz' there must be a function '_run_xyz' to handle that 11 | # argument. 12 | 13 | source $(dirname ${BASH_SOURCE[0]})/lib.sh 14 | 15 | _run_noarg() { 16 | die "runner.sh must be called with a single argument" 17 | } 18 | 19 | _run_build() { 20 | # Assume we're on a fast VM, compile everything needed by the 21 | # rest of CI since subsequent tasks may have limited resources. 22 | make all debug=1 23 | make build_unit # reuses some debug binaries 24 | make all # optimized/non-debug binaries 25 | 26 | # This will get scooped up and become part of the artifact archive. 27 | # Identify where the binary came from to benefit downstream consumers. 28 | cat | tee bin/aardvark-dns.info << EOF 29 | repo: $CIRRUS_REPO_CLONE_URL 30 | branch: $CIRRUS_BASE_BRANCH 31 | title: $CIRRUS_CHANGE_TITLE 32 | commit: $CIRRUS_CHANGE_IN_REPO 33 | build: https://cirrus-ci.com/build/$CIRRUS_BUILD_ID 34 | task: https://cirrus-ci.com/task/$CIRRUS_TASK_ID 35 | EOF 36 | } 37 | 38 | _run_build_aarch64() { 39 | _run_build 40 | } 41 | 42 | _run_validate() { 43 | make validate 44 | } 45 | 46 | _run_validate_aarch64() { 47 | _run_validate 48 | } 49 | 50 | _run_unit() { 51 | make unit 52 | } 53 | 54 | _run_unit_aarch64() { 55 | _run_unit 56 | } 57 | 58 | _run_integration() { 59 | make integration 60 | } 61 | 62 | _run_integration_aarch64() { 63 | make # FIXME: (@lsm5) investigate why cached binary isn't being reused 64 | _run_integration 65 | } 66 | 67 | show_env_vars 68 | 69 | msg "************************************************************" 70 | msg "Toolchain details" 71 | msg "************************************************************" 72 | rustc --version 73 | cargo --version 74 | 75 | msg "************************************************************" 76 | msg "Runner executing '$1' on $OS_REL_VER" 77 | msg "************************************************************" 78 | 79 | ((${SETUP_ENVIRONMENT:-0})) || \ 80 | die "Expecting setup.sh to have completed successfully" 81 | 82 | cd "${CIRRUS_WORKING_DIR}/" 83 | 84 | handler="_run_${1:-noarg}" 85 | 86 | if [ "$(type -t $handler)" != "function" ]; then 87 | die "Unknown/Unsupported runner.sh argument '$1'" 88 | fi 89 | 90 | $handler 91 | -------------------------------------------------------------------------------- /DISTRO_PACKAGE.md: -------------------------------------------------------------------------------- 1 | # Aardvark-dns: Authoritative DNS server for A/AAAA container records 2 | 3 | This document is currently written with Fedora as a reference. As Aardvark-dns 4 | gets shipped in other distros, this should become a distro-agnostic 5 | document. 6 | 7 | ## Fedora Users 8 | Aardvark-dns is available as an official Fedora package on Fedora 35 and newer versions 9 | and is only meant to be used with Podman v4 and newer releases. On Fedora 36 10 | and newer, fresh installations of the podman package will automatically install 11 | Aardvark-dns along with Netavark. If Aardvark-dns isn't present on your system, 12 | install it using: 13 | 14 | ```console 15 | $ sudo dnf install aardvark-dns 16 | ``` 17 | 18 | **NOTE:** Fedora 35 users will not be able to install Podman v4 using the default yum 19 | repositories. Please consult the Podman packaging docs for instructions on how 20 | to fetch Podman v4.0 on Fedora 35. 21 | 22 | If you would like to test the latest unreleased upstream code, try the 23 | podman-next COPR: 24 | 25 | ```console 26 | $ sudo dnf copr enable rhcontainerbot/podman-next 27 | 28 | $ sudo dnf install aardvark-dns 29 | ``` 30 | 31 | **CAUTION:** The podman-next COPR provides the latest unreleased sources of Podman, 32 | Aardvark-dns and Aardvark-dns as rpms which would override the versions provided by 33 | the official packages. 34 | 35 | ## Distro Packagers 36 | 37 | The Fedora packaging sources for Aardvark-dns are available at the [Aardvark-dns 38 | dist-git](https://src.fedoraproject.org/rpms/aardvark-dns). 39 | 40 | The Fedora package builds Aardvark-dns using a compressed tarball of the vendored 41 | libraries that is attached to each upstream release. 42 | You can download them with the following: 43 | 44 | `https://github.com/containers/netavark/releases/download/v{version}/aardvark-dns-v{version}.tar.gz` 45 | 46 | And then create a cargo config file to point it to the vendor dir: 47 | ``` 48 | tar xvf %{SOURCE} 49 | mkdir -p .cargo 50 | cat >.cargo/config << EOF 51 | [source.crates-io] 52 | replace-with = "vendored-sources" 53 | 54 | [source.vendored-sources] 55 | directory = "vendor" 56 | EOF 57 | ``` 58 | 59 | The `aardvark-dns` binary is installed to `/usr/libexec/podman/aardvark-dns`. 60 | 61 | ## Dependency of netavark package 62 | The netavark package has a `Recommends` on the `aardvark-dns` package. The 63 | aardvark-dns package will be installed by default with netavark, but Netavark 64 | and Podman will be functional without it. 65 | 66 | ## Listing bundled dependencies 67 | If you need to list the bundled dependencies in your packaging sources, you can 68 | run the `cargo tree` command in the upstream source. 69 | For example, Fedora's packaging source uses: 70 | 71 | ``` 72 | $ cargo tree --prefix none | awk '{print "Provides: bundled(crate("$1")) = "$2}' | sort | uniq 73 | ``` 74 | -------------------------------------------------------------------------------- /hack/get_ci_vm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # For help and usage information, simply execute the script w/o any arguments. 5 | # 6 | # This script is intended to be run by Red Hat podman developers who need 7 | # to debug problems specifically related to Cirrus-CI automated testing. 8 | # It requires that you have been granted prior access to create VMs in 9 | # google-cloud. For non-Red Hat contributors, VMs are available as-needed, 10 | # with supervision upon request. 11 | 12 | set -e 13 | 14 | SCRIPT_FILEPATH=$(realpath "${BASH_SOURCE[0]}") 15 | SCRIPT_DIRPATH=$(dirname "$SCRIPT_FILEPATH") 16 | REPO_DIRPATH=$(realpath "$SCRIPT_DIRPATH/../") 17 | 18 | # Help detect what get_ci_vm container called this script 19 | GET_CI_VM="${GET_CI_VM:-0}" 20 | in_get_ci_vm() { 21 | if ((GET_CI_VM==0)); then 22 | echo "Error: $1 is not intended for use in this context" 23 | exit 2 24 | fi 25 | } 26 | 27 | # get_ci_vm APIv1 container entrypoint calls into this script 28 | # to obtain required repo. specific configuration options. 29 | if [[ "$1" == "--config" ]]; then 30 | in_get_ci_vm "$1" # handles GET_CI_VM==0 case 31 | case "$GET_CI_VM" in 32 | 1) 33 | cat < /dev/stderr 60 | source ./contrib/cirrus/lib.sh 61 | echo "+ Running environment setup" > /dev/stderr 62 | ./contrib/cirrus/setup.sh "$CIRRUS_TASK_NAME" 63 | else 64 | # Pass this repo and CLI args into container for VM creation/management 65 | mkdir -p $HOME/.config/gcloud/ssh 66 | mkdir -p $HOME/.aws 67 | podman run -it --rm \ 68 | --tz=local \ 69 | -e NAME="$USER" \ 70 | -e SRCDIR=/src \ 71 | -e GCLOUD_ZONE="$GCLOUD_ZONE" \ 72 | -e A_DEBUG="${A_DEBUG:-0}" \ 73 | -v $REPO_DIRPATH:/src:O \ 74 | -v $HOME/.config/gcloud:/root/.config/gcloud:z \ 75 | -v $HOME/.config/gcloud/ssh:/root/.ssh:z \ 76 | -v $HOME/.aws:/root/.aws:z \ 77 | quay.io/libpod/get_ci_vm:latest "$@" 78 | fi 79 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::str::FromStr; 3 | 4 | use clap::{Parser, Subcommand}; 5 | 6 | use aardvark_dns::commands::{run, version}; 7 | use log::Level; 8 | use syslog::{BasicLogger, Facility, Formatter3164}; 9 | 10 | #[derive(Parser, Debug)] 11 | #[clap(version = env!("CARGO_PKG_VERSION"))] 12 | struct Opts { 13 | /// Path to configuration directory 14 | #[clap(short, long)] 15 | config: Option, 16 | /// Host port for aardvark servers, defaults to 5533 17 | #[clap(short, long)] 18 | port: Option, 19 | /// Filters search domain for backward compatiblity with dnsname/dnsmasq 20 | #[clap(short, long)] 21 | filter_search_domain: Option, 22 | /// Aardvark-dns trig command 23 | #[clap(subcommand)] 24 | subcmd: SubCommand, 25 | } 26 | 27 | #[derive(Subcommand, Debug)] 28 | enum SubCommand { 29 | /// Runs the aardvark dns server with the specified configuration directory. 30 | Run(run::Run), 31 | /// Display info about aardvark. 32 | Version(version::Version), 33 | } 34 | 35 | fn main() { 36 | let formatter = Formatter3164 { 37 | facility: Facility::LOG_USER, 38 | hostname: None, 39 | process: "aardvark-dns".into(), 40 | pid: 0, 41 | }; 42 | 43 | let log_level = match env::var("RUST_LOG") { 44 | Ok(val) => match Level::from_str(&val) { 45 | Ok(level) => level, 46 | Err(e) => { 47 | eprintln!("failed to parse RUST_LOG level: {e}"); 48 | Level::Info 49 | } 50 | }, 51 | // if env is not set default to info 52 | Err(_) => Level::Info, 53 | }; 54 | 55 | // On error do nothing, running on system without syslog is fine and we should not clutter 56 | // logs with meaningless errors, https://github.com/containers/podman/issues/19809. 57 | if let Ok(logger) = syslog::unix(formatter) { 58 | if let Err(e) = log::set_boxed_logger(Box::new(BasicLogger::new(logger))) 59 | .map(|()| log::set_max_level(log_level.to_level_filter())) 60 | { 61 | eprintln!("failed to initialize syslog logger: {e}") 62 | }; 63 | } 64 | 65 | let opts = Opts::parse(); 66 | 67 | let dir = opts.config.unwrap_or_else(|| String::from("/dev/stdin")); 68 | let port = opts.port.unwrap_or(5533_u16); 69 | let filter_search_domain = opts 70 | .filter_search_domain 71 | .unwrap_or_else(|| String::from(".dns.podman")); 72 | let result = match opts.subcmd { 73 | SubCommand::Run(run) => run.exec(dir, port, filter_search_domain), 74 | SubCommand::Version(version) => { 75 | version.exec(); 76 | Ok(()) 77 | } 78 | }; 79 | 80 | match result { 81 | Ok(_) => {} 82 | Err(err) => { 83 | eprintln!("{err}"); 84 | std::process::exit(1); 85 | } 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod test; 91 | -------------------------------------------------------------------------------- /src/commands/run.rs: -------------------------------------------------------------------------------- 1 | //! Runs the aardvark dns server with provided config 2 | use crate::error::{AardvarkError, AardvarkResult}; 3 | use crate::server::serve; 4 | use clap::Parser; 5 | use nix::unistd; 6 | use nix::unistd::{fork, ForkResult}; 7 | 8 | #[derive(Parser, Debug)] 9 | pub struct Run {} 10 | 11 | impl Run { 12 | /// The run command runs the aardvark-dns server with the given configuration. 13 | pub fn new() -> Self { 14 | Self {} 15 | } 16 | 17 | pub fn exec( 18 | &self, 19 | input_dir: String, 20 | port: u16, 21 | filter_search_domain: String, 22 | ) -> AardvarkResult<()> { 23 | // create a temporary path for unix socket 24 | // so parent can communicate with child and 25 | // only exit when child is ready to serve. 26 | let (ready_pipe_read, ready_pipe_write) = nix::unistd::pipe()?; 27 | 28 | // fork and verify if server is running 29 | // and exit parent 30 | // setsid() ensures that there is no controlling terminal on the child process 31 | match unsafe { fork() } { 32 | Ok(ForkResult::Parent { child, .. }) => { 33 | log::debug!("starting aardvark on a child with pid {child}"); 34 | // close write here to make sure the read does not hang when 35 | // child never sends message because it exited to early... 36 | drop(ready_pipe_write); 37 | // verify aardvark here and block till will start 38 | let i = unistd::read(&ready_pipe_read, &mut [0_u8; 1])?; 39 | drop(ready_pipe_read); 40 | if i == 0 { 41 | // we did not get any message -> child exited with error 42 | Err(AardvarkError::msg("Error from child process")) 43 | } else { 44 | Ok(()) 45 | } 46 | } 47 | Ok(ForkResult::Child) => { 48 | drop(ready_pipe_read); 49 | // create aardvark pid and then notify parent 50 | if let Err(er) = serve::create_pid(&input_dir) { 51 | return Err(AardvarkError::msg(format!( 52 | "Error creating aardvark pid {er}" 53 | ))); 54 | } 55 | 56 | if let Err(er) = 57 | serve::serve(&input_dir, port, &filter_search_domain, ready_pipe_write) 58 | { 59 | return Err(AardvarkError::msg(format!("Error starting server {er}"))); 60 | } 61 | Ok(()) 62 | } 63 | Err(err) => { 64 | log::debug!("fork failed with error {err}"); 65 | Err(AardvarkError::msg(format!("fork failed with error: {err}"))) 66 | } 67 | } 68 | } 69 | } 70 | 71 | impl Default for Run { 72 | fn default() -> Self { 73 | Self::new() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | pub type AardvarkResult = Result; 4 | 5 | #[derive(Debug)] 6 | pub enum AardvarkError { 7 | Message(String), 8 | IOError(std::io::Error), 9 | Chain(String, Box), 10 | List(AardvarkErrorList), 11 | AddrParseError(std::net::AddrParseError), 12 | } 13 | 14 | impl AardvarkError { 15 | pub fn msg(msg: S) -> Self 16 | where 17 | S: Into, 18 | { 19 | Self::Message(msg.into()) 20 | } 21 | 22 | pub fn wrap(msg: S, chained: Self) -> Self 23 | where 24 | S: Into, 25 | { 26 | Self::Chain(msg.into(), Box::new(chained)) 27 | } 28 | } 29 | 30 | pub trait AardvarkWrap { 31 | /// Wrap the error value with additional context. 32 | fn wrap(self, context: C) -> AardvarkResult 33 | where 34 | C: Into, 35 | E: Into; 36 | } 37 | 38 | impl AardvarkWrap for Result 39 | where 40 | E: Into, 41 | { 42 | fn wrap(self, msg: C) -> AardvarkResult 43 | where 44 | C: Into, 45 | E: Into, 46 | { 47 | // Not using map_err to save 2 useless frames off the captured backtrace 48 | // in ext_context. 49 | match self { 50 | Ok(ok) => Ok(ok), 51 | Err(error) => Err(AardvarkError::wrap(msg, error.into())), 52 | } 53 | } 54 | } 55 | 56 | impl fmt::Display for AardvarkError { 57 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 58 | match self { 59 | Self::Message(s) => write!(f, "{s}"), 60 | Self::Chain(s, e) => write!(f, "{s}: {e}"), 61 | Self::IOError(e) => write!(f, "IO error: {e}"), 62 | Self::AddrParseError(e) => write!(f, "parse address: {e}"), 63 | Self::List(list) => { 64 | // some extra code to only add \n when it contains multiple errors 65 | let mut iter = list.0.iter(); 66 | if let Some(first) = iter.next() { 67 | write!(f, "{first}")?; 68 | } 69 | for err in iter { 70 | write!(f, "\n{err}")?; 71 | } 72 | Ok(()) 73 | } 74 | } 75 | } 76 | } 77 | 78 | impl From for AardvarkError { 79 | fn from(err: std::io::Error) -> Self { 80 | Self::IOError(err) 81 | } 82 | } 83 | 84 | impl From for AardvarkError { 85 | fn from(err: nix::Error) -> Self { 86 | Self::IOError(err.into()) 87 | } 88 | } 89 | 90 | impl From for AardvarkError { 91 | fn from(err: std::net::AddrParseError) -> Self { 92 | Self::AddrParseError(err) 93 | } 94 | } 95 | 96 | #[derive(Debug)] 97 | pub struct AardvarkErrorList(Vec); 98 | 99 | impl AardvarkErrorList { 100 | pub fn new() -> Self { 101 | Self(vec![]) 102 | } 103 | 104 | pub fn push(&mut self, err: AardvarkError) { 105 | self.0.push(err) 106 | } 107 | 108 | pub fn is_empty(&self) -> bool { 109 | self.0.is_empty() 110 | } 111 | } 112 | 113 | // we do not need it but clippy wants it 114 | impl Default for AardvarkErrorList { 115 | fn default() -> Self { 116 | Self::new() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /test/500-reverse-lookups.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | 8 | @test "check reverse lookups" { 9 | # container a1 10 | subnet_a=$(random_subnet 5) 11 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" aliases='"a1", "1a"' 12 | a1_config="$config" 13 | a1_ip=$(echo "$a1_config" | jq -r .networks.podman1.static_ips[0]) 14 | gw=$(echo "$a1_config" | jq -r .network_info.podman1.subnets[0].gateway) 15 | create_container "$a1_config" 16 | a1_pid=$CONTAINER_NS_PID 17 | 18 | # container a2 19 | create_config network_name="podman1" container_id=$(random_string 64) container_name="atwo" subnet="$subnet_a" aliases='"a2", "2a"' 20 | a2_config="$config" 21 | a2_ip=$(echo "$a2_config" | jq -r .networks.podman1.static_ips[0]) 22 | create_container "$a2_config" 23 | a2_pid="$CONTAINER_NS_PID" 24 | 25 | echo "a1 config:\n${a1_config}\n" 26 | echo "a2 config:\n${a2_config}\n" 27 | 28 | # Resolve IPs to container names 29 | dig_reverse "$a1_pid" "$a2_ip" "$gw" 30 | echo -e "Output:\n${output}\n" 31 | a2_expected_name=$(echo $a2_ip | awk -F. '{printf "%d.%d.%d.%d.in-addr.arpa.", $4, $3, $2, $1}') 32 | assert "$output" =~ "$a2_expected_name[[:space:]]*0[[:space:]]*IN[[:space:]]*PTR[[:space:]]*atwo\." 33 | assert "$output" =~ "$a2_expected_name[[:space:]]*0[[:space:]]*IN[[:space:]]*PTR[[:space:]]*a2\." 34 | assert "$output" =~ "$a2_expected_name[[:space:]]*0[[:space:]]*IN[[:space:]]*PTR[[:space:]]*2a\." 35 | dig_reverse "$a2_pid" "$a1_ip" "$gw" 36 | echo -e "Output:\n${output}\n" 37 | a1_expected_name=$(echo $a1_ip | awk -F. '{printf "%d.%d.%d.%d.in-addr.arpa.", $4, $3, $2, $1}') 38 | assert "$output" =~ "$a1_expected_name[[:space:]]*0[[:space:]]*IN[[:space:]]*PTR[[:space:]]*aone\." 39 | assert "$output" =~ "$a1_expected_name[[:space:]]*0[[:space:]]*IN[[:space:]]*PTR[[:space:]]*a1\." 40 | assert "$output" =~ "$a1_expected_name[[:space:]]*0[[:space:]]*IN[[:space:]]*PTR[[:space:]]*1a\." 41 | } 42 | 43 | @test "check reverse lookups on ipaddress v6" { 44 | # container a1 45 | subnet_a=$(random_subnet 6) 46 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" aliases='"a1", "1a"' 47 | a1_config="$config" 48 | a1_ip=$(echo "$a1_config" | jq -r .networks.podman1.static_ips[0]) 49 | gw=$(echo "$a1_config" | jq -r .network_info.podman1.subnets[0].gateway) 50 | create_container "$a1_config" 51 | a1_pid=$CONTAINER_NS_PID 52 | 53 | # container a2 54 | create_config network_name="podman1" container_id=$(random_string 64) container_name="atwo" subnet="$subnet_a" aliases='"a2", "2a"' 55 | a2_config="$config" 56 | a2_ip=$(echo "$a2_config" | jq -r .networks.podman1.static_ips[0]) 57 | create_container "$a2_config" 58 | a2_pid="$CONTAINER_NS_PID" 59 | 60 | echo "$a1_config" 61 | echo "$a2_config" 62 | 63 | # Resolve IPs to container names 64 | # It is much harder to construct the arpa address in ipv6 so we just check that we are in the fd::/8 range 65 | dig_reverse "$a1_pid" "$a2_ip" "$gw" 66 | assert "$output" =~ '([0-9a-f]\.){30}d\.f\.ip6\.arpa\.[ ].*[ ]atwo\.' 67 | assert "$output" =~ '([0-9a-f]\.){30}d\.f\.ip6\.arpa\.[ ].*[ ]a2\.' 68 | assert "$output" =~ '([0-9a-f]\.){30}d\.f\.ip6\.arpa\.[ ].*[ ]2a\.' 69 | dig_reverse "$a2_pid" "$a1_ip" "$gw" 70 | assert "$output" =~ '([0-9a-f]\.){30}d\.f\.ip6\.arpa\.[ ].*[ ]aone\.' 71 | assert "$output" =~ '([0-9a-f]\.){30}d\.f\.ip6\.arpa\.[ ].*[ ]a1\.' 72 | assert "$output" =~ '([0-9a-f]\.){30}d\.f\.ip6\.arpa\.[ ].*[ ]1a\.' 73 | } 74 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v1.17.0 4 | 5 | * Aardvark-dns now updates the upstream nameservers from /etc/resolv.conf when the file content changes using inotify. This means a container restart is no longer required to re-read resolv.conf. 6 | * Dependency updates. 7 | 8 | ## v1.16.0 9 | 10 | * Allow more than one DNS message per tcp socket. ([#605](https://github.com/containers/aardvark-dns/issues/605)) 11 | * Dependency updates. 12 | 13 | ## v1.15.0 14 | 15 | * Dependency updates. 16 | 17 | ## v1.14.0 18 | 19 | * Dependency updates. 20 | 21 | ## v1.13.1 22 | 23 | * Fix parsing of ipv6 link local addresses in resolv.conf ([#535](https://github.com/containers/aardvark-dns/issues/535)) 24 | 25 | ## v1.13.0 26 | 27 | * Set TTL to 0 for container names 28 | * Allow forwarding of names with no ndots 29 | * DNS: limit to 3 resolvers and use better timeout for them 30 | * Ignore unknown resolv.conf options 31 | 32 | ## v1.12.2 33 | 34 | * This releases fixes a security issue (CVE-2024-8418) where tcp connections where not handled correctly which allowed a container to block dns queries for other clients on the same network #500. Versions before v1.12.0 are unaffected as they do not have tcp support. 35 | 36 | ## v1.12.1 37 | 38 | * Fixed problem with categories in Cargo.toml that prevented us from publishing v1.12.0 39 | 40 | ## v1.12.0 41 | 42 | * Dependency updates 43 | * Improve all around error handling and logging 44 | * Added TCP/IP support 45 | * Update upsteam resolvers on each refresh 46 | 47 | ## v1.11.0 48 | * Do not allow "internal" networks to access DNS 49 | * On SIGHUP, stop AV threads no longer needed and reload in memory those that are 50 | * updated dependencies 51 | 52 | ## v1.10.0 53 | * removed unused kill switch 54 | * updated dependencies 55 | 56 | ## v1.9.0 57 | * update trust-dns to hickory 58 | * never report an error when the syslog init fails 59 | * dependency updates 60 | 61 | ## v1.8.0 62 | * dependency updates 63 | 64 | ## v1.7.0 65 | * dependency updates 66 | 67 | ## v1.6.0 68 | * dependency updates 69 | * lower the TTL to 60s for container names 70 | 71 | ## v1.5.0 72 | * dependency updates 73 | * code of conduct added 74 | 75 | ## v1.4.0 76 | * Add support for network scoped dns servers; declare DNS at a network level 77 | 78 | ## v1.3.0 79 | * allow one or more dns servers in the aardvark config 80 | 81 | ## v1.2.0 82 | * coredns: do not combine results of A and AAAA records 83 | * run,serve: create aardvark pid in child before we notify parent process 84 | * coredns: response message set recursion available if RD is true 85 | * document configuration format 86 | 87 | ## v1.1.0 88 | * Changed Aardvark to fork on startup to daemonize, as opposed to have this done by callers. This avoids race conditions around startup. 89 | * Name resolution is now case-insensitive. 90 | 91 | ## v1.0.3 92 | * Updated dependancy libraries 93 | * Reduction in CPU use 94 | * Fixed bug with duplicate network names 95 | 96 | ## v1.0.2 97 | * Updated dependency libraries 98 | * Removed vergen dependency 99 | 100 | ## v1.0.1 101 | - Remove vendor directory from upstream github repository 102 | - Vendored libraries updates 103 | 104 | ## v1.0.0 105 | - First release of aardvark-dns. 106 | 107 | ## v1.0.0-RC2 108 | - Slew of bug fixes related to reverse lookups, NXDOMAIN returns, and so on. Getting very close to first official release. 109 | 110 | ## v1.0.0-RC1 111 | - This is the first release candidate of Aardvark's initial release! All major functionality is implemented and working. 112 | -------------------------------------------------------------------------------- /rpm/aardvark-dns.spec: -------------------------------------------------------------------------------- 1 | # trust-dns-{client,server} not available 2 | # using vendored deps 3 | 4 | %global with_debug 1 5 | 6 | %if 0%{?with_debug} 7 | %global _find_debuginfo_dwz_opts %{nil} 8 | %global _dwz_low_mem_die_limit 0 9 | %else 10 | %global debug_package %{nil} 11 | %endif 12 | 13 | Name: aardvark-dns 14 | %if %{defined copr_username} 15 | Epoch: 102 16 | %else 17 | Epoch: 2 18 | %endif 19 | # DO NOT TOUCH the Version string! 20 | # The TRUE source of this specfile is: 21 | # https://github.com/containers/podman/blob/main/rpm/podman.spec 22 | # If that's what you're reading, Version must be 0, and will be updated by Packit for 23 | # copr and koji builds. 24 | # If you're reading this on dist-git, the version is automatically filled in by Packit. 25 | Version: 0 26 | # The `AND` needs to be uppercase in the License for SPDX compatibility 27 | License: Apache-2.0 AND MIT AND Zlib 28 | Release: %autorelease 29 | %if %{defined golang_arches_future} 30 | ExclusiveArch: %{golang_arches_future} 31 | %else 32 | ExclusiveArch: aarch64 ppc64le s390x x86_64 33 | %endif 34 | Summary: Authoritative DNS server for A/AAAA container records 35 | URL: https://github.com/containers/%{name} 36 | # Tarballs fetched from upstream's release page 37 | Source0: %{url}/archive/v%{version}.tar.gz 38 | Source1: %{url}/releases/download/v%{version}/%{name}-v%{version}-vendor.tar.gz 39 | BuildRequires: cargo 40 | BuildRequires: git-core 41 | BuildRequires: make 42 | %if %{defined rhel} 43 | # rust-toolset requires the `local` repo enabled on non-koji ELN build environments 44 | BuildRequires: rust-toolset 45 | %else 46 | BuildRequires: rust-packaging 47 | BuildRequires: rust-srpm-macros 48 | %endif 49 | 50 | %description 51 | %{summary} 52 | 53 | Forwards other request to configured resolvers. 54 | Read more about configuration in `src/backend/mod.rs`. 55 | 56 | %package tests 57 | Summary: Tests for %{name} 58 | 59 | Requires: %{name} = %{epoch}:%{version}-%{release} 60 | Requires: bats 61 | Requires: bind-utils 62 | Requires: jq 63 | Requires: netavark 64 | Requires: socat 65 | Requires: dnsmasq 66 | 67 | %description tests 68 | %{summary} 69 | 70 | This package contains system tests for %{name} and is only intended to be used 71 | for gating tests. 72 | 73 | %prep 74 | %autosetup -Sgit %{name}-%{version} 75 | # Following steps are only required on environments like koji which have no 76 | # network access and thus depend on the vendored tarball. Copr pulls 77 | # dependencies directly from the network. 78 | %if !%{defined copr_username} 79 | tar fx %{SOURCE1} 80 | %if 0%{?fedora} || 0%{?rhel} >= 10 81 | %cargo_prep -v vendor 82 | %else 83 | %cargo_prep -V 1 84 | %endif 85 | %endif 86 | 87 | %build 88 | %{__make} CARGO="%{__cargo}" build 89 | %if (0%{?fedora} || 0%{?rhel} >= 10) && !%{defined copr_username} 90 | %cargo_license_summary 91 | %{cargo_license} > LICENSE.dependencies 92 | %cargo_vendor_manifest 93 | %endif 94 | 95 | %install 96 | %{__make} DESTDIR=%{buildroot} PREFIX=%{_prefix} install 97 | 98 | %{__install} -d -p %{buildroot}%{_datadir}/%{name}/test 99 | %{__cp} -rp test/* %{buildroot}%{_datadir}/%{name}/test/ 100 | %{__rm} -rf %{buildroot}%{_datadir}/%{name}/test/tmt/ 101 | 102 | # Add empty check section to silence rpmlint warning. 103 | # No tests meant to be run here. 104 | %check 105 | 106 | %files 107 | %license LICENSE 108 | %if (0%{?fedora} || 0%{?rhel} >= 10) && !%{defined copr_username} 109 | %license LICENSE.dependencies 110 | %license cargo-vendor.txt 111 | %endif 112 | %dir %{_libexecdir}/podman 113 | %{_libexecdir}/podman/%{name} 114 | 115 | %files tests 116 | %{_datadir}/%{name}/test 117 | 118 | %changelog 119 | %autochangelog 120 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This Makefile is intended for developer convenience. For the most part 2 | # all the targets here simply wrap calls to the `cargo` tool. Therefore, 3 | # most targets must be marked 'PHONY' to prevent `make` getting in the way 4 | # 5 | #prog :=xnixperms 6 | 7 | DESTDIR ?= 8 | PREFIX ?= /usr/local 9 | LIBEXECDIR ?= ${PREFIX}/libexec 10 | LIBEXECPODMAN ?= ${LIBEXECDIR}/podman 11 | 12 | SELINUXOPT ?= $(shell test -x /usr/sbin/selinuxenabled && selinuxenabled && echo -Z) 13 | # Get crate version by parsing the line that starts with version. 14 | CRATE_VERSION ?= $(shell grep ^version Cargo.toml | awk '{print $$3}') 15 | GIT_TAG ?= $(shell git describe --tags) 16 | 17 | # Set this to any non-empty string to enable unoptimized 18 | # build w/ debugging features. 19 | debug ?= 20 | 21 | # Set path to cargo executable, when running under CI make sure to add --locked so Cargo.lock is not modified 22 | CARGO ?= cargo $(if $(CI),--locked,) 23 | 24 | # All complication artifacts, including dependencies and intermediates 25 | # will be stored here, for all architectures. Use a non-default name 26 | # since the (default) 'target' is used/referenced ambiguously in many 27 | # places in the tool-chain (including 'make' itself). 28 | CARGO_TARGET_DIR ?= targets 29 | export CARGO_TARGET_DIR # 'cargo' is sensitive to this env. var. value. 30 | 31 | ifdef debug 32 | $(info debug is $(debug)) 33 | # These affect both $(CARGO_TARGET_DIR) layout and contents 34 | # Ref: https://doc.rust-lang.org/cargo/guide/build-cache.html 35 | release := 36 | profile :=debug 37 | else 38 | release :=--release 39 | profile :=release 40 | endif 41 | 42 | .PHONY: all 43 | all: build 44 | 45 | bin: 46 | mkdir -p $@ 47 | 48 | $(CARGO_TARGET_DIR): 49 | mkdir -p $@ 50 | 51 | .PHONY: build 52 | build: bin $(CARGO_TARGET_DIR) 53 | $(CARGO) build $(release) 54 | cp $(CARGO_TARGET_DIR)/$(profile)/aardvark-dns bin/aardvark-dns$(if $(debug),.debug,) 55 | 56 | .PHONY: crate-publish 57 | crate-publish: 58 | @if [ "v$(CRATE_VERSION)" != "$(GIT_TAG)" ]; then\ 59 | echo "Git tag is not equivalent to the version set in Cargo.toml. Please checkout the correct tag";\ 60 | exit 1;\ 61 | fi 62 | @echo "It is expected that you have already done 'cargo login' before running this command. If not command may fail later" 63 | $(CARGO) publish --dry-run 64 | $(CARGO) publish 65 | 66 | .PHONY: clean 67 | clean: 68 | rm -rf bin 69 | rm -rf vendor-tarball 70 | if [ "$(CARGO_TARGET_DIR)" = "targets" ]; then rm -rf targets; fi 71 | $(MAKE) -C docs clean 72 | 73 | #.PHONY: docs 74 | #docs: ## build the docs on the host 75 | # $(MAKE) -C docs 76 | 77 | .PHONY: install 78 | install: 79 | install ${SELINUXOPT} -D -m0755 bin/aardvark-dns $(DESTDIR)/$(LIBEXECPODMAN)/aardvark-dns 80 | #$(MAKE) -C docs install 81 | 82 | .PHONY: uninstall 83 | uninstall: 84 | rm -f $(DESTDIR)/$(LIBEXECPODMAN)/aardvark-dns 85 | rm -f $(PREFIX)/share/man/man1/aardvark-dns*.1 86 | 87 | #.PHONY: test 88 | test: unit integration 89 | 90 | # Used by CI to compile the unit tests but not run them 91 | .PHONY: build_unit 92 | build_unit: $(CARGO_TARGET_DIR) 93 | $(CARGO) test --no-run 94 | 95 | #.PHONY: unit 96 | unit: $(CARGO_TARGET_DIR) 97 | $(CARGO) test 98 | 99 | #.PHONY: code_coverage 100 | # Can be used by CI and users to generate code coverage report based on aardvark unit tests 101 | code_coverage: $(CARGO_TARGET_DIR) 102 | # Downloads tarpaulin only if same version is not present on local 103 | $(CARGO) install cargo-tarpaulin 104 | $(CARGO) tarpaulin -v 105 | 106 | #.PHONY: integration 107 | integration: $(CARGO_TARGET_DIR) 108 | # needs to be run as root or with podman unshare --rootless-netns 109 | bats test/ 110 | 111 | .PHONY: mock-rpm 112 | mock-rpm: 113 | rpkg local 114 | 115 | .PHONY: validate 116 | validate: $(CARGO_TARGET_DIR) 117 | $(CARGO) fmt --all -- --check 118 | $(CARGO) clippy -p aardvark-dns -- -D warnings 119 | 120 | .PHONY: vendor-tarball 121 | vendor-tarball: build install.cargo-vendor-filterer 122 | VERSION=$(shell bin/aardvark-dns --version | cut -f2 -d" ") && \ 123 | $(CARGO) vendor-filterer --format=tar.gz --prefix vendor/ && \ 124 | mkdir -p vendor-tarball && \ 125 | mv vendor.tar.gz vendor-tarball/aardvark-dns-v$$VERSION-vendor.tar.gz && \ 126 | gzip -c bin/aardvark-dns > vendor-tarball/aardvark-dns.gz && \ 127 | cd vendor-tarball && \ 128 | sha256sum aardvark-dns.gz aardvark-dns-v$$VERSION-vendor.tar.gz > sha256sum 129 | 130 | .PHONY: install.cargo-vendor-filterer 131 | install.cargo-vendor-filterer: 132 | $(CARGO) install cargo-vendor-filterer 133 | 134 | .PHONY: help 135 | help: 136 | @echo "usage: make $(prog) [debug=1]" 137 | -------------------------------------------------------------------------------- /.packit.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # See the documentation for more information: 3 | # https://packit.dev/docs/configuration/ 4 | 5 | downstream_package_name: aardvark-dns 6 | upstream_tag_template: v{version} 7 | 8 | # These files get synced from upstream to downstream (Fedora / CentOS Stream) on every 9 | # propose-downstream job. This is done so tests maintained upstream can be run 10 | # downstream in Zuul CI and Bodhi. 11 | # Ref: https://packit.dev/docs/configuration#files_to_sync 12 | files_to_sync: 13 | - src: rpm/gating.yaml 14 | dest: gating.yaml 15 | delete: true 16 | - src: plans/ 17 | dest: plans/ 18 | delete: true 19 | mkpath: true 20 | - src: test/tmt 21 | dest: test/tmt 22 | delete: true 23 | mkpath: true 24 | - src: .fmf/ 25 | dest: .fmf/ 26 | delete: true 27 | - .packit.yaml 28 | 29 | packages: 30 | aardvark-dns-fedora: 31 | pkg_tool: fedpkg 32 | specfile_path: rpm/aardvark-dns.spec 33 | aardvark-dns-centos: 34 | pkg_tool: centpkg 35 | specfile_path: rpm/aardvark-dns.spec 36 | aardvark-dns-eln: 37 | specfile_path: rpm/aardvark-dns.spec 38 | 39 | srpm_build_deps: 40 | - cargo 41 | - git-archive-all 42 | - make 43 | - openssl-devel 44 | 45 | # https://packit.dev/docs/configuration/actions 46 | # fix-spec-file only operates on copr builds and doesn't affect official distro builds 47 | actions: 48 | fix-spec-file: "bash .packit-copr-rpm.sh" 49 | 50 | jobs: 51 | - job: copr_build 52 | trigger: pull_request 53 | packages: [aardvark-dns-fedora] 54 | notifications: &copr_build_failure_notification 55 | failure_comment: 56 | message: "Ephemeral COPR build failed. @containers/packit-build please check." 57 | targets: &fedora_copr_targets 58 | - fedora-all-x86_64 59 | - fedora-all-aarch64 60 | enable_net: true 61 | osh_diff_scan_after_copr_build: false 62 | 63 | - job: copr_build 64 | trigger: pull_request 65 | packages: [aardvark-dns-eln] 66 | notifications: *copr_build_failure_notification 67 | targets: 68 | fedora-eln-x86_64: 69 | additional_repos: 70 | - "https://kojipkgs.fedoraproject.org/repos/eln-build/latest/x86_64/" 71 | fedora-eln-aarch64: 72 | additional_repos: 73 | - "https://kojipkgs.fedoraproject.org/repos/eln-build/latest/aarch64/" 74 | enable_net: true 75 | 76 | - job: copr_build 77 | trigger: pull_request 78 | packages: [aardvark-dns-centos] 79 | notifications: *copr_build_failure_notification 80 | targets: ¢os_copr_targets 81 | - centos-stream-9-x86_64 82 | - centos-stream-9-aarch64 83 | - centos-stream-10-x86_64 84 | - centos-stream-10-aarch64 85 | enable_net: true 86 | 87 | # Run on commit to main branch 88 | - job: copr_build 89 | trigger: commit 90 | packages: [aardvark-dns-fedora] 91 | notifications: 92 | failure_comment: 93 | message: "podman-next COPR build failed. @containers/packit-build please check." 94 | branch: main 95 | owner: rhcontainerbot 96 | project: podman-next 97 | enable_net: true 98 | 99 | # Tests on Fedora 100 | - job: tests 101 | trigger: pull_request 102 | packages: [aardvark-dns-fedora] 103 | notifications: &test_failure_notification 104 | failure_comment: 105 | message: "Tests failed. @containers/packit-build please check." 106 | targets: *fedora_copr_targets 107 | tf_extra_params: 108 | environments: 109 | - artifacts: 110 | - type: repository-file 111 | id: https://copr.fedorainfracloud.org/coprs/rhcontainerbot/podman-next/repo/fedora-$releasever/rhcontainerbot-podman-next-fedora-$releasever.repo 112 | 113 | # Tests on CentOS Stream 114 | - job: tests 115 | trigger: pull_request 116 | packages: [aardvark-dns-centos] 117 | notifications: *test_failure_notification 118 | targets: *centos_copr_targets 119 | tf_extra_params: 120 | environments: 121 | - artifacts: 122 | - type: repository-file 123 | id: https://copr.fedorainfracloud.org/coprs/rhcontainerbot/podman-next/repo/centos-stream-$releasever/rhcontainerbot-podman-next-centos-stream-$releasever.repo 124 | 125 | # Sync to Fedora 126 | - job: propose_downstream 127 | trigger: release 128 | packages: [aardvark-dns-fedora] 129 | update_release: false 130 | dist_git_branches: &fedora_targets 131 | - fedora-all 132 | 133 | # Sync to CentOS Stream 134 | - job: propose_downstream 135 | trigger: release 136 | packages: [aardvark-dns-centos] 137 | update_release: false 138 | dist_git_branches: 139 | - c10s 140 | - c9s 141 | 142 | - job: koji_build 143 | trigger: commit 144 | packages: [aardvark-dns-fedora] 145 | sidetag_group: netavark-releases 146 | dependents: 147 | - netavark 148 | dist_git_branches: *fedora_targets 149 | -------------------------------------------------------------------------------- /test/200-two-networks.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | 8 | @test "two containers on different networks" { 9 | setup_dnsmasq 10 | 11 | # container a1 on subnet a 12 | subnet_a=$(random_subnet 5) 13 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" 14 | a1_config="$config" 15 | a1_ip=$(echo "$a1_config" | jq -r .networks.podman1.static_ips[0]) 16 | a_gw=$(echo "$a1_config" | jq -r .network_info.podman1.subnets[0].gateway) 17 | create_container "$a1_config" 18 | a1_pid="$CONTAINER_NS_PID" 19 | 20 | # container b1 on subnet b 21 | subnet_b=$(random_subnet 5) 22 | create_config network_name="podman2" container_id=$(random_string 64) container_name="bone" subnet="$subnet_b" 23 | b1_config="$config" 24 | b1_ip=$(echo "$b1_config" | jq -r .networks.podman2.static_ips[0]) 25 | b_gw=$(echo "$b1_config" | jq -r .network_info.podman2.subnets[0].gateway) 26 | create_container "$b1_config" 27 | b1_pid="$CONTAINER_NS_PID" 28 | 29 | # container a1 should not resolve b1 and we should get 30 | # a NXDOMAIN 31 | run_in_container_netns "$a1_pid" "dig" "bone" "@$a_gw" 32 | assert "$output" =~ "status: NXDOMAIN" "a1 resolves b2" 33 | 34 | # container b1 should not resolve a1 and we should get 35 | # a NXDOMAIN 36 | run_in_container_netns "$b1_pid" "dig" "aone" "@$b_gw" 37 | assert "$output" =~ "status: NXDOMAIN" "b1 resolves a1" 38 | 39 | # a1 should be able to resolve itself 40 | dig "$a1_pid" "aone" "$a_gw" 41 | assert $a1_ip 42 | # b1 should be able to resolve itself 43 | dig "$b1_pid" "bone" "$b_gw" 44 | assert $b1_ip 45 | 46 | # we should be able to resolve a from the host if we use the a gw as server 47 | run_in_host_netns dig +short "aone" "@$a_gw" 48 | assert $a1_ip 49 | # but NOT when using b as server 50 | run_in_host_netns "dig" "aone" "@$b_gw" 51 | assert "$output" =~ "status: NXDOMAIN" "b1 listener can resolve a1" 52 | 53 | # but b on network b is allowed again 54 | run_in_host_netns dig +short "bone" "@$b_gw" 55 | assert $b1_ip 56 | } 57 | 58 | @test "two subnets with isolated container and one shared" { 59 | setup_dnsmasq 60 | 61 | # container a1 on subnet a 62 | subnet_a=$(random_subnet 5) 63 | subnet_b=$(random_subnet 5) 64 | 65 | # A1 66 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" 67 | a1_config=$config 68 | a1_container_id=$(echo "$a1_config" | jq -r .container_id) 69 | a1_ip=$(echo "$a1_config" | jq -r .networks.podman1.static_ips[0]) 70 | a_gw=$(echo "$a1_config" | jq -r .network_info.podman1.subnets[0].gateway) 71 | a1_hash=$(echo "$a1_config" | jq -r .network_info.podman1.id) 72 | create_container "$a1_config" 73 | a1_pid=$CONTAINER_NS_PID 74 | 75 | # container b1 on subnet b 76 | create_config network_name="podman2" container_id=$(random_string 64) container_name="bone" subnet="$subnet_b" 77 | b1_config=$config 78 | b1_ip=$(echo "$b1_config" | jq -r .networks.podman2.static_ips[0]) 79 | b_gw=$(echo "$b1_config" | jq -r .network_info.podman2.subnets[0].gateway) 80 | b1_hash=$(echo "$b1_config" | jq -r .network_info.podman1.id) 81 | create_container "$b1_config" 82 | b1_pid=$CONTAINER_NS_PID 83 | b_subnets=$(echo $b1_config | jq -r .network_info.podman2.subnets[0]) 84 | 85 | # AB2 86 | create_config network_name="podman1" container_id=$(random_string 64) container_name="abtwo" subnet="$subnet_a" 87 | a2_config=$config 88 | a2_ip=$(echo "$a2_config" | jq -r .networks.podman1.static_ips[0]) 89 | 90 | b2_ip=$(random_ip_in_subnet "$subnet_b") 91 | create_network "podman2" "$b2_ip" "eth1" 92 | b2_network="{$new_network}" 93 | create_network_infos "podman2" "$b1_hash" "$b_subnets" 94 | b2_network_info="{$new_network_info}" 95 | ab2_config=$(jq -r ".networks += $b2_network" <<<"$a2_config") 96 | ab2_config=$(jq -r ".network_info += $b2_network_info" <<<"$ab2_config") 97 | 98 | create_container "$ab2_config" 99 | ab2_pid=$CONTAINER_NS_PID 100 | 101 | # aone should be able to resolve AB2 and NOT B1 102 | dig "$a1_pid" "abtwo" "$a_gw" 103 | assert "$a2_ip" 104 | dig "$a1_pid" "bone" "$a_gw" 105 | assert "" 106 | 107 | # bone should be able to resolve AB2 and NOT A1 108 | dig "$b1_pid" "abtwo" "$b_gw" 109 | assert "$b2_ip" 110 | dig "$b1_pid" "aone" "$b_gw" 111 | assert "" 112 | 113 | # abtwo should be able to resolve A1, B1, and AB2 on both gws 114 | dig "$ab2_pid" "aone" "$a_gw" 115 | assert "$a1_ip" 116 | dig "$ab2_pid" "bone" "$b_gw" 117 | assert "$b1_ip" 118 | # check ab2 from itself, first from the a side 119 | dig "$ab2_pid" "abtwo" "$a_gw" 120 | assert "${#lines[@]}" = 2 121 | assert "$output" =~ "$a2_ip" 122 | assert "$output" =~ "$b2_ip" 123 | 124 | # and now from the bside 125 | dig "$ab2_pid" "abtwo" "$b_gw" 126 | assert "${#lines[@]}" = 2 127 | assert "$output" =~ "$a2_ip" 128 | assert "$output" =~ "$b2_ip" 129 | } 130 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'Release version to build and upload (e.g. "v9.8.7")' 10 | required: true 11 | buildonly: 12 | description: 'Build only: Do not create release' 13 | default: "true" # 'choice' type requires string value 14 | type: choice 15 | options: 16 | - "true" # Must be quoted string, boolean value not supported. 17 | - "false" 18 | 19 | jobs: 20 | check: 21 | name: Check 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Determine Version 25 | id: getversion 26 | run: | 27 | if [[ -z "${{ inputs.version }}" ]] 28 | then 29 | VERSION=${{ github.ref_name }} 30 | else 31 | VERSION=${{ inputs.version }} 32 | fi 33 | if ! grep -Eq 'v[0-9]+(\.[0-9]+(\.[0-9]+(-.+)?)?)?$' <<<"$VERSION" 34 | then 35 | echo "Unable to parse release version '$VERSION' from github event JSON, or workflow 'version' input." 36 | exit 1 37 | fi 38 | 39 | echo "version=$VERSION" >> $GITHUB_OUTPUT 40 | echo "::notice::Building $VERSION" 41 | - name: Determine release 42 | id: buildonly 43 | run: | 44 | # The 'tag' trigger will not have a 'buildonly' input set. Handle 45 | # this case in a readable/maintainable way. 46 | if [[ -z "${{ inputs.buildonly }}" ]] 47 | then 48 | BUILDONLY=false 49 | else 50 | BUILDONLY=${{ inputs.buildonly }} 51 | fi 52 | echo "buildonly=$BUILDONLY" >> $GITHUB_OUTPUT 53 | echo "::notice::This will be build-only: $BUILDONLY" 54 | outputs: 55 | version: ${{ steps.getversion.outputs.version }} 56 | buildonly: ${{ steps.buildonly.outputs.buildonly }} 57 | 58 | build-artifacts: 59 | name: Build Artifacts 60 | runs-on: ubuntu-latest 61 | needs: check 62 | steps: 63 | - name: Checkout Version 64 | uses: actions/checkout@v6 65 | with: 66 | ref: ${{needs.check.outputs.version}} 67 | - name: Update Rust 68 | run: | 69 | rustup update stable 70 | rustc --version 71 | cargo --version 72 | - name: Build Artifacts 73 | run: | 74 | make vendor-tarball 75 | - name: Upload to Actions as artifact 76 | uses: actions/upload-artifact@v5 77 | with: 78 | name: release-artifacts 79 | path: vendor-tarball 80 | 81 | release: 82 | name: Create Release 83 | runs-on: ubuntu-latest 84 | if: needs.check.outputs.buildonly == 'false' 85 | needs: [check, build-artifacts] 86 | permissions: 87 | contents: write 88 | env: 89 | VERSION: ${{needs.check.outputs.version}} 90 | steps: 91 | - name: Checkout Version 92 | uses: actions/checkout@v6 93 | with: 94 | ref: ${{needs.check.outputs.version}} 95 | - name: Get release notes 96 | run: | 97 | ver="${VERSION%-rc*}" 98 | releasenotes="$VERSION-release-notes.md" 99 | awk -v ver=$ver '/^## / { if (p) { exit }; if ($2 == ver) { p=1; next } } p' RELEASE_NOTES.md > $releasenotes 100 | if [[ -z $(grep '[^[:space:]]' $releasenotes) ]]; then 101 | if [[ $VERSION != *-rc* ]]; then 102 | echo "::notice:: Release does not have release notes" 103 | exit 1 104 | else 105 | echo "This is a release candidate of aardvark-dns $ver." > $releasenotes 106 | fi 107 | fi 108 | - name: Display release notes 109 | run: cat $VERSION-release-notes.md 110 | - name: Download all artifacts 111 | uses: actions/download-artifact@v6 112 | with: 113 | path: release-artifacts 114 | - name: Create release 115 | env: 116 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 117 | run: | 118 | title=$VERSION 119 | if [[ $VERSION == *-rc* ]]; then 120 | RC="--prerelease" 121 | title="${title/rc/"RC"}" 122 | else 123 | # check if this version should not be marked latest 124 | prevrelease=$(curl --retry 3 --silent -m 10 --connect-timeout 5 "https://api.github.com/repos/${{ github.repository }}/releases/latest") 125 | prevvers=$(echo "$prevrelease" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed -e "s/^v//") 126 | vers=${VERSION#"v"} 127 | echo "${prevvers},${vers}" 128 | # sort -V -C returns 0 if args are ascending version order 129 | if !(echo "${prevvers},${vers}" | tr ',' '\n' | sort -V -C) 130 | then 131 | LATEST="--latest=false" 132 | fi 133 | fi 134 | 135 | gh release create $VERSION \ 136 | -t $title \ 137 | --notes-file $VERSION-release-notes.md \ 138 | --verify-tag \ 139 | $RC \ 140 | $LATEST \ 141 | release-artifacts/* 142 | 143 | publish-crate: 144 | name: Publish Crate 145 | if: needs.check.outputs.buildonly == 'false' 146 | runs-on: ubuntu-latest 147 | needs: check 148 | steps: 149 | - name: Update Rust 150 | run: | 151 | rustup update stable 152 | rustc --version 153 | cargo --version 154 | - name: Checkout Version 155 | uses: actions/checkout@v6 156 | with: 157 | ref: ${{needs.check.outputs.version}} 158 | - name: Publish crate 159 | run: make crate-publish 160 | env: 161 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 162 | -------------------------------------------------------------------------------- /src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use std::collections::HashMap; 3 | use std::net::IpAddr; 4 | use std::vec::Vec; 5 | 6 | // The core structure of the in-memory backing store for the DNS server. 7 | // TODO: I've initially intermingled v4 and v6 addresses for simplicity; the 8 | // server will get back a mix of responses and filter for v4/v6 from there. 9 | // This may not be a good decision, not sure yet; we can split later if 10 | // necessary. 11 | pub struct DNSBackend { 12 | // Map of IP -> Network membership. 13 | // Every container must have an entry in this map, otherwise we will not 14 | // service requests to the Podman TLD for it. 15 | pub ip_mappings: HashMap>, 16 | // Map of network name to map of name to IP addresses. 17 | pub name_mappings: HashMap>>, 18 | // Map of network name to map of IP address to container name. 19 | pub reverse_mappings: HashMap>>, 20 | // Map of IP address to DNS server IPs to service queries not handled 21 | // directly. 22 | pub ctr_dns_server: HashMap>>, 23 | // Map of network name and DNS server IPs. 24 | pub network_dns_server: HashMap>, 25 | // Map of network name to bool (network is/is not internal) 26 | pub network_is_internal: HashMap, 27 | 28 | // search_domain used by aardvark-dns 29 | pub search_domain: String, 30 | } 31 | 32 | impl DNSBackend { 33 | // Create a new backend from the given set of network mappings. 34 | pub fn new( 35 | containers: HashMap>, 36 | networks: HashMap>>, 37 | reverse: HashMap>>, 38 | ctr_dns_server: HashMap>>, 39 | network_dns_server: HashMap>, 40 | network_is_internal: HashMap, 41 | mut search_domain: String, 42 | ) -> DNSBackend { 43 | // dns request always end with dot so append one for easier compare later 44 | if let Some(c) = search_domain.chars().rev().nth(0) { 45 | if c != '.' { 46 | search_domain.push('.') 47 | } 48 | } 49 | DNSBackend { 50 | ip_mappings: containers, 51 | name_mappings: networks, 52 | reverse_mappings: reverse, 53 | ctr_dns_server, 54 | network_dns_server, 55 | network_is_internal, 56 | search_domain, 57 | } 58 | } 59 | 60 | // Handle a single DNS lookup made by a given IP. 61 | // Returns all the ips for the given entry name 62 | pub fn lookup( 63 | &self, 64 | requester: &IpAddr, 65 | network_name: &str, 66 | entry: &str, 67 | ) -> Option> { 68 | // Normalize lookup entry to lowercase. 69 | let mut name = entry.to_lowercase(); 70 | 71 | // Trim off configured search domain if needed as keys do not contain it. 72 | // There doesn't seem to be a nicer way to do that: 73 | // https://users.rust-lang.org/t/can-strip-suffix-mutate-a-string-value/86852 74 | if name.ends_with(&self.search_domain) { 75 | name.truncate(name.len() - self.search_domain.len()) 76 | } 77 | 78 | // if this is a fully qualified name, remove dots so backend can perform search 79 | if name.ends_with(".") { 80 | name.truncate(name.len() - 1) 81 | } 82 | 83 | let owned_netns: Vec; 84 | 85 | let nets = match self.ip_mappings.get(requester) { 86 | Some(n) => n, 87 | // no source ip found let's just allow access to the current network where the request was made 88 | // On newer rust versions in CI we can return &vec![network_name.to_string()] directly without the extra assignment to the outer scope 89 | None => { 90 | owned_netns = vec![network_name.to_string()]; 91 | &owned_netns 92 | } 93 | }; 94 | 95 | let mut results: Vec = Vec::new(); 96 | 97 | for net in nets { 98 | let net_names = match self.name_mappings.get(net) { 99 | Some(n) => n, 100 | None => { 101 | error!("Container with IP {requester} belongs to network {net} but there is no listing in networks table!"); 102 | continue; 103 | } 104 | }; 105 | 106 | if let Some(addrs) = net_names.get(&name) { 107 | results.append(&mut addrs.clone()); 108 | } 109 | } 110 | 111 | if results.is_empty() { 112 | return None; 113 | } 114 | 115 | Some(results) 116 | } 117 | 118 | // Returns list of network resolvers for a particular container 119 | pub fn get_network_scoped_resolvers(&self, requester: &IpAddr) -> Option> { 120 | let mut results: Vec = Vec::new(); 121 | 122 | match self.ip_mappings.get(requester) { 123 | Some(nets) => { 124 | for net in nets { 125 | match self.network_dns_server.get(net) { 126 | Some(resolvers) => results.extend_from_slice(resolvers), 127 | None => { 128 | continue; 129 | } 130 | }; 131 | } 132 | } 133 | None => return None, 134 | }; 135 | 136 | Some(results) 137 | } 138 | 139 | // Checks if a container is associated with only internal networks. 140 | // Returns true if and only if a container is only present in 141 | // internal networks. 142 | pub fn ctr_is_internal(&self, requester: &IpAddr) -> bool { 143 | match self.ip_mappings.get(requester) { 144 | Some(nets) => { 145 | for net in nets { 146 | match self.network_is_internal.get(net) { 147 | Some(internal) => { 148 | if !internal { 149 | return false; 150 | } 151 | } 152 | None => continue, 153 | } 154 | } 155 | } 156 | // For safety, if we don't know about the IP, assume it's probably 157 | // someone on the host asking; let them access DNS. 158 | None => return false, 159 | } 160 | 161 | true 162 | } 163 | 164 | /// Return a single name resolved via mapping if it exists. 165 | pub fn reverse_lookup(&self, requester: &IpAddr, lookup_ip: &IpAddr) -> Option<&Vec> { 166 | let nets = self.ip_mappings.get(requester)?; 167 | 168 | for ips in nets.iter().filter_map(|v| self.reverse_mappings.get(v)) { 169 | if let Some(names) = ips.get(lookup_ip) { 170 | return Some(names); 171 | } 172 | } 173 | 174 | None 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /.cirrus.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Format Ref: https://cirrus-ci.org/guide/writing-tasks/ 4 | 5 | # Main collection of env. vars to set for all tasks and scripts. 6 | env: 7 | # Actual|intended branch for this run 8 | DEST_BRANCH: "main" 9 | # The default is 'sh' if unspecified 10 | CIRRUS_SHELL: "/bin/bash" 11 | # Location where source repo. will be cloned 12 | CIRRUS_WORKING_DIR: "/var/tmp/aardvark-dns" 13 | # Rust package cache also lives here 14 | CARGO_HOME: "/var/cache/cargo" 15 | # Rust compiler output lives here (see Makefile) 16 | CARGO_TARGET_DIR: "$CIRRUS_WORKING_DIR/targets" 17 | # Testing depends on the latest netavark binary from upstream CI 18 | NETAVARK_BRANCH: "main" 19 | NETAVARK_URL: "https://api.cirrus-ci.com/v1/artifact/github/containers/netavark/success/binary.zip?branch=${NETAVARK_BRANCH}" 20 | # Save a little typing (path relative to $CIRRUS_WORKING_DIR) 21 | SCRIPT_BASE: "./contrib/cirrus" 22 | IMAGE_SUFFIX: "c20250820t132717z-f42f41d13" 23 | FEDORA_NETAVARK_IMAGE: "fedora-netavark-${IMAGE_SUFFIX}" 24 | FEDORA_NETAVARK_AMI: "fedora-netavark-aws-arm64-${IMAGE_SUFFIX}" 25 | EC2_INST_TYPE: "t4g.xlarge" 26 | 27 | 28 | gcp_credentials: ENCRYPTED[f6a0e4101418bec8180783b208721fc990772817364fed0346f5fd126bf0cfca03738dd8c7fb867944637a1eac7cec37] 29 | 30 | aws_credentials: ENCRYPTED[db54f7f642877c68cc64fb78468ef99170d387ef6ece5172b2d6fbbb8095d4d276909468c339fe3b38234340bae2189d] 31 | 32 | build_task: 33 | alias: "build" 34 | # Compiling is very CPU intensive, make it chooch quicker for this task only 35 | gce_instance: &standard_build_gce_x86_64 36 | image_project: "libpod-218412" 37 | zone: "us-central1-c" 38 | disk: 200 # GB, do not set <200 per gcloud warning re: I/O performance 39 | cpu: 8 40 | memory: "8Gb" 41 | image_name: "${FEDORA_NETAVARK_IMAGE}" 42 | cargo_cache: &cargo_cache 43 | folder: "$CARGO_HOME" 44 | fingerprint_script: echo -e "cargo_v3_${DEST_BRANCH}_amd64\n---\n$(- 241 | aardvark-dns 242 | aardvark-dns.debug 243 | aardvark-dns.info 244 | aardvark-dns.aarch64-unknown-linux-gnu 245 | aardvark-dns.debug.aarch64-unknown-linux-gnu 246 | aardvark-dns.info.aarch64-unknown-linux-gnu 247 | bin_cache: *ro_bin_cache 248 | clone_script: *noop 249 | # The paths used for uploaded artifacts are relative here and in Cirrus 250 | artifacts_prep_script: 251 | - set -x 252 | - curl --fail --location -o /tmp/armbinary.zip ${API_URL_BASE}/build_aarch64/armbinary.zip 253 | - unzip /tmp/armbinary.zip 254 | - mv bin/* ./ 255 | - rm -rf bin 256 | artifacts_test_script: # Other CI systems depend on all files being present 257 | - ls -la 258 | # If there's a missing file, show what it was in the output 259 | - for fn in $EXP_BINS; do [[ -r "$(echo $fn|tee /dev/stderr)" ]] || exit 1; done 260 | # Upload tested binary for consumption downstream 261 | # https://cirrus-ci.org/guide/writing-tasks/#artifacts-instruction 262 | binary_artifacts: 263 | path: ./aardvark-dns* 264 | -------------------------------------------------------------------------------- /test/300-three-networks.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | 8 | @test "three networks with a connect" { 9 | setup_dnsmasq 10 | 11 | subnet_a=$(random_subnet 5) 12 | subnet_b=$(random_subnet 5) 13 | 14 | # A1 15 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" 16 | a1_config=$config 17 | a1_container_id=$(echo "$a1_config" | jq -r .container_id) 18 | a1_ip=$(echo "$a1_config" | jq -r .networks.podman1.static_ips[0]) 19 | a_gw=$(echo "$a1_config" | jq -r .network_info.podman1.subnets[0].gateway) 20 | a1_hash=$(echo "$a1_config" | jq -r .network_info.podman1.id) 21 | create_container "$a1_config" 22 | a1_pid=$CONTAINER_NS_PID 23 | 24 | # container b1 on subnet b 25 | create_config network_name="podman2" container_id=$(random_string 64) container_name="bone" subnet="$subnet_b" 26 | b1_config=$config 27 | b1_ip=$(echo "$b1_config" | jq -r .networks.podman2.static_ips[0]) 28 | b_gw=$(echo "$b1_config" | jq -r .network_info.podman2.subnets[0].gateway) 29 | b1_hash=$(echo "$b1_config" | jq -r .network_info.podman1.id) 30 | create_container "$b1_config" 31 | b1_pid=$CONTAINER_NS_PID 32 | b_subnets=$(echo $b1_config | jq -r .network_info.podman2.subnets[0]) 33 | 34 | # AB2 35 | create_config network_name="podman1" container_id=$(random_string 64) container_name="abtwo" subnet="$subnet_a" 36 | a2_config=$config 37 | a2_ip=$(echo "$a2_config" | jq -r .networks.podman1.static_ips[0]) 38 | 39 | b2_ip=$(random_ip_in_subnet "$subnet_b") 40 | create_network "podman2" "$b2_ip" "eth1" 41 | b2_network="{$new_network}" 42 | create_network_infos "podman2" "$b1_hash" "$b_subnets" 43 | b2_network_info="{$new_network_info}" 44 | ab2_config=$(jq -r ".networks += $b2_network" <<<"$a2_config") 45 | ab2_config=$(jq -r ".network_info += $b2_network_info" <<<"$ab2_config") 46 | 47 | create_container "$ab2_config" 48 | ab2_pid=$CONTAINER_NS_PID 49 | 50 | # aone should be able to resolve AB2 and NOT B1 51 | dig "$a1_pid" "abtwo" "$a_gw" 52 | assert "$a2_ip" 53 | dig "$a1_pid" "bone" "$a_gw" 54 | assert "" 55 | 56 | # bone should be able to resolve AB2 and NOT A1 57 | dig "$b1_pid" "abtwo" "$b_gw" 58 | assert "$b2_ip" 59 | dig "$b1_pid" "aone" "$b_gw" 60 | assert "" 61 | 62 | # abtwo should be able to resolve A1, B1, and AB2 on both gws 63 | dig "$ab2_pid" "aone" "$a_gw" 64 | assert "$a1_ip" 65 | dig "$ab2_pid" "aone" "$b_gw" 66 | assert "$a1_ip" 67 | 68 | dig "$ab2_pid" "bone" "$a_gw" 69 | assert "$b1_ip" 70 | dig "$ab2_pid" "bone" "$b_gw" 71 | assert "$b1_ip" 72 | 73 | # now the same again with search domain set 74 | dig "$ab2_pid" "aone.dns.podman" "$a_gw" 75 | assert "$a1_ip" 76 | dig "$ab2_pid" "aone.dns.podman" "$b_gw" 77 | assert "$a1_ip" 78 | 79 | dig "$ab2_pid" "bone.dns.podman" "$a_gw" 80 | assert "$b1_ip" 81 | dig "$ab2_pid" "bone.dns.podman" "$b_gw" 82 | assert "$b1_ip" 83 | 84 | # check ab2 from itself, first from the a side 85 | dig "$ab2_pid" "abtwo" "$a_gw" 86 | assert "${#lines[@]}" = 2 87 | assert "$output" =~ "$a2_ip" 88 | assert "$output" =~ "$b2_ip" 89 | 90 | # and now from the bside 91 | dig "$ab2_pid" "abtwo" "$b_gw" 92 | assert "${#lines[@]}" = 2 93 | assert "$output" =~ "$a2_ip" 94 | assert "$output" =~ "$b2_ip" 95 | } 96 | 97 | @test "three subnets, one container on two of the subnets, network connect" { 98 | # Create all three subnets 99 | subnet_a=$(random_subnet 5) 100 | subnet_b=$(random_subnet 5) 101 | subnet_c=$(random_subnet 5) 102 | 103 | # A1 on subnet A 104 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" 105 | a1_config=$config 106 | a1_container_id=$(echo "$a1_config" | jq -r .container_id) 107 | a1_ip=$(echo "$a1_config" | jq -r .networks.podman1.static_ips[0]) 108 | a_gw=$(echo "$a1_config" | jq -r .network_info.podman1.subnets[0].gateway) 109 | a1_hash=$(echo "$a1_config" | jq -r .network_info.podman1.id) 110 | create_container "$a1_config" 111 | a1_pid=$CONTAINER_NS_PID 112 | 113 | # C1 on subnet C 114 | create_config network_name="podman3" container_id=$(random_string 64) container_name="cone" subnet="$subnet_c" 115 | c1_config=$config 116 | c1_container_id=$(echo "$c1_config" | jq -r .container_id) 117 | c1_ip=$(echo "$c1_config" | jq -r .networks.podman3.static_ips[0]) 118 | c_gw=$(echo "$c1_config" | jq -r .network_info.podman3.subnets[0].gateway) 119 | c1_hash=$(echo "$c1_config" | jq -r .network_info.podman3.id) 120 | create_container "$c1_config" 121 | c1_pid=$CONTAINER_NS_PID 122 | c_subnets=$(echo $c1_config | jq -r .network_info.podman3.subnets[0]) 123 | 124 | # We now have one container on A and one on C. We now similate 125 | # a network connect on both to B. 126 | # 127 | # This is also where things get tricky and we are trying to mimic 128 | # a connect. First, we need to trim off the last two container 129 | # configs for teardown. We will leave the NS_PIDS alone because 130 | # the order should be OK. 131 | 132 | # Create B1 config for network connect 133 | create_config network_name="podman2" container_id=$(random_string 64) container_name="aone" subnet="$subnet_b" aliases='"aone_nw"' 134 | b1_config=$config 135 | # The container ID should be the same 136 | b1_config=$(jq ".container_id |= \"$a1_container_id\"" <<<"$b1_config") 137 | b1_config=$(jq ".networks.podman2.interface_name |= \"eth1\"" <<<"$b1_config") 138 | b1_network=$(echo "$b1_config" | jq -r .networks) 139 | b1_network_info=$(echo "$b1_config" | jq -r .network_info) 140 | b1_ip=$(echo "$b1_network" | jq -r .podman2.static_ips[0]) 141 | b_gw=$(echo "$b1_network_info" | jq -r .podman2.subnets[0].gateway) 142 | 143 | # Now we must merge a1 and b1 for eventual teardown 144 | a1b1_config=$(jq -r ".networks += $b1_network" <<<"$a1_config") 145 | a1b1_config=$(jq -r ".network_info += $b1_network_info" <<<"$a1b1_config") 146 | 147 | # Create B2 config for network connect 148 | # 149 | create_config network_name="podman2" container_id=$(random_string 64) container_name="cone" subnet="$subnet_b" aliases='"cone_nw"' 150 | b2_config=$config 151 | # The container ID should be the same 152 | b2_config=$(jq ".container_id |= \"$c1_container_id\"" <<<"$b2_config") 153 | b2_config=$(jq ".networks.podman2.interface_name |= \"eth1\"" <<<"$b2_config") 154 | b2_network=$(echo "$b2_config" | jq -r .networks) 155 | b2_network_info=$(echo "$b2_config" | jq -r .network_info) 156 | b2_ip=$(echo "$b2_network" | jq -r .podman2.static_ips[0]) 157 | 158 | # Now we must merge c1 and b2 for eventual teardown 159 | c1b2_config=$(jq -r ".networks += $b2_network" <<<"$c1_config") 160 | c1b2_config=$(jq -r ".network_info += $b2_network_info" <<<"$c1b2_config") 161 | 162 | # Create the containers but do not add to NS_PIDS or CONTAINER_CONFIGS 163 | connect "$a1_pid" "$b1_config" 164 | connect "$c1_pid" "$b2_config" 165 | 166 | # Reset CONTAINER_CONFIGS and add the two news ones 167 | CONTAINER_CONFIGS=("$a1b1_config" "$c1b2_config") 168 | 169 | # Verify 170 | # b1 should be able to resolve cone through b subnet 171 | dig "$a1_pid" "cone" "$b_gw" 172 | assert "$b2_ip" 173 | 174 | # a1 should be able to resolve cone 175 | dig "$a1_pid" "cone" "$a_gw" 176 | assert "$b2_ip" 177 | 178 | # a1b1 should be able to resolve cone_nw alias 179 | dig "$a1_pid" "cone_nw" "$a_gw" 180 | assert "$b2_ip" 181 | 182 | # b2 should be able to resolve cone through b subnet 183 | dig "$c1_pid" "aone" "$b_gw" 184 | assert "$b1_ip" 185 | 186 | # c1 should be able to resolve aone 187 | dig "$c1_pid" "aone" "$c_gw" 188 | assert "$b1_ip" 189 | 190 | # b2c1 should be able to resolve aone_nw alias 191 | dig "$c1_pid" "aone_nw" "$c_gw" 192 | assert "$b1_ip" 193 | } 194 | 195 | 196 | @test "three subnets two ipaddress v6 and one ipaddress v4, one container on two of the subnets, network connect" { 197 | # Create all three subnets 198 | # Two of the subnets must be on ip addresss v6 and one on ip address v4 199 | subnet_a=$(random_subnet 5) 200 | subnet_b=$(random_subnet 6) 201 | subnet_c=$(random_subnet 6) 202 | 203 | # A1 on subnet A 204 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" 205 | a1_config=$config 206 | a1_container_id=$(echo "$a1_config" | jq -r .container_id) 207 | a1_ip=$(echo "$a1_config" | jq -r .networks.podman1.static_ips[0]) 208 | a_gw=$(echo "$a1_config" | jq -r .network_info.podman1.subnets[0].gateway) 209 | a1_hash=$(echo "$a1_config" | jq -r .network_info.podman1.id) 210 | create_container "$a1_config" 211 | a1_pid=$CONTAINER_NS_PID 212 | 213 | # C1 on subnet C 214 | create_config network_name="podman3" container_id=$(random_string 64) container_name="cone" subnet="$subnet_c" 215 | c1_config=$config 216 | c1_container_id=$(echo "$c1_config" | jq -r .container_id) 217 | c1_ip=$(echo "$c1_config" | jq -r .networks.podman3.static_ips[0]) 218 | c_gw=$(echo "$c1_config" | jq -r .network_info.podman3.subnets[0].gateway) 219 | c1_hash=$(echo "$c1_config" | jq -r .network_info.podman3.id) 220 | create_container "$c1_config" 221 | c1_pid=$CONTAINER_NS_PID 222 | c_subnets=$(echo $c1_config | jq -r .network_info.podman3.subnets[0]) 223 | 224 | # We now have one container on A and one on C. We now similate 225 | # a network connect on both to B. 226 | 227 | # Create B1 config for network connect 228 | create_config network_name="podman2" container_id=$(random_string 64) container_name="aone" subnet="$subnet_b" aliases='"aone_nw"' 229 | b1_config=$config 230 | # The container ID should be the same 231 | b1_config=$(jq ".container_id |= \"$a1_container_id\"" <<<"$b1_config") 232 | b1_config=$(jq ".networks.podman2.interface_name |= \"eth1\"" <<<"$b1_config") 233 | b1_network=$(echo "$b1_config" | jq -r .networks) 234 | b1_network_info=$(echo "$b1_config" | jq -r .network_info) 235 | b1_ip=$(echo "$b1_network" | jq -r .podman2.static_ips[0]) 236 | b_gw=$(echo "$b1_network_info" | jq -r .podman2.subnets[0].gateway) 237 | 238 | # Now we must merge a1 and b1 for eventual teardown 239 | a1b1_config=$(jq -r ".networks += $b1_network" <<<"$a1_config") 240 | a1b1_config=$(jq -r ".network_info += $b1_network_info" <<<"$a1b1_config") 241 | 242 | # Create B2 config for network connect 243 | # 244 | create_config network_name="podman2" container_id=$(random_string 64) container_name="cone" subnet="$subnet_b" aliases='"cone_nw"' 245 | b2_config=$config 246 | # The container ID should be the same 247 | b2_config=$(jq ".container_id |= \"$c1_container_id\"" <<<"$b2_config") 248 | b2_config=$(jq ".networks.podman2.interface_name |= \"eth1\"" <<<"$b2_config") 249 | b2_network=$(echo "$b2_config" | jq -r .networks) 250 | b2_network_info=$(echo "$b2_config" | jq -r .network_info) 251 | b2_ip=$(echo "$b2_network" | jq -r .podman2.static_ips[0]) 252 | 253 | # Now we must merge c1 and b2 for eventual teardown 254 | c1b2_config=$(jq -r ".networks += $b2_network" <<<"$c1_config") 255 | c1b2_config=$(jq -r ".network_info += $b2_network_info" <<<"$c1b2_config") 256 | 257 | # Create the containers but do not add to NS_PIDS or CONTAINER_CONFIGS 258 | connect "$a1_pid" "$b1_config" 259 | connect "$c1_pid" "$b2_config" 260 | 261 | # Reset CONTAINER_CONFIGS and add the two news ones 262 | CONTAINER_CONFIGS=("$a1b1_config" "$c1b2_config") 263 | 264 | # Verify 265 | # b1 should be able to resolve cone through b subnet 266 | dig "$a1_pid" "cone" "$b_gw" "AAAA" 267 | assert "$b2_ip" 268 | 269 | # a1 should be able to resolve cone 270 | dig "$a1_pid" "cone" "$a_gw" "AAAA" 271 | assert "$b2_ip" 272 | } 273 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::backend::DNSBackend; 2 | use crate::error::{AardvarkError, AardvarkResult}; 3 | use log::error; 4 | use std::collections::HashMap; 5 | use std::fs::{metadata, read_dir, read_to_string}; 6 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 7 | use std::vec::Vec; 8 | pub mod constants; 9 | 10 | // Parse configuration files in the given directory. 11 | // Configuration files are formatted as follows: 12 | // The name of the file will be interpreted as the name of the network. 13 | // The first line must be the gateway IP(s) of the network, comma-separated. 14 | // All subsequent individual lines contain info on a single container and are 15 | // formatted as: 16 | // 17 | // Where space is a single space character. 18 | // Returns a complete DNSBackend struct (all that is necessary for looks) and 19 | 20 | // Silent clippy: sometimes clippy marks useful tyes as complex and for this case following type is 21 | // convinient 22 | #[allow(clippy::type_complexity)] 23 | pub fn parse_configs( 24 | dir: &str, 25 | filter_search_domain: &str, 26 | ) -> AardvarkResult<( 27 | DNSBackend, 28 | HashMap>, 29 | HashMap>, 30 | )> { 31 | if !metadata(dir)?.is_dir() { 32 | return Err(AardvarkError::msg(format!( 33 | "config directory {dir} must exist and be a directory" 34 | ))); 35 | } 36 | 37 | let mut network_membership: HashMap> = HashMap::new(); 38 | let mut container_ips: HashMap> = HashMap::new(); 39 | let mut reverse: HashMap>> = HashMap::new(); 40 | let mut network_names: HashMap>> = HashMap::new(); 41 | let mut listen_ips_4: HashMap> = HashMap::new(); 42 | let mut listen_ips_6: HashMap> = HashMap::new(); 43 | let mut ctr_dns_server: HashMap>> = HashMap::new(); 44 | let mut network_dns_server: HashMap> = HashMap::new(); 45 | let mut network_is_internal: HashMap = HashMap::new(); 46 | 47 | // Enumerate all files in the directory, read them in one by one. 48 | // Steadily build a map of what container has what IPs and what 49 | // container is in what networks. 50 | let configs = read_dir(dir)?; 51 | for config in configs { 52 | // Each entry is a result. Interpret Err to mean the config was removed 53 | // while we were working; warn only, don't error. 54 | // Might be safer to completely restart the process, but there's also a 55 | // chance that, if we do that, we never finish and update the config, 56 | // assuming the files in question are modified at a sufficiently high 57 | // rate. 58 | match config { 59 | Ok(cfg) => { 60 | // dont process aardvark pid files 61 | if let Some(path) = cfg.path().file_name() { 62 | if path == constants::AARDVARK_PID_FILE { 63 | continue; 64 | } 65 | } 66 | let parsed_network_config = match parse_config(cfg.path().as_path()) { 67 | Ok(c) => c, 68 | Err(e) => { 69 | match &e { 70 | AardvarkError::IOError(io) 71 | if io.kind() != std::io::ErrorKind::NotFound => 72 | { 73 | // Do no log the error if the file was removed 74 | } 75 | _ => { 76 | error!( 77 | "Error reading config file {:?} for server update: {}", 78 | cfg.path(), 79 | e 80 | ) 81 | } 82 | } 83 | continue; 84 | } 85 | }; 86 | 87 | let mut internal = false; 88 | 89 | let network_name: String = match cfg.path().file_name() { 90 | // This isn't *completely* safe, but I do not foresee many 91 | // cases where our network names include non-UTF8 92 | // characters. 93 | Some(s) => match s.to_str() { 94 | Some(st) => { 95 | let name_full = st.to_string(); 96 | if name_full.ends_with(constants::INTERNAL_SUFFIX) { 97 | internal = true; 98 | } 99 | name_full.strip_suffix(constants::INTERNAL_SUFFIX).unwrap_or(&name_full).to_string() 100 | }, 101 | None => return Err(AardvarkError::msg( 102 | format!("configuration file {} name has non-UTF8 characters", s.to_string_lossy()), 103 | )), 104 | }, 105 | None => return Err(AardvarkError::msg( 106 | format!("configuration file {} does not have a file name, cannot identify network name", cfg.path().to_string_lossy()), 107 | )), 108 | }; 109 | 110 | // Network DNS Servers were found while parsing config 111 | // lets populate the backend 112 | // Only if network is not internal. 113 | // If internal, explicitly insert empty list. 114 | if !parsed_network_config.network_dnsservers.is_empty() && !internal { 115 | network_dns_server.insert( 116 | network_name.clone(), 117 | parsed_network_config.network_dnsservers, 118 | ); 119 | } 120 | if internal { 121 | network_dns_server.insert(network_name.clone(), Vec::new()); 122 | } 123 | 124 | for ip in parsed_network_config.network_bind_ip { 125 | match ip { 126 | IpAddr::V4(a) => listen_ips_4 127 | .entry(network_name.clone()) 128 | .or_default() 129 | .push(a), 130 | IpAddr::V6(b) => listen_ips_6 131 | .entry(network_name.clone()) 132 | .or_default() 133 | .push(b), 134 | } 135 | } 136 | 137 | for entry in parsed_network_config.container_entry { 138 | // Container network membership 139 | let ctr_networks = network_membership.entry(entry.id.clone()).or_default(); 140 | 141 | // Keep the network deduplicated 142 | if !ctr_networks.contains(&network_name) { 143 | ctr_networks.push(network_name.clone()); 144 | } 145 | 146 | // Container IP addresses 147 | let mut new_ctr_ips: Vec = Vec::new(); 148 | if let Some(v4) = entry.v4 { 149 | for ip in v4 { 150 | reverse 151 | .entry(network_name.clone()) 152 | .or_default() 153 | .entry(IpAddr::V4(ip)) 154 | .or_default() 155 | .append(&mut entry.aliases.clone()); 156 | // DNS only accepted on non-internal networks. 157 | if !internal { 158 | ctr_dns_server.insert(IpAddr::V4(ip), entry.dns_servers.clone()); 159 | } 160 | new_ctr_ips.push(IpAddr::V4(ip)); 161 | } 162 | } 163 | if let Some(v6) = entry.v6 { 164 | for ip in v6 { 165 | reverse 166 | .entry(network_name.clone()) 167 | .or_default() 168 | .entry(IpAddr::V6(ip)) 169 | .or_default() 170 | .append(&mut entry.aliases.clone()); 171 | // DNS only accepted on non-internal networks. 172 | if !internal { 173 | ctr_dns_server.insert(IpAddr::V6(ip), entry.dns_servers.clone()); 174 | } 175 | new_ctr_ips.push(IpAddr::V6(ip)); 176 | } 177 | } 178 | 179 | let ctr_ips = container_ips.entry(entry.id.clone()).or_default(); 180 | ctr_ips.append(&mut new_ctr_ips.clone()); 181 | 182 | // Network aliases to IPs map. 183 | let network_aliases = network_names.entry(network_name.clone()).or_default(); 184 | for alias in entry.aliases { 185 | let alias_entries = network_aliases.entry(alias).or_default(); 186 | alias_entries.append(&mut new_ctr_ips.clone()); 187 | } 188 | 189 | network_is_internal.insert(network_name.clone(), internal); 190 | } 191 | } 192 | Err(e) => { 193 | if e.kind() != std::io::ErrorKind::NotFound { 194 | error!("Error listing config file for server update: {e}") 195 | } 196 | } 197 | } 198 | } 199 | 200 | // Set up types to be returned. 201 | let mut ctrs: HashMap> = HashMap::new(); 202 | 203 | for (ctr_id, ips) in container_ips { 204 | match network_membership.get(&ctr_id) { 205 | Some(s) => { 206 | for ip in ips { 207 | let ip_networks = ctrs.entry(ip).or_default(); 208 | ip_networks.append(&mut s.clone()); 209 | } 210 | } 211 | None => { 212 | return Err(AardvarkError::msg(format!( 213 | "Container ID {ctr_id} has an entry in IPs table, but not network membership table" 214 | ))) 215 | } 216 | } 217 | } 218 | 219 | Ok(( 220 | DNSBackend::new( 221 | ctrs, 222 | network_names, 223 | reverse, 224 | ctr_dns_server, 225 | network_dns_server, 226 | network_is_internal, 227 | filter_search_domain.to_owned(), 228 | ), 229 | listen_ips_4, 230 | listen_ips_6, 231 | )) 232 | } 233 | 234 | // A single entry in a config file 235 | struct CtrEntry { 236 | id: String, 237 | v4: Option>, 238 | v6: Option>, 239 | aliases: Vec, 240 | dns_servers: Option>, 241 | } 242 | 243 | // A simplified type for results retured by 244 | // parse_config after parsing a single network 245 | // config. 246 | struct ParsedNetworkConfig { 247 | network_bind_ip: Vec, 248 | container_entry: Vec, 249 | network_dnsservers: Vec, 250 | } 251 | 252 | // Read and parse a single given configuration file 253 | fn parse_config(path: &std::path::Path) -> AardvarkResult { 254 | let content = read_to_string(path)?; 255 | let mut is_first = true; 256 | 257 | let mut bind_addrs: Vec = Vec::new(); 258 | let mut network_dns_servers: Vec = Vec::new(); 259 | let mut ctrs: Vec = Vec::new(); 260 | 261 | // Split on newline, parse each line 262 | for line in content.split('\n') { 263 | if line.is_empty() { 264 | continue; 265 | } 266 | if is_first { 267 | let network_parts = line.split(' ').collect::>(); 268 | if network_parts.is_empty() { 269 | return Err(AardvarkError::msg(format!( 270 | "invalid network configuration file: {}", 271 | path.display() 272 | ))); 273 | } 274 | // process bind ip 275 | for ip in network_parts[0].split(',') { 276 | let local_ip = match ip.parse() { 277 | Ok(l) => l, 278 | Err(e) => { 279 | return Err(AardvarkError::msg(format!( 280 | "error parsing ip address {ip}: {e}" 281 | ))) 282 | } 283 | }; 284 | bind_addrs.push(local_ip); 285 | } 286 | 287 | // If network parts contain more than one col then 288 | // we have custom dns server also defined at network level 289 | // lets process that. 290 | if network_parts.len() > 1 { 291 | for ip in network_parts[1].split(',') { 292 | let local_ip = match ip.parse() { 293 | Ok(l) => l, 294 | Err(e) => { 295 | return Err(AardvarkError::msg(format!( 296 | "error parsing network dns address {ip}: {e}" 297 | ))) 298 | } 299 | }; 300 | network_dns_servers.push(local_ip); 301 | } 302 | } 303 | 304 | is_first = false; 305 | continue; 306 | } 307 | 308 | // Split on space 309 | let parts = line.split(' ').collect::>(); 310 | if parts.len() < 4 { 311 | return Err(AardvarkError::msg(format!( 312 | "configuration file {} line {} is improperly formatted - too few entries", 313 | path.to_string_lossy(), 314 | line 315 | ))); 316 | } 317 | 318 | let v4_addrs: Option> = if !parts[1].is_empty() { 319 | let ipv4 = match parts[1].split(',').map(|i| i.parse()).collect() { 320 | Ok(i) => i, 321 | Err(e) => { 322 | return Err(AardvarkError::msg(format!( 323 | "error parsing IP address {}: {}", 324 | parts[1], e 325 | ))) 326 | } 327 | }; 328 | Some(ipv4) 329 | } else { 330 | None 331 | }; 332 | 333 | let v6_addrs: Option> = if !parts[2].is_empty() { 334 | let ipv6 = match parts[2].split(',').map(|i| i.parse()).collect() { 335 | Ok(i) => i, 336 | Err(e) => { 337 | return Err(AardvarkError::msg(format!( 338 | "error parsing IP address {}: {}", 339 | parts[2], e 340 | ))) 341 | } 342 | }; 343 | Some(ipv6) 344 | } else { 345 | None 346 | }; 347 | 348 | let aliases: Vec = parts[3] 349 | .split(',') 350 | .map(|x| x.to_string().to_lowercase()) 351 | .collect::>(); 352 | 353 | if aliases.is_empty() { 354 | return Err(AardvarkError::msg(format!( 355 | "configuration file {} line {} is improperly formatted - no names given", 356 | path.to_string_lossy(), 357 | line 358 | ))); 359 | } 360 | 361 | let dns_servers: Option> = if parts.len() == 5 && !parts[4].is_empty() { 362 | let dns_server = match parts[4].split(',').map(|i| i.parse()).collect() { 363 | Ok(i) => i, 364 | Err(e) => { 365 | return Err(AardvarkError::msg(format!( 366 | "error parsing DNS server address {}: {}", 367 | parts[4], e 368 | ))) 369 | } 370 | }; 371 | Some(dns_server) 372 | } else { 373 | None 374 | }; 375 | 376 | ctrs.push(CtrEntry { 377 | id: parts[0].to_string().to_lowercase(), 378 | v4: v4_addrs, 379 | v6: v6_addrs, 380 | aliases, 381 | dns_servers, 382 | }); 383 | } 384 | 385 | // Must provide at least one bind address 386 | if bind_addrs.is_empty() { 387 | return Err(AardvarkError::msg(format!( 388 | "configuration file {} does not provide any bind addresses", 389 | path.to_string_lossy() 390 | ))); 391 | } 392 | 393 | Ok(ParsedNetworkConfig { 394 | network_bind_ip: bind_addrs, 395 | container_entry: ctrs, 396 | network_dnsservers: network_dns_servers, 397 | }) 398 | } 399 | -------------------------------------------------------------------------------- /test/100-basic-name-resolution.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -*- bats -*- 2 | # 3 | # basic netavark tests 4 | # 5 | 6 | load helpers 7 | 8 | 9 | HELPER_PID= 10 | function teardown() { 11 | if [[ -n "$HELPER_PID" ]]; then 12 | kill -9 $HELPER_PID 13 | fi 14 | basic_teardown 15 | } 16 | 17 | # custom DNS server is set to `127.0.0.255` which is invalid DNS server 18 | # hence all the external request must fail, this test is expected to fail 19 | # with exit code 124 20 | @test "basic container - dns itself (custom bad dns server)" { 21 | setup_dnsmasq 22 | 23 | subnet_a=$(random_subnet 5) 24 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" custom_dns_server='"127.0.0.255"' aliases='"a1", "1a"' 25 | config_a1=$config 26 | ip_a1=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 27 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 28 | create_container "$config_a1" 29 | a1_pid=$CONTAINER_NS_PID 30 | run_in_container_netns "$a1_pid" "dig" "+short" "aone" "@$gw" 31 | assert "$ip_a1" 32 | # Set recursion bit is already set if requested so output must not 33 | # contain unexpected warning. 34 | assert "$output" !~ "WARNING: recursion requested but not available" 35 | 36 | # custom dns server is set to 127.0.0.255 which is not a valid DNS server so external DNS request must fail 37 | expected_rc=124 run_in_container_netns "$a1_pid" "dig" "+short" "$TEST_DOMAIN" "@$gw" 38 | } 39 | 40 | # custom DNS server is set to `8.8.8.8, 1.1.1.1` which is valid DNS server 41 | # hence all the external request must paas. 42 | @test "basic container - dns itself (custom good dns server)" { 43 | setup_dnsmasq 44 | 45 | # launch dnsmasq to run a second local server with a unique name so we know custom_dns_server works 46 | run_in_host_netns dnsmasq --conf-file=/dev/null --pid-file="$AARDVARK_TMPDIR/dnsmasq2.pid" \ 47 | --except-interface=lo --listen-address=127.1.1.53 --bind-interfaces \ 48 | --address=/unique-name.local/192.168.0.1 --no-resolv --no-hosts 49 | HELPER_PID=$(cat $AARDVARK_TMPDIR/dnsmasq2.pid) 50 | 51 | subnet_a=$(random_subnet 5) 52 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" custom_dns_server='"127.1.1.53"' aliases='"a1", "1a"' 53 | 54 | config_a1=$config 55 | ip_a1=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 56 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 57 | create_container "$config_a1" 58 | a1_pid=$CONTAINER_NS_PID 59 | run_in_container_netns "$a1_pid" "dig" "aone" "@$gw" 60 | # check for TTL 0 here as well 61 | assert "$output" =~ "aone\.[[:space:]]*0[[:space:]]*IN[[:space:]]*A[[:space:]]*$ip_a1" 62 | # Set recursion bit is already set if requested so output must not 63 | # contain unexpected warning. 64 | assert "$output" !~ "WARNING: recursion requested but not available" 65 | 66 | run_in_container_netns "$a1_pid" "dig" "+short" "unique-name.local" "@$gw" 67 | # validate that we get the right ip 68 | assert "$output" == "192.168.0.1" 69 | # Set recursion bit is already set if requested so output must not 70 | # contain unexpected warning. 71 | assert "$output" !~ "WARNING: recursion requested but not available" 72 | } 73 | 74 | @test "basic container - dns itself (bad and good should fall back)" { 75 | setup_dnsmasq 76 | 77 | # using exec to keep the udp query hanging for at least 3 seconds 78 | nsenter -m -n -t $HOST_NS_PID socat UDP4-LISTEN:53,bind=127.5.5.5 EXEC:"sleep 3" 3>/dev/null & 79 | HELPER_PID=$! 80 | 81 | subnet_a=$(random_subnet 5) 82 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" custom_dns_server='"127.5.5.5", "127.0.0.1"' aliases='"a1", "1a"' 83 | config_a1=$config 84 | ip_a1=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 85 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 86 | create_container "$config_a1" 87 | a1_pid=$CONTAINER_NS_PID 88 | 89 | # first custom server is wrong but second server should work 90 | run_in_container_netns "$a1_pid" "dig" "$TEST_DOMAIN" "@$gw" 91 | assert "$output" =~ "Query time: [23][0-9]{3} msec" "timeout should be 2.5s so request should then work shortly after (udp)" 92 | 93 | kill -9 "$HELPER_PID" || true 94 | # Now the same with tcp. 95 | nsenter -m -n -t $HOST_NS_PID socat TCP4-LISTEN:53,bind=127.5.5.5 EXEC:"sleep 3" 3>/dev/null & 96 | HELPER_PID=$! 97 | run_in_container_netns "$a1_pid" "dig" +tcp "$TEST_DOMAIN" "@$gw" 98 | assert "$output" =~ "Query time: [23][0-9]{3} msec" "timeout should be 2.5s so request should then work shortly after (tcp)" 99 | } 100 | 101 | @test "basic container - dns itself custom" { 102 | setup_dnsmasq 103 | 104 | subnet_a=$(random_subnet 5) 105 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" aliases='"a1", "1a"' 106 | config_a1=$config 107 | ip_a1=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 108 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 109 | create_container "$config_a1" 110 | a1_pid=$CONTAINER_NS_PID 111 | run_in_container_netns "$a1_pid" "dig" "+short" "aone" "@$gw" 112 | assert "$ip_a1" 113 | # Set recursion bit is already set if requested so output must not 114 | # contain unexpected warning. 115 | assert "$output" !~ "WARNING: recursion requested but not available" 116 | 117 | # check TCP support 118 | run_in_container_netns "$a1_pid" "dig" "+tcp" "+short" "aone" "@$gw" 119 | assert "$ip_a1" 120 | 121 | # check multiple TCP requests over single connecting by using +keepopen 122 | # https://github.com/containers/aardvark-dns/issues/605 123 | run_in_container_netns "$a1_pid" "dig" "+tcp" "+short" +keepopen "@$gw" "aone" "a1" "1a" 124 | assert "${lines[0]}" == "$ip_a1" "ip for aone" 125 | assert "${lines[1]}" == "$ip_a1" "ip for a1" 126 | assert "${lines[2]}" == "$ip_a1" "ip for 1a" 127 | 128 | run_in_container_netns "$a1_pid" "dig" "+short" "$TEST_DOMAIN" "@$gw" 129 | # validate that we get an ipv4 130 | assert "$output" =~ "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" 131 | # Set recursion bit is already set if requested so output must not 132 | # contain unexpected warning. 133 | assert "$output" !~ "WARNING: recursion requested but not available" 134 | 135 | # check TCP support for forwarding 136 | # note there is no guarantee that the forwarding is happening via TCP though 137 | # TODO add custom dns record that is to big for udp so we can be sure... 138 | run_in_container_netns "$a1_pid" "dig" "+tcp" "$TEST_DOMAIN" "@$gw" 139 | # validate that we get an ipv4 140 | assert "$output" =~ "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" 141 | # TODO This is not working on rhel/centos 9 as the dig version there doesn't print the line, 142 | # so we trust that dig +tcp does the right thing. 143 | # assert "$output" =~ "\(TCP\)" "server used TCP" 144 | # Set recursion bit is already set if requested so output must not 145 | # contain unexpected warning. 146 | assert "$output" !~ "WARNING: recursion requested but not available" 147 | } 148 | 149 | @test "basic container - ndots incomplete entry" { 150 | setup_dnsmasq 151 | 152 | subnet_a=$(random_subnet 5) 153 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" \ 154 | subnet="$subnet_a" aliases='"a1", "1a"' 155 | config_a1=$config 156 | ip_a1=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 157 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 158 | create_container "$config_a1" 159 | a1_pid=$CONTAINER_NS_PID 160 | run_in_container_netns "$a1_pid" "dig" "someshortname" "@$gw" 161 | assert "$output" =~ "status: REFUSED" "dnsmasq returns REFUSED" 162 | 163 | run_in_container_netns "$a1_pid" "dig" "+short" "testname" "@$gw" 164 | assert "198.51.100.1" "should resolve local name from external nameserver (dnsmasq)" 165 | } 166 | 167 | @test "basic container - dns itself on container with ipaddress v6" { 168 | setup_dnsmasq 169 | 170 | subnet_a=$(random_subnet 6) 171 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" aliases='"a1", "1a"' 172 | config_a1=$config 173 | ip_a1=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 174 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 175 | create_container "$config_a1" 176 | a1_pid=$CONTAINER_NS_PID 177 | run_in_container_netns "$a1_pid" "dig" "+short" "aone" "@$gw" "AAAA" 178 | assert "$ip_a1" 179 | # Set recursion bit is already set if requested so output must not 180 | # contain unexpected warning. 181 | assert "$output" !~ "WARNING: recursion requested but not available" 182 | 183 | run_in_container_netns "$a1_pid" "dig" "+short" "$TEST_DOMAIN" "@$gw" "AAAA" 184 | # validate that we got valid ipv6 185 | # check that the output is not empty 186 | assert "$lines[0]" != "" "got at least one result" 187 | for ip in "${lines[@]}"; do 188 | run_helper ipcalc -6c "$ip" 189 | done 190 | # Set recursion bit is already set if requested so output must not 191 | # contain unexpected warning. 192 | assert "$output" !~ "WARNING: recursion requested but not available" 193 | } 194 | 195 | @test "basic container - dns itself with long network name" { 196 | subnet_a=$(random_subnet 5) 197 | long_name="podman11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" 198 | create_config network_name="$long_name" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" aliases='"a1", "1a"' 199 | config_a1=$config 200 | ip_a1=$(echo "$config_a1" | jq -r .networks.$long_name.static_ips[0]) 201 | gw=$(echo "$config_a1" | jq -r .network_info.$long_name.subnets[0].gateway) 202 | create_container "$config_a1" 203 | a1_pid=$CONTAINER_NS_PID 204 | run_in_container_netns "$a1_pid" "dig" "+short" "aone" "@$gw" 205 | assert "$ip_a1" 206 | # Set recursion bit is already set if requested so output must not 207 | # contain unexpected warning. 208 | assert "$output" !~ "WARNING: recursion requested but not available" 209 | } 210 | 211 | @test "two containers on the same network" { 212 | # container a1 213 | subnet_a=$(random_subnet 5) 214 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" aliases='"a1", "1a"' 215 | config_a1="$config" 216 | a1_ip=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 217 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 218 | create_container "$config_a1" 219 | a1_pid=$CONTAINER_NS_PID 220 | 221 | # container a2 222 | create_config network_name="podman1" container_id=$(random_string 64) container_name="atwo" subnet="$subnet_a" aliases='"a2", "2a"' 223 | config_a2="$config" 224 | a2_ip=$(echo "$config_a2" | jq -r .networks.podman1.static_ips[0]) 225 | create_container "$config_a2" 226 | a2_pid="$CONTAINER_NS_PID" 227 | 228 | # Resolve container names to IPs 229 | dig "$a1_pid" "atwo" "$gw" 230 | assert "$a2_ip" 231 | # Set recursion bit 232 | assert "$output" !~ "WARNING: recursion requested but not available" 233 | dig "$a2_pid" "aone" "$gw" 234 | assert "$a1_ip" 235 | # Set recursion bit is already set if requested so output must not 236 | # contain unexpected warning. 237 | assert "$output" !~ "WARNING: recursion requested but not available" 238 | } 239 | 240 | # Internal network, meaning no DNS servers. 241 | # Hence all external requests must fail. 242 | @test "basic container - internal network has no DNS" { 243 | setup_dnsmasq 244 | 245 | subnet_a=$(random_subnet) 246 | create_config network_name="podman1" internal=true container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" custom_dns_server='"1.1.1.1","8.8.8.8"' aliases='"a1", "1a"' 247 | config_a1=$config 248 | # Network name is still recorded as podman1 249 | ip_a1=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 250 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 251 | create_container "$config_a1" 252 | a1_pid=$CONTAINER_NS_PID 253 | run_in_container_netns "$a1_pid" "dig" "+short" "aone" "@$gw" 254 | assert "$ip_a1" 255 | # Set recursion bit is already set if requested so output must not 256 | # contain unexpected warning. 257 | assert "$output" !~ "WARNING: recursion requested but not available" 258 | 259 | # Internal network means no DNS server means this should hard-fail 260 | expected_rc=1 run_in_container_netns "$a1_pid" "host" "-t" "ns" "$TEST_DOMAIN" "$gw" 261 | assert "$output" =~ "Host $TEST_DOMAIN not found" 262 | assert "$output" =~ "NXDOMAIN" 263 | } 264 | 265 | # Internal network, but this time with IPv6. Same result as above expected. 266 | @test "basic container - internal network has no DNS - ipv6" { 267 | setup_dnsmasq 268 | 269 | subnet_a=$(random_subnet 6) 270 | # Cloudflare and Google public anycast DNS v6 nameservers 271 | create_config network_name="podman1" internal=true container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" custom_dns_server='"2606:4700:4700::1111","2001:4860:4860::8888"' aliases='"a1", "1a"' 272 | config_a1=$config 273 | # Network name is still recorded as podman1 274 | ip_a1=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 275 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 276 | create_container "$config_a1" 277 | a1_pid=$CONTAINER_NS_PID 278 | run_in_container_netns "$a1_pid" "dig" "+short" "aone" "@$gw" "AAAA" 279 | assert "$ip_a1" 280 | # Set recursion bit is already set if requested so output must not 281 | # contain unexpected warning. 282 | assert "$output" !~ "WARNING: recursion requested but not available" 283 | 284 | # Internal network means no DNS server means this should hard-fail 285 | expected_rc=1 run_in_container_netns "$a1_pid" "host" "-t" "ns" "$TEST_DOMAIN" "$gw" 286 | assert "$output" =~ "Host $TEST_DOMAIN not found" 287 | assert "$output" =~ "NXDOMAIN" 288 | } 289 | 290 | @test "host dns on ipv6 link local" { 291 | # create a local interface with a link local ipv6 address 292 | # disable dad as it takes some time so the initial connection fails without it 293 | run_in_host_netns sysctl -w net.ipv6.conf.default.accept_dad=0 294 | run_in_host_netns ip link set lo up 295 | run_in_host_netns ip link add test type bridge 296 | run_in_host_netns ip link set test up 297 | run_in_host_netns ip -j addr 298 | link_local_addr=$(jq -r '.[] | select(.ifname=="test").addr_info[0].local' <<<"$output") 299 | 300 | # update our fake netns resolv.conf with the link local address as only nameserver 301 | echo "nameserver $link_local_addr%test" >"$AARDVARK_TMPDIR/resolv.conf" 302 | run_in_host_netns mount --bind "$AARDVARK_TMPDIR/resolv.conf" /etc/resolv.conf 303 | 304 | # launch dnsmasq to run a second local server with a unique name so we know custom_dns_server works 305 | run_in_host_netns dnsmasq --conf-file=/dev/null --pid-file="$AARDVARK_TMPDIR/dnsmasq2.pid" \ 306 | --except-interface=lo --listen-address="$link_local_addr" --bind-interfaces \ 307 | --address=/unique-name.local/192.168.0.1 --no-resolv --no-hosts 308 | HELPER_PID=$(cat $AARDVARK_TMPDIR/dnsmasq2.pid) 309 | 310 | subnet_a=$(random_subnet 5) 311 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" 312 | 313 | config_a1=$config 314 | ip_a1=$(echo "$config_a1" | jq -r .networks.podman1.static_ips[0]) 315 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 316 | create_container "$config_a1" 317 | a1_pid=$CONTAINER_NS_PID 318 | run_in_container_netns "$a1_pid" "dig" "aone" "@$gw" 319 | # check for TTL 0 here as well 320 | assert "$output" =~ "aone\.[[:space:]]*0[[:space:]]*IN[[:space:]]*A[[:space:]]*$ip_a1" 321 | # Set recursion bit is already set if requested so output must not 322 | # contain unexpected warning. 323 | assert "$output" !~ "WARNING: recursion requested but not available" 324 | 325 | run_in_container_netns "$a1_pid" "dig" "+short" "unique-name.local" "@$gw" 326 | # validate that we get the right ip 327 | assert "$output" == "192.168.0.1" 328 | # Set recursion bit is already set if requested so output must not 329 | # contain unexpected warning. 330 | assert "$output" !~ "WARNING: recursion requested but not available" 331 | } 332 | 333 | @test "nameservers updated when resolv.conf is modified" { 334 | setup_dnsmasq 335 | 336 | # Set up second dnsmasq server with different IP 337 | run_in_host_netns dnsmasq --conf-file=/dev/null --pid-file="$AARDVARK_TMPDIR/dnsmasq_second.pid" \ 338 | --except-interface=lo --listen-address=127.1.1.2 --bind-interfaces \ 339 | --address=/second-server.test/192.168.100.2 --no-resolv --no-hosts 340 | HELPER_PID=$(cat $AARDVARK_TMPDIR/dnsmasq_second.pid) 341 | 342 | subnet_a=$(random_subnet 5) 343 | create_config network_name="podman1" container_id=$(random_string 64) container_name="aone" subnet="$subnet_a" 344 | config_a1=$config 345 | gw=$(echo "$config_a1" | jq -r .network_info.podman1.subnets[0].gateway) 346 | create_container "$config_a1" 347 | a1_pid=$CONTAINER_NS_PID 348 | 349 | # Resolve using the first DNS server 350 | run_in_container_netns "$a1_pid" "dig" "+short" "testname" "@$gw" 351 | assert "$output" == "198.51.100.1" "should resolve using first DNS server" 352 | 353 | # Cannot resolve second server's domain yet 354 | expected_rc=1 run_in_container_netns "$a1_pid" "host" "-t" "a" "second-server.test" "$gw" 355 | assert "$output" =~ "not found" "should not resolve second server's domain initially" 356 | 357 | # Update resolv.conf to point to second DNS server 358 | echo "nameserver 127.1.1.2" > "$AARDVARK_TMPDIR/resolv.conf" 359 | 360 | retries=20 361 | while [[ $retries -gt 0 ]]; do 362 | expected_rc="?" run_in_container_netns "$a1_pid" "host" "-t" "a" "second-server.test" "$gw" 363 | if [[ $status -eq 0 ]]; then 364 | break 365 | fi 366 | sleep 0.5 367 | retries=$((retries -1)) 368 | done 369 | 370 | # Resolve using the second DNS server 371 | run_in_container_netns "$a1_pid" "dig" "+short" "second-server.test" "@$gw" 372 | assert "$output" == "192.168.100.2" "should resolve using second DNS server after resolv.conf change" 373 | } 374 | -------------------------------------------------------------------------------- /src/dns/coredns.rs: -------------------------------------------------------------------------------- 1 | use crate::backend::DNSBackend; 2 | use crate::error::AardvarkResult; 3 | use arc_swap::ArcSwap; 4 | use arc_swap::Guard; 5 | use futures_util::StreamExt; 6 | use futures_util::TryStreamExt; 7 | use hickory_client::{ 8 | client::Client, proto::rr::rdata, proto::rr::Name, proto::xfer::SerialMessage, 9 | }; 10 | use hickory_proto::{ 11 | op::{Message, MessageType, ResponseCode}, 12 | rr::{RData, Record, RecordType}, 13 | runtime::{iocompat::AsyncIoTokioAsStd, TokioRuntimeProvider}, 14 | tcp::{TcpClientStream, TcpStream}, 15 | udp::{UdpClientStream, UdpStream}, 16 | xfer::{dns_handle::DnsHandle, BufDnsStreamHandle, DnsRequest}, 17 | DnsStreamHandle, 18 | }; 19 | use log::{debug, error, trace, warn}; 20 | use std::io::Error; 21 | use std::net::{IpAddr, SocketAddr}; 22 | use std::sync::Arc; 23 | use std::sync::Mutex; 24 | use std::time::Duration; 25 | use tokio::net::TcpListener; 26 | use tokio::net::UdpSocket; 27 | 28 | const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); 29 | 30 | pub const DNS_PORT: u16 = 53; 31 | 32 | pub struct CoreDns { 33 | rx: flume::Receiver<()>, // kill switch receiver 34 | inner: CoreDnsData, 35 | } 36 | 37 | #[derive(Clone)] 38 | struct CoreDnsData { 39 | network_name: String, // raw network name 40 | backend: &'static ArcSwap, // server's data store 41 | no_proxy: bool, // do not forward to external resolvers 42 | nameservers: Arc>>, // host nameservers from resolv.conf 43 | } 44 | 45 | enum Protocol { 46 | Udp, 47 | Tcp, 48 | } 49 | 50 | impl CoreDns { 51 | // Most of the arg can be removed in design refactor. 52 | // so dont create a struct for this now. 53 | pub fn new( 54 | network_name: String, 55 | backend: &'static ArcSwap, 56 | rx: flume::Receiver<()>, 57 | no_proxy: bool, 58 | nameservers: Arc>>, 59 | ) -> Self { 60 | CoreDns { 61 | rx, 62 | inner: CoreDnsData { 63 | network_name, 64 | backend, 65 | no_proxy, 66 | nameservers, 67 | }, 68 | } 69 | } 70 | 71 | pub async fn run( 72 | &self, 73 | udp_socket: UdpSocket, 74 | tcp_listener: TcpListener, 75 | ) -> AardvarkResult<()> { 76 | let address = udp_socket.local_addr()?; 77 | let (mut receiver, sender_original) = 78 | UdpStream::::with_bound(udp_socket, address); 79 | 80 | loop { 81 | tokio::select! { 82 | _ = self.rx.recv_async() => { 83 | break; 84 | }, 85 | v = receiver.next() => { 86 | let msg_received = match v { 87 | Some(value) => value, 88 | None => { 89 | // None received, nothing to process so continue 90 | debug!("None recevied from stream, continue the loop"); 91 | continue; 92 | } 93 | }; 94 | Self::process_message(&self.inner, msg_received, &sender_original, Protocol::Udp).await; 95 | }, 96 | res = tcp_listener.accept() => { 97 | match res { 98 | Ok((sock,addr)) => { 99 | tokio::spawn(Self::process_tcp_stream(self.inner.clone(), sock, addr)); 100 | } 101 | Err(e) => { 102 | error!("Failed to accept new tcp connection: {e}"); 103 | break; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | Ok(()) 110 | } 111 | 112 | async fn process_tcp_stream( 113 | data: CoreDnsData, 114 | stream: tokio::net::TcpStream, 115 | peer: SocketAddr, 116 | ) { 117 | let (mut hickory_stream, sender_original) = 118 | TcpStream::from_stream(AsyncIoTokioAsStd(stream), peer); 119 | 120 | loop { 121 | // It is possible for a client to keep the tcp socket open forever and never send any data, 122 | // we do not want this so add a 3s timeout then we close the socket. 123 | match tokio::time::timeout(Duration::from_secs(3), hickory_stream.next()).await { 124 | Ok(message) => match message { 125 | Some(msg) => { 126 | Self::process_message(&data, msg, &sender_original, Protocol::Tcp).await 127 | } 128 | // end of stream 129 | None => break, 130 | }, 131 | Err(_) => { 132 | debug!( 133 | "Tcp connection {peer} was cancelled after 3s as it took too long to receive message" 134 | ); 135 | break; 136 | } 137 | } 138 | } 139 | } 140 | 141 | async fn process_message( 142 | data: &CoreDnsData, 143 | msg_received: Result, 144 | sender_original: &BufDnsStreamHandle, 145 | proto: Protocol, 146 | ) { 147 | let msg = match msg_received { 148 | Ok(msg) => msg, 149 | Err(e) => { 150 | error!("Error parsing dns message {e:?}"); 151 | return; 152 | } 153 | }; 154 | let backend = data.backend.load(); 155 | let src_address = msg.addr(); 156 | let mut sender = sender_original.with_remote_addr(src_address); 157 | let (request_name, record_type, mut req) = match parse_dns_msg(msg) { 158 | Some((name, record_type, req)) => (name, record_type, req), 159 | _ => { 160 | error!("None received while parsing dns message, this is not expected server will ignore this message"); 161 | return; 162 | } 163 | }; 164 | let request_name_string = request_name.to_string(); 165 | 166 | // Create debug and trace info for key parameters. 167 | trace!("server network name: {:?}", data.network_name); 168 | debug!("request source address: {src_address:?}"); 169 | trace!("requested record type: {record_type:?}"); 170 | debug!( 171 | "checking if backend has entry for: {:?}", 172 | &request_name_string 173 | ); 174 | trace!("server backend.name_mappings: {:?}", backend.name_mappings); 175 | trace!("server backend.ip_mappings: {:?}", backend.ip_mappings); 176 | 177 | match record_type { 178 | RecordType::PTR => { 179 | if let Some(msg) = reply_ptr(&request_name_string, &backend, src_address, &req) { 180 | reply(&mut sender, src_address, &msg); 181 | return; 182 | } 183 | // No match found, forwarding below. 184 | } 185 | RecordType::A | RecordType::AAAA => { 186 | if let Some(msg) = reply_ip( 187 | &request_name_string, 188 | &request_name, 189 | &data.network_name, 190 | record_type, 191 | &backend, 192 | src_address, 193 | &mut req, 194 | ) { 195 | reply(&mut sender, src_address, msg); 196 | return; 197 | } 198 | // No match found, forwarding below. 199 | } 200 | 201 | // TODO: handle MX here like docker does 202 | 203 | // We do not handle this request type so do nothing, 204 | // we forward the request to upstream resolvers below. 205 | _ => {} 206 | }; 207 | 208 | // are we allowed to forward? 209 | if data.no_proxy 210 | || backend.ctr_is_internal(&src_address.ip()) 211 | || request_name_string.ends_with(&backend.search_domain) 212 | { 213 | let mut nx_message = req.clone(); 214 | nx_message.set_response_code(ResponseCode::NXDomain); 215 | reply(&mut sender, src_address, &nx_message); 216 | } else { 217 | debug!( 218 | "Forwarding dns request for {} type: {}", 219 | &request_name_string, record_type 220 | ); 221 | let mut nameservers = Vec::new(); 222 | // Add resolvers configured for container 223 | if let Some(Some(dns_servers)) = backend.ctr_dns_server.get(&src_address.ip()) { 224 | for dns_server in dns_servers.iter() { 225 | nameservers.push(SocketAddr::new(*dns_server, DNS_PORT)); 226 | } 227 | // Add network scoped resolvers only if container specific resolvers were not configured 228 | } else if let Some(network_dns_servers) = 229 | backend.get_network_scoped_resolvers(&src_address.ip()) 230 | { 231 | for dns_server in network_dns_servers.iter() { 232 | nameservers.push(SocketAddr::new(*dns_server, DNS_PORT)); 233 | } 234 | } 235 | // Use host resolvers if no custom resolvers are set for the container. 236 | if nameservers.is_empty() { 237 | nameservers.clone_from(&data.nameservers.lock().expect("lock nameservers")); 238 | } 239 | 240 | match proto { 241 | Protocol::Udp => { 242 | tokio::spawn(Self::forward_to_servers( 243 | nameservers, 244 | sender, 245 | src_address, 246 | req, 247 | proto, 248 | )); 249 | } 250 | Protocol::Tcp => { 251 | // we already spawned a new future when we read the message so there is no need to spawn another one 252 | Self::forward_to_servers(nameservers, sender, src_address, req, proto).await; 253 | } 254 | } 255 | } 256 | } 257 | 258 | async fn forward_to_servers( 259 | nameservers: Vec, 260 | mut sender: BufDnsStreamHandle, 261 | src_address: SocketAddr, 262 | req: Message, 263 | proto: Protocol, 264 | ) { 265 | let mut timeout = DEFAULT_TIMEOUT; 266 | // Remember do not divide by 0. 267 | if !nameservers.is_empty() { 268 | timeout = Duration::from_secs(5) / nameservers.len() as u32 269 | } 270 | // forward dns request to hosts's /etc/resolv.conf 271 | for addr in nameservers { 272 | let (client, handle) = match proto { 273 | Protocol::Udp => { 274 | let stream = UdpClientStream::builder(addr, TokioRuntimeProvider::default()) 275 | .with_timeout(Some(timeout)) 276 | .build(); 277 | let (cl, bg) = match Client::connect(stream).await { 278 | Ok(a) => a, 279 | Err(e) => { 280 | debug!("Failed to connect to {addr}: {e}"); 281 | continue; 282 | } 283 | }; 284 | let handle = tokio::spawn(bg); 285 | (cl, handle) 286 | } 287 | Protocol::Tcp => { 288 | let (stream, sender) = TcpClientStream::new( 289 | addr, 290 | None, 291 | Some(timeout), 292 | TokioRuntimeProvider::default(), 293 | ); 294 | //let (stream, sender) = TcpClientStream::< 295 | // AsyncIoTokioAsStd, 296 | //>::with_timeout(addr, timeout); 297 | let (cl, bg) = match Client::with_timeout(stream, sender, timeout, None).await { 298 | Ok(a) => a, 299 | Err(e) => { 300 | debug!("Failed to connect to {addr}: {e}"); 301 | continue; 302 | } 303 | }; 304 | let handle = tokio::spawn(bg); 305 | (cl, handle) 306 | } 307 | }; 308 | 309 | if let Some(resp) = forward_dns_req(client, req.clone()).await { 310 | if reply(&mut sender, src_address, &resp).is_some() { 311 | // request resolved from following resolver so 312 | // break and don't try other resolvers 313 | break; 314 | } 315 | } 316 | handle.abort(); 317 | } 318 | } 319 | } 320 | 321 | fn reply(sender: &mut BufDnsStreamHandle, socket_addr: SocketAddr, msg: &Message) -> Option<()> { 322 | let id = msg.id(); 323 | let mut msg_mut = msg.clone(); 324 | msg_mut.set_message_type(MessageType::Response); 325 | // If `RD` is set and `RA` is false set `RA`. 326 | if msg.recursion_desired() && !msg.recursion_available() { 327 | msg_mut.set_recursion_available(true); 328 | } 329 | let response = SerialMessage::new(msg_mut.to_vec().ok()?, socket_addr); 330 | 331 | match sender.send(response) { 332 | Ok(_) => { 333 | debug!("[{id}] success reponse"); 334 | } 335 | Err(e) => { 336 | error!("[{id}] fail response: {e:?}"); 337 | } 338 | } 339 | 340 | Some(()) 341 | } 342 | 343 | fn parse_dns_msg(body: SerialMessage) -> Option<(Name, RecordType, Message)> { 344 | match Message::from_vec(body.bytes()) { 345 | Ok(msg) => { 346 | let mut name = Name::default(); 347 | let mut record_type: RecordType = RecordType::A; 348 | 349 | let parsed_msg = format!( 350 | "[{}] parsed message body: {} edns: {}", 351 | msg.id(), 352 | msg.queries() 353 | .first() 354 | .map(|q| { 355 | name = q.name().clone(); 356 | record_type = q.query_type(); 357 | 358 | format!("{} {} {}", q.name(), q.query_type(), q.query_class(),) 359 | }) 360 | .unwrap_or_else(Default::default,), 361 | msg.extensions().is_some(), 362 | ); 363 | 364 | debug!("parsed message {parsed_msg:?}"); 365 | 366 | Some((name, record_type, msg)) 367 | } 368 | Err(e) => { 369 | warn!("Failed while parsing message: {e}"); 370 | None 371 | } 372 | } 373 | } 374 | 375 | async fn forward_dns_req(cl: Client, message: Message) -> Option { 376 | let req = DnsRequest::new(message, Default::default()); 377 | let id = req.id(); 378 | 379 | match cl.send(req).try_next().await { 380 | Ok(Some(response)) => { 381 | for answer in response.answers() { 382 | debug!( 383 | "{} {} {} {} => {:#?}", 384 | id, 385 | answer.name(), 386 | answer.record_type(), 387 | answer.dns_class(), 388 | answer.data(), 389 | ); 390 | } 391 | let mut response_message = response.into_message(); 392 | response_message.set_id(id); 393 | Some(response_message) 394 | } 395 | Ok(None) => { 396 | error!("{id} dns request got empty response"); 397 | None 398 | } 399 | Err(e) => { 400 | error!("{id} dns request failed: {e}"); 401 | None 402 | } 403 | } 404 | } 405 | 406 | fn reply_ptr( 407 | name: &str, 408 | backend: &Guard>, 409 | src_address: SocketAddr, 410 | req: &Message, 411 | ) -> Option { 412 | let ptr_lookup_ip: String; 413 | // Are we IPv4 or IPv6? 414 | 415 | match name.strip_suffix(".in-addr.arpa.") { 416 | Some(n) => ptr_lookup_ip = n.split('.').rev().collect::>().join("."), 417 | None => { 418 | // not ipv4 419 | match name.strip_suffix(".ip6.arpa.") { 420 | Some(n) => { 421 | // ipv6 string is 39 chars max 422 | let mut tmp_ip = String::with_capacity(40); 423 | for (i, c) in n.split('.').rev().enumerate() { 424 | tmp_ip.push_str(c); 425 | // insert colon after 4 hex chars but not at the end 426 | if i % 4 == 3 && i < 31 { 427 | tmp_ip.push(':'); 428 | } 429 | } 430 | ptr_lookup_ip = tmp_ip; 431 | } 432 | // neither ipv4 or ipv6, something we do not understand 433 | None => return None, 434 | } 435 | } 436 | } 437 | 438 | trace!("Performing reverse lookup for ip: {}", &ptr_lookup_ip); 439 | 440 | // We should probably log malformed queries, but for now if-let should be fine. 441 | if let Ok(lookup_ip) = ptr_lookup_ip.parse() { 442 | if let Some(reverse_lookup) = backend.reverse_lookup(&src_address.ip(), &lookup_ip) { 443 | let mut req_clone = req.clone(); 444 | for entry in reverse_lookup { 445 | if let Ok(answer) = Name::from_ascii(format!("{entry}.")) { 446 | let record = Record::::from_rdata( 447 | Name::from_str_relaxed(name).unwrap_or_default(), 448 | 0, 449 | RData::PTR(rdata::PTR(answer)), 450 | ); 451 | req_clone.add_answer(record); 452 | } 453 | } 454 | return Some(req_clone); 455 | } 456 | }; 457 | None 458 | } 459 | 460 | fn reply_ip<'a>( 461 | name: &str, 462 | request_name: &Name, 463 | network_name: &str, 464 | record_type: RecordType, 465 | backend: &Guard>, 466 | src_address: SocketAddr, 467 | req: &'a mut Message, 468 | ) -> Option<&'a Message> { 469 | // attempt intra network resolution 470 | let resolved_ip_list = backend.lookup(&src_address.ip(), network_name, name)?; 471 | 472 | if record_type == RecordType::A { 473 | for record_addr in resolved_ip_list { 474 | if let IpAddr::V4(ipv4) = record_addr { 475 | // Set TTL to 0 which means client should not cache it. 476 | // Containers can be be restarted with a different ip at any time so allowing 477 | // caches here doesn't make much sense given the server is local and queries 478 | // should be fast enough anyway. 479 | let record = 480 | Record::::from_rdata(request_name.clone(), 0, RData::A(rdata::A(ipv4))); 481 | req.add_answer(record); 482 | } 483 | } 484 | } else if record_type == RecordType::AAAA { 485 | for record_addr in resolved_ip_list { 486 | if let IpAddr::V6(ipv6) = record_addr { 487 | let record = Record::::from_rdata( 488 | request_name.clone(), 489 | 0, 490 | RData::AAAA(rdata::AAAA(ipv6)), 491 | ); 492 | req.add_answer(record); 493 | } 494 | } 495 | } 496 | Some(req) 497 | } 498 | -------------------------------------------------------------------------------- /test/helpers.bash: -------------------------------------------------------------------------------- 1 | # -*- bash -*- 2 | 3 | # Netavark binary to run 4 | NETAVARK=${NETAVARK:-/usr/libexec/podman/netavark} 5 | 6 | TESTSDIR=${TESTSDIR:-$(dirname ${BASH_SOURCE})} 7 | 8 | AARDVARK=${AARDVARK:-$TESTSDIR/../bin/aardvark-dns} 9 | 10 | # export RUST_BACKTRACE so that we get a helpful stack trace 11 | export RUST_BACKTRACE=full 12 | 13 | TEST_DOMAIN=example.podman.io 14 | 15 | HOST_NS_PID= 16 | CONTAINER_NS_PID= 17 | 18 | CONTAINER_CONFIGS=() 19 | CONTAINER_NS_PIDS=() 20 | 21 | #### Functions below are taken from podman and buildah and adapted to netavark. 22 | 23 | ################ 24 | # run_helper # Invoke args, with timeout, using BATS 'run' 25 | ################ 26 | # 27 | # Second, we use 'timeout' to abort (with a diagnostic) if something 28 | # takes too long; this is preferable to a CI hang. 29 | # 30 | # Third, we log the command run and its output. This doesn't normally 31 | # appear in BATS output, but it will if there's an error. 32 | # 33 | # Next, we check exit status. Since the normal desired code is 0, 34 | # that's the default; but the expected_rc var can override: 35 | # 36 | # expected_rc=125 run_helper nonexistent-subcommand 37 | # expected_rc=? run_helper some-other-command # let our caller check status 38 | # 39 | # Since we use the BATS 'run' mechanism, $output and $status will be 40 | # defined for our caller. 41 | # 42 | function run_helper() { 43 | # expected_rc if unset set default to 0 44 | expected_rc="${expected_rc-0}" 45 | if [ "$expected_rc" == "?" ]; then 46 | expected_rc= 47 | fi 48 | # Remember command args, for possible use in later diagnostic messages 49 | MOST_RECENT_COMMAND="$*" 50 | 51 | # stdout is only emitted upon error; this echo is to help a debugger 52 | echo "$_LOG_PROMPT $*" 53 | 54 | # BATS hangs if a subprocess remains and keeps FD 3 open; this happens 55 | # if a process crashes unexpectedly without cleaning up subprocesses. 56 | run timeout --foreground -v --kill=10 10 "$@" 3>&- 57 | # without "quotes", multiple lines are glommed together into one 58 | if [ -n "$output" ]; then 59 | echo "$output" 60 | fi 61 | if [ "$status" -ne 0 ]; then 62 | echo -n "[ rc=$status " 63 | if [ -n "$expected_rc" ]; then 64 | if [ "$status" -eq "$expected_rc" ]; then 65 | echo -n "(expected) " 66 | else 67 | echo -n "(** EXPECTED $expected_rc **) " 68 | fi 69 | fi 70 | echo "]" 71 | fi 72 | 73 | if [ "$status" -eq 124 ]; then 74 | if expr "$output" : ".*timeout: sending" >/dev/null; then 75 | # It's possible for a subtest to _want_ a timeout 76 | if [[ "$expected_rc" != "124" ]]; then 77 | echo "*** TIMED OUT ***" 78 | false 79 | fi 80 | fi 81 | fi 82 | 83 | if [ -n "$expected_rc" ]; then 84 | if [ "$status" -ne "$expected_rc" ]; then 85 | die "exit code is $status; expected $expected_rc" 86 | fi 87 | fi 88 | 89 | # unset 90 | unset expected_rc 91 | } 92 | 93 | ######### 94 | # die # Abort with helpful message 95 | ######### 96 | function die() { 97 | # FIXME: handle multi-line output 98 | echo "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" >&2 99 | echo "#| FAIL: $*" >&2 100 | echo "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" >&2 101 | false 102 | } 103 | ############ 104 | # assert # Compare actual vs expected string; fail if mismatch 105 | ############ 106 | # 107 | # Compares string (default: $output) against the given string argument. 108 | # By default we do an exact-match comparison against $output, but there 109 | # are two different ways to invoke us, each with an optional description: 110 | # 111 | # xpect "EXPECT" [DESCRIPTION] 112 | # xpect "RESULT" "OP" "EXPECT" [DESCRIPTION] 113 | # 114 | # The first form (one or two arguments) does an exact-match comparison 115 | # of "$output" against "EXPECT". The second (three or four args) compares 116 | # the first parameter against EXPECT, using the given OPerator. If present, 117 | # DESCRIPTION will be displayed on test failure. 118 | # 119 | # Examples: 120 | # 121 | # xpect "this is exactly what we expect" 122 | # xpect "${lines[0]}" =~ "^abc" "first line begins with abc" 123 | # 124 | function assert() { 125 | local actual_string="$output" 126 | local operator='==' 127 | local expect_string="$1" 128 | local testname="$2" 129 | 130 | case "${#*}" in 131 | 0) die "Internal error: 'assert' requires one or more arguments" ;; 132 | 1 | 2) ;; 133 | 3 | 4) 134 | actual_string="$1" 135 | operator="$2" 136 | expect_string="$3" 137 | testname="$4" 138 | ;; 139 | *) die "Internal error: too many arguments to 'assert'" ;; 140 | esac 141 | 142 | # Comparisons. 143 | # Special case: there is no !~ operator, so fake it via '! x =~ y' 144 | local not= 145 | local actual_op="$operator" 146 | if [[ $operator == '!~' ]]; then 147 | not='!' 148 | actual_op='=~' 149 | fi 150 | if [[ $operator == '=' || $operator == '==' ]]; then 151 | # Special case: we can't use '=' or '==' inside [[ ... ]] because 152 | # the right-hand side is treated as a pattern... and '[xy]' will 153 | # not compare literally. There seems to be no way to turn that off. 154 | if [ "$actual_string" = "$expect_string" ]; then 155 | return 156 | fi 157 | elif [[ $operator == '!=' ]]; then 158 | # Same special case as above 159 | if [ "$actual_string" != "$expect_string" ]; then 160 | return 161 | fi 162 | else 163 | if eval "[[ $not \$actual_string $actual_op \$expect_string ]]"; then 164 | return 165 | elif [ $? -gt 1 ]; then 166 | die "Internal error: could not process 'actual' $operator 'expect'" 167 | fi 168 | fi 169 | 170 | # Test has failed. Get a descriptive test name. 171 | if [ -z "$testname" ]; then 172 | testname="${MOST_RECENT_BUILDAH_COMMAND:-[no test name given]}" 173 | fi 174 | 175 | # Display optimization: the typical case for 'expect' is an 176 | # exact match ('='), but there are also '=~' or '!~' or '-ge' 177 | # and the like. Omit the '=' but show the others; and always 178 | # align subsequent output lines for ease of comparison. 179 | local op='' 180 | local ws='' 181 | if [ "$operator" != '==' ]; then 182 | op="$operator " 183 | ws=$(printf "%*s" ${#op} "") 184 | fi 185 | 186 | # This is a multi-line message, which may in turn contain multi-line 187 | # output, so let's format it ourself, readably 188 | local actual_split 189 | IFS=$'\n' read -rd '' -a actual_split <<<"$actual_string" || true 190 | printf "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" >&2 191 | printf "#| FAIL: %s\n" "$testname" >&2 192 | printf "#| expected: %s'%s'\n" "$op" "$expect_string" >&2 193 | printf "#| actual: %s'%s'\n" "$ws" "${actual_split[0]}" >&2 194 | local line 195 | for line in "${actual_split[@]:1}"; do 196 | printf "#| > %s'%s'\n" "$ws" "$line" >&2 197 | done 198 | printf "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" >&2 199 | false 200 | } 201 | 202 | ################# 203 | # assert_json # Compare actual json vs expected string; fail if mismatch 204 | ################# 205 | # assert_json works like assert except that it accepts one extra parameter, 206 | # the jq query string. 207 | # There are two different ways to invoke us, each with an optional description: 208 | # 209 | # xpect "JQ_QUERY" "EXPECT" [DESCRIPTION] 210 | # xpect "JSON_STRING" "JQ_QUERY" "OP" "EXPECT" [DESCRIPTION] 211 | # Important this function will overwrite $output, so if you need to use the value 212 | # more than once you need to safe it in another variable. 213 | function assert_json() { 214 | local actual_json="$output" 215 | local operator='==' 216 | local jq_query="$1" 217 | local expect_string="$2" 218 | local testname="$3" 219 | 220 | case "${#*}" in 221 | 0 | 1) die "Internal error: 'assert_json' requires two or more arguments" ;; 222 | 2 | 3) ;; 223 | 4 | 5) 224 | actual_json="$1" 225 | jq_query="$2" 226 | operator="$3" 227 | expect_string="$4" 228 | testname="$5" 229 | ;; 230 | *) die "Internal error: too many arguments to 'assert_json'" ;; 231 | esac 232 | run_helper jq -r "$jq_query" <<<"$actual_json" 233 | assert "$output" "$operator" "$expect_string" "$testname" 234 | } 235 | 236 | ################### 237 | # random_string # Pseudorandom alphanumeric string of given length 238 | ################### 239 | function random_string() { 240 | local length=${1:-10} 241 | head /dev/urandom | tr -dc a-zA-Z0-9 | head -c$length 242 | } 243 | 244 | ################### 245 | # random_subnet # generate a random private subnet 246 | ################### 247 | # 248 | # by default it will return a 10.x.x.0/24 ipv4 subnet 249 | # if "6" is given as first argument it will return a "fdx:x:x:x::/64" ipv6 subnet 250 | function random_subnet() { 251 | if [[ "$1" == "6" ]]; then 252 | printf "fd%02x:%x:%x:%x::/64" $((RANDOM % 256)) $((RANDOM % 65535)) $((RANDOM % 65535)) $((RANDOM % 65535)) 253 | else 254 | printf "10.%d.%d.0/24" $((RANDOM % 256)) $((RANDOM % 256)) 255 | fi 256 | } 257 | 258 | ######################### 259 | # random_ip_in_subnet # get a random from a given subnet 260 | ######################### 261 | # the first arg must be an subnet created by random_subnet 262 | # otherwise this function might return an invalid ip 263 | function random_ip_in_subnet() { 264 | # first trim subnet 265 | local net_ip=${1%/*} 266 | local num= 267 | local add=$2 268 | # if ip has colon it is ipv6 269 | if [[ "$net_ip" == *":"* ]]; then 270 | num=$((RANDOM % 65533 )) 271 | # see below 272 | num=$((num - num % 10 + add + 2)) 273 | num=$(printf "%x" $num) 274 | else 275 | # if ipv4 we have to trim the final 0 276 | net_ip=${net_ip%0} 277 | # make sure to not get 0, 1 or 255 278 | num=$((RANDOM % 252)) 279 | # Avoid giving out duplicated ips if we are called more than once. 280 | # The caller needs to keep a counter because this is executed ina subshell so we cannot use global var here. 281 | # Basically subtract mod 10 then add the counter so we can never get a dup ip assuming counter < 10 which 282 | # should always be the case here. Add 2 to avoid using .0 .1 which have special meaning. 283 | num=$((num - num % 10 + add + 2)) 284 | fi 285 | printf "$net_ip%s" $num 286 | } 287 | 288 | ######################### 289 | # gateway_from_subnet # get the first ip from a given subnet 290 | ######################### 291 | # the first arg must be an subnet created by random_subnet 292 | # otherwise this function might return an invalid ip 293 | function gateway_from_subnet() { 294 | # first trim subnet 295 | local net_ip=${1%/*} 296 | # set first ip in network as gateway 297 | local num=1 298 | # if ip has dor it is ipv4 299 | if [[ "$net_ip" == *"."* ]]; then 300 | # if ipv4 we have to trim the final 0 301 | net_ip=${net_ip%0} 302 | fi 303 | printf "$net_ip%s" $num 304 | } 305 | 306 | function create_netns() { 307 | # create a new netns and mountns and run a sleep process to keep it alive 308 | # we have to redirect stdout/err to /dev/null otherwise bats will hang 309 | unshare -mn sleep inf &>/dev/null & 310 | pid=$! 311 | 312 | # we have to wait for unshare and check that we have a new ns before returning 313 | local timeout=2 314 | while [[ $timeout -gt 0 ]]; do 315 | if [ "$(readlink /proc/self/ns/net)" != "$(readlink /proc/$pid/ns/net)" ]; then 316 | echo $pid 317 | return 318 | fi 319 | sleep 1 320 | let timeout=$timeout-1 321 | done 322 | 323 | die "Timed out waiting for unshare new netns" 324 | } 325 | 326 | function get_container_netns_path() { 327 | echo /proc/$1/ns/net 328 | } 329 | 330 | ################ 331 | # run_netavark # Invoke $NETAVARK, with timeout, using BATS 'run' 332 | ################ 333 | # 334 | # This is the preferred mechanism for invoking netavark: first, it 335 | # it joins the test network namespace before it invokes $NETAVARK, 336 | # which may be 'netavark' or '/some/path/netavark'. 337 | function run_netavark() { 338 | run_in_host_netns $NETAVARK "--config" "$AARDVARK_TMPDIR" "-a" "$AARDVARK" "$@" 339 | } 340 | 341 | ################ 342 | # run_in_container_netns # Run args in container netns 343 | ################ 344 | # 345 | # first arg must be the container pid 346 | function run_in_container_netns() { 347 | con_pid=$1 348 | shift 349 | run_helper nsenter -n -t $con_pid "$@" 350 | } 351 | 352 | ################ 353 | # run_in_host_netns # Run args in host netns 354 | ################ 355 | # 356 | function run_in_host_netns() { 357 | run_helper nsenter -m -n -t $HOST_NS_PID "$@" 358 | } 359 | 360 | ################ 361 | # create_config# Creates a config netavark can use 362 | ################ 363 | # 364 | # The following arguments are supported, the order does not matter: 365 | # network_name=$network_name 366 | # container_id=$container_id 367 | # container_name=$container_name 368 | # subnet=$subnet specifies the network subnet 369 | # custom_dns_serve=$custom_dns_server 370 | # aliases=$aliases comma seperated container aliases for dns resolution. 371 | # internal={true,false} default is false 372 | function create_config() { 373 | local network_name="" 374 | local container_id="" 375 | local container_name="" 376 | local subnet="" 377 | local custom_dns_server 378 | local aliases="" 379 | local internal=false 380 | 381 | # parse arguments 382 | while [[ "$#" -gt 0 ]]; do 383 | IFS='=' read -r arg value <<<"$1" 384 | case "$arg" in 385 | network_name) 386 | network_name="$value" 387 | ;; 388 | container_id) 389 | container_id="$value" 390 | ;; 391 | container_name) 392 | container_name="$value" 393 | ;; 394 | subnet) 395 | subnet="$value" 396 | ;; 397 | custom_dns_server) 398 | custom_dns_server="$value" 399 | ;; 400 | aliases) 401 | aliases="$value" 402 | ;; 403 | internal) 404 | internal="$value" 405 | ;; 406 | *) die "unknown argument for '$arg' create_config" ;; 407 | esac 408 | shift 409 | done 410 | 411 | container_ip=$(random_ip_in_subnet $subnet $IP_COUNT) 412 | IP_COUNT=$((IP_COUNT + 1)) 413 | container_gw=$(gateway_from_subnet $subnet) 414 | subnets="{\"subnet\":\"$subnet\",\"gateway\":\"$container_gw\"}" 415 | 416 | create_network "$network_name" "$container_ip" "eth0" "$aliases" 417 | create_network_infos "$network_name" $(random_string 64) "$subnets" "$internal" 418 | 419 | read -r -d '\0' config <"$AARDVARK_TMPDIR/resolv.conf" 548 | run_in_host_netns mount --bind "$AARDVARK_TMPDIR/resolv.conf" /etc/resolv.conf 549 | } 550 | 551 | function basic_teardown() { 552 | # Now call netavark with all the configs and then kill the netns associated with it 553 | for i in "${!CONTAINER_CONFIGS[@]}"; do 554 | netavark_teardown $(get_container_netns_path "${CONTAINER_NS_PIDS[$i]}") "${CONTAINER_CONFIGS[$i]}" 555 | kill -9 "${CONTAINER_NS_PIDS[$i]}" 556 | done 557 | 558 | if [[ -n "$DNSMASQ_PID" ]]; then 559 | kill -9 $DNSMASQ_PID 560 | DNSMASQ_PID="" 561 | fi 562 | 563 | # Finally kill the host netns 564 | if [ ! -z "$HOST_NS_PID" ]; then 565 | echo "$HOST_NS_PID" 566 | kill -9 "$HOST_NS_PID" 567 | fi 568 | 569 | rm -fr "$AARDVARK_TMPDIR" 570 | } 571 | 572 | ################ 573 | # netavark_teardown# tears down a network 574 | ################ 575 | function netavark_teardown() { 576 | run_netavark teardown $1 <<<"$2" 577 | } 578 | 579 | function teardown() { 580 | basic_teardown 581 | } 582 | 583 | function dig() { 584 | # first arg is container_netns_pid 585 | # second arg is name 586 | # third arg is server addr 587 | run_in_container_netns "$1" "dig" "+short" "$2" "@$3" $4 588 | } 589 | 590 | function dig_reverse() { 591 | # first arg is container_netns_pid 592 | # second arg is the IP address 593 | # third arg is server addr 594 | run_in_container_netns "$1" "dig" "-x" "$2" "@$3" 595 | } 596 | 597 | function setup() { 598 | basic_host_setup 599 | } 600 | -------------------------------------------------------------------------------- /src/server/serve.rs: -------------------------------------------------------------------------------- 1 | use crate::backend::DNSBackend; 2 | use crate::config::constants::AARDVARK_PID_FILE; 3 | use crate::config::parse_configs; 4 | use crate::dns::coredns::CoreDns; 5 | use crate::dns::coredns::DNS_PORT; 6 | use crate::error::AardvarkError; 7 | use crate::error::AardvarkErrorList; 8 | use crate::error::AardvarkResult; 9 | use crate::error::AardvarkWrap; 10 | use arc_swap::ArcSwap; 11 | use log::{debug, error, info}; 12 | use nix::unistd::{self, dup2_stderr, dup2_stdin, dup2_stdout}; 13 | use std::collections::HashMap; 14 | use std::collections::HashSet; 15 | use std::env; 16 | use std::fs; 17 | use std::fs::OpenOptions; 18 | use std::hash::Hash; 19 | use std::io::Error; 20 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; 21 | use std::os::fd::OwnedFd; 22 | use std::sync::Arc; 23 | use std::sync::Mutex; 24 | use std::sync::OnceLock; 25 | use tokio::net::{TcpListener, UdpSocket}; 26 | use tokio::signal::unix::{signal, SignalKind}; 27 | use tokio::task::JoinHandle; 28 | 29 | use futures::StreamExt; 30 | use inotify::{EventStream, Inotify, WatchMask}; 31 | use std::fs::File; 32 | use std::io::prelude::*; 33 | use std::path::Path; 34 | use std::process; 35 | 36 | const RESOLV_CONF: &str = "/etc/resolv.conf"; 37 | 38 | type ThreadHandleMap = 39 | HashMap<(String, Ip), (flume::Sender<()>, JoinHandle>)>; 40 | 41 | pub fn create_pid(config_path: &str) -> AardvarkResult<()> { 42 | // before serving write its pid to _config_path so other process can notify 43 | // aardvark of data change. 44 | let path = Path::new(config_path).join(AARDVARK_PID_FILE); 45 | let mut pid_file = match File::create(path) { 46 | Err(err) => { 47 | return Err(AardvarkError::msg(format!( 48 | "Unable to get process pid: {err}" 49 | ))); 50 | } 51 | Ok(file) => file, 52 | }; 53 | 54 | let server_pid = process::id().to_string(); 55 | if let Err(err) = pid_file.write_all(server_pid.as_bytes()) { 56 | return Err(AardvarkError::msg(format!( 57 | "Unable to write pid to file: {err}" 58 | ))); 59 | } 60 | 61 | Ok(()) 62 | } 63 | 64 | #[tokio::main] 65 | pub async fn serve( 66 | config_path: &str, 67 | port: u16, 68 | filter_search_domain: &str, 69 | ready: OwnedFd, 70 | ) -> AardvarkResult<()> { 71 | let mut signals = signal(SignalKind::hangup())?; 72 | let no_proxy: bool = env::var("AARDVARK_NO_PROXY").is_ok(); 73 | 74 | let mut handles_v4 = HashMap::new(); 75 | let mut handles_v6 = HashMap::new(); 76 | let nameservers = Arc::new(Mutex::new(Vec::new())); 77 | 78 | read_config_and_spawn( 79 | config_path, 80 | port, 81 | filter_search_domain, 82 | &mut handles_v4, 83 | &mut handles_v6, 84 | nameservers.clone(), 85 | no_proxy, 86 | ) 87 | .await?; 88 | // We are ready now, this is far from perfect we should at least wait for the first bind 89 | // to work but this is not really possible with the current code flow and needs more changes. 90 | daemonize()?; 91 | let msg: [u8; 1] = [b'1']; 92 | unistd::write(&ready, &msg)?; 93 | drop(ready); 94 | 95 | // Setup inotify to monitor resolv.conf 96 | let mut event_stream = get_inotify_event_stream(); 97 | loop { 98 | tokio::select! { 99 | // Block until we receive a SIGHUP. 100 | _= signals.recv()=>{ 101 | debug!("Received SIGHUP"); 102 | if let Err(e) = read_config_and_spawn( 103 | config_path, 104 | port, 105 | filter_search_domain, 106 | &mut handles_v4, 107 | &mut handles_v6, 108 | nameservers.clone(), 109 | no_proxy, 110 | ) 111 | .await 112 | { 113 | // do not exit here, we just keep running even if something failed 114 | error!("{e}"); 115 | }; 116 | } 117 | // Block until resolv.conf is changed, monitored via inotify. Then reload nameservers 118 | _ = event_stream.as_mut().unwrap().next(), if event_stream.is_some() => { 119 | let upstream_resolvers = match get_upstream_resolvers() { 120 | Ok(ns) => ns, 121 | Err(err) => { 122 | error!("Failed to reload nameservers on change: {err}"); 123 | continue; 124 | } 125 | }; 126 | match nameservers.lock() { 127 | Ok(mut ns) => *ns = upstream_resolvers, 128 | Err(err) => { 129 | error!("Failed to reload nameservers, could not obtain lock: {err}"); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | /// # Ensure the expected DNS server threads are running 137 | /// 138 | /// Stop threads corresponding to listen IPs no longer in the configuration and start threads 139 | /// corresponding to listen IPs that were added. 140 | async fn stop_and_start_threads( 141 | port: u16, 142 | backend: &'static ArcSwap, 143 | listen_ips: HashMap>, 144 | thread_handles: &mut ThreadHandleMap, 145 | no_proxy: bool, 146 | nameservers: Arc>>, 147 | ) -> AardvarkResult<()> 148 | where 149 | Ip: Eq + Hash + Copy + Into + Send + 'static, 150 | { 151 | let mut expected_threads = HashSet::new(); 152 | for (network_name, listen_ip_list) in listen_ips { 153 | for ip in listen_ip_list { 154 | expected_threads.insert((network_name.clone(), ip)); 155 | } 156 | } 157 | 158 | // First we shut down any old threads that should no longer be running. This should be 159 | // done before starting new ones in case a listen IP was moved from being under one network 160 | // name to another. 161 | let to_shut_down: Vec<_> = thread_handles 162 | .keys() 163 | .filter(|k| !expected_threads.contains(k)) 164 | .cloned() 165 | .collect(); 166 | stop_threads(thread_handles, Some(to_shut_down)).await; 167 | 168 | // Then we start any new threads. 169 | let to_start: Vec<_> = expected_threads 170 | .iter() 171 | .filter(|k| !thread_handles.contains_key(*k)) 172 | .cloned() 173 | .collect(); 174 | 175 | let mut errors = AardvarkErrorList::new(); 176 | 177 | for (network_name, ip) in to_start { 178 | let (shutdown_tx, shutdown_rx) = flume::bounded(0); 179 | let network_name_ = network_name.clone(); 180 | let ns = nameservers.clone(); 181 | let addr = SocketAddr::new(ip.into(), port); 182 | let udp_sock = match UdpSocket::bind(addr).await { 183 | Ok(s) => s, 184 | Err(err) => { 185 | errors.push(AardvarkError::wrap( 186 | format!("failed to bind udp listener on {addr}"), 187 | err.into(), 188 | )); 189 | continue; 190 | } 191 | }; 192 | 193 | let tcp_sock = match TcpListener::bind(addr).await { 194 | Ok(s) => s, 195 | Err(err) => { 196 | errors.push(AardvarkError::wrap( 197 | format!("failed to bind tcp listener on {addr}"), 198 | err.into(), 199 | )); 200 | continue; 201 | } 202 | }; 203 | 204 | let handle = tokio::spawn(async move { 205 | start_dns_server( 206 | network_name_, 207 | udp_sock, 208 | tcp_sock, 209 | backend, 210 | shutdown_rx, 211 | no_proxy, 212 | ns, 213 | ) 214 | .await 215 | }); 216 | 217 | thread_handles.insert((network_name, ip), (shutdown_tx, handle)); 218 | } 219 | 220 | if errors.is_empty() { 221 | return Ok(()); 222 | } 223 | 224 | Err(AardvarkError::List(errors)) 225 | } 226 | 227 | /// # Stop DNS server threads 228 | /// 229 | /// If the `filter` parameter is `Some` only threads in the filter `Vec` will be stopped. 230 | async fn stop_threads( 231 | thread_handles: &mut ThreadHandleMap, 232 | filter: Option>, 233 | ) where 234 | Ip: Eq + Hash + Copy, 235 | { 236 | let mut handles = Vec::new(); 237 | 238 | let to_shut_down: Vec<_> = filter.unwrap_or_else(|| thread_handles.keys().cloned().collect()); 239 | 240 | for key in to_shut_down { 241 | let (tx, handle) = thread_handles.remove(&key).unwrap(); 242 | handles.push(handle); 243 | drop(tx); 244 | } 245 | 246 | for handle in handles { 247 | match handle.await { 248 | Ok(res) => { 249 | // result returned by the future, i.e. that actual 250 | // result from start_dns_server() 251 | if let Err(e) = res { 252 | error!("Error from dns server: {e}") 253 | } 254 | } 255 | // error from tokio itself 256 | Err(e) => error!("Error from dns server task: {e}"), 257 | } 258 | } 259 | } 260 | 261 | async fn start_dns_server( 262 | name: String, 263 | udp_socket: UdpSocket, 264 | tcp_socket: TcpListener, 265 | backend: &'static ArcSwap, 266 | rx: flume::Receiver<()>, 267 | no_proxy: bool, 268 | nameservers: Arc>>, 269 | ) -> AardvarkResult<()> { 270 | let server = CoreDns::new(name, backend, rx, no_proxy, nameservers); 271 | server 272 | .run(udp_socket, tcp_socket) 273 | .await 274 | .wrap("run dns server") 275 | } 276 | 277 | async fn read_config_and_spawn( 278 | config_path: &str, 279 | port: u16, 280 | filter_search_domain: &str, 281 | handles_v4: &mut ThreadHandleMap, 282 | handles_v6: &mut ThreadHandleMap, 283 | nameservers: Arc>>, 284 | no_proxy: bool, 285 | ) -> AardvarkResult<()> { 286 | let (conf, listen_ip_v4, listen_ip_v6) = 287 | parse_configs(config_path, filter_search_domain).wrap("unable to parse config")?; 288 | 289 | // We store the `DNSBackend` in an `ArcSwap` so we can replace it when the configuration is 290 | // reloaded. 291 | static DNSBACKEND: OnceLock> = OnceLock::new(); 292 | let backend = match DNSBACKEND.get() { 293 | Some(b) => { 294 | b.store(Arc::new(conf)); 295 | b 296 | } 297 | None => DNSBACKEND.get_or_init(|| ArcSwap::from(Arc::new(conf))), 298 | }; 299 | 300 | debug!("Successfully parsed config"); 301 | debug!("Listen v4 ip {listen_ip_v4:?}"); 302 | debug!("Listen v6 ip {listen_ip_v6:?}"); 303 | 304 | // kill server if listen_ip's are empty 305 | if listen_ip_v4.is_empty() && listen_ip_v6.is_empty() { 306 | info!("No configuration found stopping the sever"); 307 | 308 | let path = Path::new(config_path).join(AARDVARK_PID_FILE); 309 | if let Err(err) = fs::remove_file(path) { 310 | error!("failed to remove the pid file: {}", &err); 311 | process::exit(1); 312 | } 313 | 314 | // Gracefully stop all server threads first. 315 | stop_threads(handles_v4, None).await; 316 | stop_threads(handles_v6, None).await; 317 | 318 | process::exit(0); 319 | } 320 | 321 | let mut errors = AardvarkErrorList::new(); 322 | 323 | // get host nameservers 324 | let upstream_resolvers = match get_upstream_resolvers() { 325 | Ok(ns) => ns, 326 | Err(err) => { 327 | errors.push(AardvarkError::wrap( 328 | "failed to get upstream nameservers, dns forwarding will not work", 329 | err, 330 | )); 331 | Vec::new() 332 | } 333 | }; 334 | debug!("Using the following upstream servers: {upstream_resolvers:?}"); 335 | 336 | { 337 | // use new scope to only lock for a short time 338 | *nameservers.lock().expect("lock nameservers") = upstream_resolvers; 339 | } 340 | 341 | if let Err(err) = stop_and_start_threads( 342 | port, 343 | backend, 344 | listen_ip_v4, 345 | handles_v4, 346 | no_proxy, 347 | nameservers.clone(), 348 | ) 349 | .await 350 | { 351 | errors.push(err) 352 | }; 353 | 354 | if let Err(err) = stop_and_start_threads( 355 | port, 356 | backend, 357 | listen_ip_v6, 358 | handles_v6, 359 | no_proxy, 360 | nameservers, 361 | ) 362 | .await 363 | { 364 | errors.push(err) 365 | }; 366 | 367 | if errors.is_empty() { 368 | return Ok(()); 369 | } 370 | 371 | Err(AardvarkError::List(errors)) 372 | } 373 | 374 | // creates new session and put /dev/null on the stdio streams 375 | fn daemonize() -> Result<(), Error> { 376 | // remove any controlling terminals 377 | // but don't hardstop if this fails 378 | let _ = unsafe { libc::setsid() }; // check https://docs.rs/libc 379 | 380 | let dev_null = OpenOptions::new() 381 | .read(true) 382 | .write(true) 383 | .open("/dev/null") 384 | .map_err(|e| std::io::Error::new(e.kind(), format!("/dev/null: {e:#}")))?; 385 | // redirect stdout, stdin and stderr to /dev/null 386 | let _ = dup2_stdin(&dev_null); 387 | let _ = dup2_stdout(&dev_null); 388 | let _ = dup2_stderr(&dev_null); 389 | Ok(()) 390 | } 391 | 392 | // read /etc/resolv.conf and return all nameservers 393 | fn get_upstream_resolvers() -> AardvarkResult> { 394 | let mut f = File::open(RESOLV_CONF).wrap("open resolv.conf")?; 395 | let mut buf = String::with_capacity(4096); 396 | f.read_to_string(&mut buf).wrap("read resolv.conf")?; 397 | 398 | parse_resolv_conf(&buf) 399 | } 400 | 401 | fn get_inotify_event_stream() -> Option> { 402 | // Min buffer size is 272 (sizeof(struct inotify_event) + NAME_MAX + 1) 403 | let buffer = [0; 1024]; 404 | let inotify = match Inotify::init() { 405 | Ok(inotify) => inotify, 406 | Err(e) => { 407 | error!( 408 | "Failed to initialize inotify. Nameservers will not be updated on resolv.conf change: {e}" 409 | ); 410 | return None; 411 | } 412 | }; 413 | 414 | match inotify 415 | .watches() 416 | .add(RESOLV_CONF, WatchMask::CLOSE_WRITE | WatchMask::MOVED_TO) 417 | { 418 | Ok(_) => match inotify.into_event_stream(buffer) { 419 | Ok(stream) => return Some(stream), 420 | Err(e) => error!("Failed to stream inotify events. Nameservers will not be updated on resolv.conf change: {e}"), 421 | }, 422 | Err(e) => error!("Failed to add watch on {RESOLV_CONF}. Nameservers will not be updated on resolv.conf change: {e}"), 423 | } 424 | None 425 | } 426 | 427 | fn parse_resolv_conf(content: &str) -> AardvarkResult> { 428 | let mut nameservers = Vec::new(); 429 | for line in content.split('\n') { 430 | // split of comments 431 | let line = match line.split_once(['#', ';']) { 432 | Some((f, _)) => f, 433 | None => line, 434 | }; 435 | let mut line_parts = line.split_whitespace(); 436 | match line_parts.next() { 437 | Some(first) => { 438 | if first == "nameserver" { 439 | if let Some(ip) = line_parts.next() { 440 | // split of zone, we do not support the link local zone currently with ipv6 addresses 441 | let mut scope = None; 442 | let ip = match ip.split_once("%") { 443 | Some((ip, scope_name)) => { 444 | // allow both interface names or static ids 445 | let id = match scope_name.parse() { 446 | Ok(id) => id, 447 | Err(_) => nix::net::if_::if_nametoindex(scope_name) 448 | .wrap("resolve scope id")?, 449 | }; 450 | 451 | scope = Some(id); 452 | ip 453 | } 454 | None => ip, 455 | }; 456 | let ip = ip.parse().wrap(ip)?; 457 | 458 | let addr = match ip { 459 | IpAddr::V4(ip) => { 460 | if scope.is_some() { 461 | return Err(AardvarkError::msg( 462 | "scope id not supported for ipv4 address", 463 | )); 464 | } 465 | SocketAddr::V4(SocketAddrV4::new(ip, DNS_PORT)) 466 | } 467 | IpAddr::V6(ip) => SocketAddr::V6(SocketAddrV6::new( 468 | ip, 469 | DNS_PORT, 470 | 0, 471 | scope.unwrap_or(0), 472 | )), 473 | }; 474 | 475 | nameservers.push(addr); 476 | } 477 | } 478 | } 479 | None => continue, 480 | } 481 | } 482 | 483 | // we do not have time to try many nameservers anyway so only use the first three 484 | nameservers.truncate(3); 485 | Ok(nameservers) 486 | } 487 | 488 | #[cfg(test)] 489 | mod tests { 490 | use super::*; 491 | 492 | const IP_1_1_1_1: SocketAddr = 493 | SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(1, 1, 1, 1), DNS_PORT)); 494 | const IP_1_1_1_2: SocketAddr = 495 | SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(1, 1, 1, 2), DNS_PORT)); 496 | const IP_1_1_1_3: SocketAddr = 497 | SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(1, 1, 1, 3), DNS_PORT)); 498 | 499 | /// fdfd:733b:dc3:220b::2 500 | const IP_FDFD_733B_DC3_220B_2: SocketAddr = SocketAddr::V6(SocketAddrV6::new( 501 | Ipv6Addr::new(0xfdfd, 0x733b, 0xdc3, 0x220b, 0, 0, 0, 2), 502 | DNS_PORT, 503 | 0, 504 | 0, 505 | )); 506 | 507 | /// fe80::1%lo 508 | const IP_FE80_1: SocketAddr = SocketAddr::V6(SocketAddrV6::new( 509 | Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1), 510 | DNS_PORT, 511 | 0, 512 | 1, 513 | )); 514 | 515 | #[test] 516 | fn test_parse_resolv_conf() { 517 | let res = parse_resolv_conf("nameserver 1.1.1.1").expect("failed to parse"); 518 | assert_eq!(res, vec![IP_1_1_1_1]); 519 | } 520 | 521 | #[test] 522 | fn test_parse_resolv_conf_multiple() { 523 | let res = parse_resolv_conf( 524 | "nameserver 1.1.1.1 525 | nameserver 1.1.1.2 526 | nameserver 1.1.1.3", 527 | ) 528 | .expect("failed to parse"); 529 | assert_eq!(res, vec![IP_1_1_1_1, IP_1_1_1_2, IP_1_1_1_3]); 530 | } 531 | 532 | #[test] 533 | fn test_parse_resolv_conf_search_and_options() { 534 | let res = parse_resolv_conf( 535 | "nameserver 1.1.1.1 536 | nameserver 1.1.1.2 537 | nameserver 1.1.1.3 538 | search test.podman 539 | options rotate", 540 | ) 541 | .expect("failed to parse"); 542 | assert_eq!(res, vec![IP_1_1_1_1, IP_1_1_1_2, IP_1_1_1_3]); 543 | } 544 | #[test] 545 | fn test_parse_resolv_conf_with_comment() { 546 | let res = parse_resolv_conf( 547 | "# mytest 548 | nameserver 1.1.1.1 # space 549 | nameserver 1.1.1.2#nospace 550 | #leading spaces 551 | nameserver 1.1.1.3", 552 | ) 553 | .expect("failed to parse"); 554 | assert_eq!(res, vec![IP_1_1_1_1, IP_1_1_1_2, IP_1_1_1_3]); 555 | } 556 | 557 | #[test] 558 | fn test_parse_resolv_conf_with_invalid_content() { 559 | let res = parse_resolv_conf( 560 | "hey I am not known 561 | nameserver 1.1.1.1 562 | nameserver 1.1.1.2 somestuff here 563 | abc 564 | nameserver 1.1.1.3", 565 | ) 566 | .expect("failed to parse"); 567 | assert_eq!(res, vec![IP_1_1_1_1, IP_1_1_1_2, IP_1_1_1_3]); 568 | } 569 | 570 | #[test] 571 | fn test_parse_resolv_conf_truncate_to_three() { 572 | let res = parse_resolv_conf( 573 | "nameserver 1.1.1.1 574 | nameserver 1.1.1.2 575 | nameserver 1.1.1.3 576 | nameserver 1.1.1.4 577 | nameserver 1.2.3.4", 578 | ) 579 | .expect("failed to parse"); 580 | assert_eq!(res, vec![IP_1_1_1_1, IP_1_1_1_2, IP_1_1_1_3]); 581 | } 582 | 583 | #[test] 584 | fn test_parse_resolv_conf_with_invalid_ip() { 585 | parse_resolv_conf("nameserver abc").expect_err("invalid ip must error"); 586 | } 587 | 588 | #[test] 589 | fn test_parse_resolv_ipv6() { 590 | let res = parse_resolv_conf( 591 | "nameserver fdfd:733b:dc3:220b::2 592 | nameserver 1.1.1.2", 593 | ) 594 | .expect("failed to parse"); 595 | assert_eq!(res, vec![IP_FDFD_733B_DC3_220B_2, IP_1_1_1_2]); 596 | } 597 | 598 | #[test] 599 | fn test_parse_resolv_ipv6_link_local_zone() { 600 | // Using lo here because we know that will always be id 1 and we 601 | // cannot assume any other interface name here. 602 | let res = parse_resolv_conf( 603 | "nameserver fe80::1%lo 604 | ", 605 | ) 606 | .expect("failed to parse"); 607 | assert_eq!(res, vec![IP_FE80_1]); 608 | } 609 | 610 | #[test] 611 | fn test_parse_resolv_ipv6_link_local_zone_id() { 612 | let res = parse_resolv_conf( 613 | "nameserver fe80::1%1 614 | ", 615 | ) 616 | .expect("failed to parse"); 617 | assert_eq!(res, vec![IP_FE80_1]); 618 | } 619 | } 620 | --------------------------------------------------------------------------------