diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 028fef3..0f4cfd8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,9 @@ jobs: strategy: fail-fast: false matrix: - os: [noble, jammy, focal, trixie, bookworm, bullseye] + os: [noble, jammy, focal, trixie, bookworm, bullseye, + noble-desktop, jammy-desktop, focal-desktop, + trixie-desktop, bookworm-desktop, bullseye-desktop] steps: - uses: actions/checkout@v4 diff --git a/Makefile b/Makefile index 599edbd..7eab15d 100644 --- a/Makefile +++ b/Makefile @@ -12,4 +12,18 @@ test-systemd: build ./test/systemd/build-local-deb.sh bin/docker-dns 0.0.0-local ./test/systemd/test-systemd.sh $(OS) -.PHONY: build test run test-systemd +test-systemd-all: build + @for os in noble jammy focal trixie bookworm bullseye noble-desktop jammy-desktop focal-desktop trixie-desktop bookworm-desktop bullseye-desktop; do \ + echo "=== Testing $$os ==="; \ + ./test/systemd/build-local-deb.sh bin/docker-dns 0.0.0-local && \ + ./test/systemd/test-systemd.sh $$os || echo "FAILED: $$os"; \ + done + +test-systemd-desktop-all: build + @for os in noble-desktop jammy-desktop focal-desktop trixie-desktop bookworm-desktop bullseye-desktop; do \ + echo "=== Testing $$os ==="; \ + ./test/systemd/build-local-deb.sh bin/docker-dns 0.0.0-local && \ + ./test/systemd/test-systemd.sh $$os || echo "FAILED: $$os"; \ + done + +.PHONY: build test run test-systemd test-systemd-all test-systemd-desktop-all diff --git a/README.md b/README.md index 3863f2e..d8febb0 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,15 @@ between containers and the host. ## Features -- **Automatic DNS Resolution**: Resolve Docker container names with a custom TLD to their respective IP addresses. -- **Caching Mechanism**: Configurable TTL for DNS cache entries to improve performance. -- **Simple Configuration**: Minimal setup with command-line options or a configuration file. -- **Debian Package**: Easy-to-install `.deb` package for seamless integration. -- **Fallback DNS**: Forwards non-Docker queries to configurable DNS servers. -- **Lightweight and Fast**: Built with Go for high performance. +- **Automatic DNS Resolution**: Resolve Docker container names with a custom TLD (default `.docker`) to their IP addresses. Supports multiple TLDs and containers on any Docker network. +- **Fallback DNS**: Forwards non-Docker queries in parallel to configurable upstream resolvers (default: `8.8.8.8`, `1.1.1.1`, `8.8.4.4`), returning the first successful response. +- **Caching**: TTL-based DNS cache with background eviction, size limits, and hit/miss telemetry. +- **Rate Limiting**: Per-IP token-bucket rate limiter with automatic idle cleanup. +- **Health & Metrics**: HTTP server on `:8080` exposes `/health` and `/metrics` (cache stats, query counts, error rates). +- **UDP + TCP**: Full DNS protocol support with EDNS0 handling and proper truncation. +- **Debian Package**: `.deb` package with automatic systemd integration and clean uninstall. +- **Tested on 12 Configurations**: Full install → resolve → uninstall lifecycle CI on Ubuntu 20.04/22.04/24.04 and Debian 11/12/13, both server and desktop variants. +- **Lightweight**: Single Go binary, minimal resource footprint. ## Architecture @@ -146,23 +149,18 @@ integrates with DNS resolution for each ditro/version.. --- -## Important Notes +## DNS Integration -### Default Resolver Integration +The `.deb` package auto-detects your system's DNS resolver and integrates accordingly: -- While Docker DNS runs by default on a custom IP (not `127.0.0.1`), it is possible to use tools like `dig` to test - queries: - ```bash - dig mycontainer.docker @127.0.0.153 +short - ``` - - However, making Docker DNS the system-wide default resolver requires additional configuration or hacks. +- **Ubuntu** (systemd-resolved): routing domain drop-in at `/etc/systemd/resolved.conf.d/docker-dns.conf` — only + `.docker` queries go to docker-dns, everything else is untouched +- **Debian Desktop** (NetworkManager): dispatcher script at `/etc/NetworkManager/dispatcher.d/docker-dns` re-prepends + the nameserver after every NM event +- **Debian Server** (plain resolv.conf): prepends `nameserver 127.0.0.153` to `/etc/resolv.conf` -### Systemd-Resolved Compatibility: Direct integration with -`systemd-resolved` is non-trivial and not recommended without advanced setup. - -### Protocol - -* `docker-dns` is configured to run on `UDP` mode only, so `TCP` requests won't be answered +See [docs/Systemd.md](./docs/Systemd.md) for the full details, browser DNS-over-HTTPS caveats, and manual integration +examples for custom resolvers (dnsmasq, unbound, Pi-hole). --- @@ -192,17 +190,31 @@ integrates with DNS resolution for each ditro/version.. - Once you have the binary generated, you can run at as follows (linux): ```bash sudo ./docker-dns -h - Usage of docker-dns: - -ip string - IP address the DNS server listens on (default "127.0.0.153") - -tld string - Comma-separated managed top-level domains for container resolution (default "docker") - -ttl int - TTL in seconds for cache entries and DNS responses (default 300) - -resolvers string - Comma-separated fallback DNS resolver IPs (default "8.8.8.8,1.1.1.1,8.8.4.4") - -forward-timeout duration - Per-resolver timeout for forwarded DNS queries (default 2s) + Usage of docker-dns: + -ip string + IP address the DNS server listens on (default "127.0.0.153") + -tld string + Comma-separated managed top-level domains for container resolution (default "docker") + -ttl int + TTL in seconds for cache entries and DNS responses (default 300) + -resolvers string + Comma-separated fallback DNS resolver IPs (default "8.8.8.8,1.1.1.1,8.8.4.4") + -forward-timeout duration + Per-resolver timeout for forwarded DNS queries (default 2s) + -rate-limit float + Max queries/sec per client IP; 0 disables rate limiting (default 100) + -rate-burst int + Burst allowance for per-IP rate limiting (default 50) + -max-cache-size int + Max DNS cache entries; 0 = unlimited (default 10000) + -http-addr string + Address for the health/metrics HTTP server; empty to disable (default ":8080") + -docker-timeout duration + Timeout for Docker API calls (default 5s) + -docker-host string + Docker host override (empty = use DOCKER_HOST env / socket default) + -log-level string + Log level: debug | info | warn | error (default "info") ``` - P.S: `sudo` (or `root`) is required as the server will be listening on port `53`, which is a [previewed port](https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html) diff --git a/build/dpkg/DEBIAN/postinst b/build/dpkg/DEBIAN/postinst index 0c3908f..abaf013 100644 --- a/build/dpkg/DEBIAN/postinst +++ b/build/dpkg/DEBIAN/postinst @@ -57,6 +57,16 @@ configure) done ROUTING_DOMAINS=$(echo "$ROUTING_DOMAINS" | xargs) # trim leading space + # Prepend `nameserver $CUSTOM_IP` to /etc/resolv.conf (idempotent). + prepend_resolv_conf() { + grep -q "$CUSTOM_IP" /etc/resolv.conf 2>/dev/null && \ + echo "detected an entry of $CUSTOM_IP in /etc/resolv.conf, removing.." && \ + grep -v "$CUSTOM_IP" /etc/resolv.conf > /tmp/resolv.tmp && \ + cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp + { printf '# Generated by docker-dns\nnameserver %s\n' "$CUSTOM_IP"; cat /etc/resolv.conf; } > /tmp/resolv.tmp \ + && cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp + } + if systemctl is-active systemd-resolved.service >/dev/null 2>&1; then echo "Detected systemd-resolved running. Extending its configuration.." @@ -72,15 +82,41 @@ Domains=$ROUTING_DOMAINS EOF systemctl restart systemd-resolved.service echo "systemd-resolved extended with DNS=$CUSTOM_IP and Domains=$ROUTING_DOMAINS." + elif systemctl is-active NetworkManager.service >/dev/null 2>&1; then + # NetworkManager active without systemd-resolved (Debian Desktop). + # NM rewrites /etc/resolv.conf on every connection event and would clobber + # a one-shot prepend. Install a dispatcher script so NM re-prepends for us + # after every up/down/dhcp event, and prepend once immediately so DNS + # works between now and the next NM event. + echo "Detected NetworkManager without systemd-resolved. Installing dispatcher.." + + mkdir -p /etc/NetworkManager/dispatcher.d + cat </etc/NetworkManager/dispatcher.d/docker-dns +#!/bin/bash +# Re-prepend docker-dns nameserver after NetworkManager rewrites /etc/resolv.conf. +# NM invokes dispatchers with "\$1 = interface" and "\$2 = action". +# up/dhcp*-change: NM has just written resolv.conf with connection-provided DNS. +# down: NM rewrites resolv.conf to drop the departing interface's DNS servers. +case "\$2" in + up|down|dhcp4-change|dhcp6-change) + if ! grep -q '$CUSTOM_IP' /etc/resolv.conf 2>/dev/null; then + { printf '# Generated by docker-dns\nnameserver %s\n' '$CUSTOM_IP' + cat /etc/resolv.conf + } > /tmp/resolv.docker-dns.tmp \\ + && cat /tmp/resolv.docker-dns.tmp > /etc/resolv.conf \\ + && rm -f /tmp/resolv.docker-dns.tmp + fi + ;; +esac +EOF + chmod 0755 /etc/NetworkManager/dispatcher.d/docker-dns + chown root:root /etc/NetworkManager/dispatcher.d/docker-dns + + prepend_resolv_conf + echo "NetworkManager dispatcher installed; /etc/resolv.conf prepended with nameserver $CUSTOM_IP." else - echo "systemd-resolved is not active. Updating /etc/resolv.conf.." - grep -q "$CUSTOM_IP" /etc/resolv.conf 2>/dev/null && \ - echo "detected an entry of $CUSTOM_IP in /etc/resolv.conf, removing.." && \ - grep -v "$CUSTOM_IP" /etc/resolv.conf > /tmp/resolv.tmp && \ - cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp - # Prepend so docker-dns is queried before other nameservers - { printf '# Generated by docker-dns\nnameserver %s\n' "$CUSTOM_IP"; cat /etc/resolv.conf; } > /tmp/resolv.tmp \ - && cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp + echo "Neither systemd-resolved nor NetworkManager is active. Updating /etc/resolv.conf.." + prepend_resolv_conf echo "/etc/resolv.conf updated with nameserver $CUSTOM_IP." fi ;; diff --git a/build/dpkg/DEBIAN/postrm b/build/dpkg/DEBIAN/postrm index 6abf401..3284a38 100644 --- a/build/dpkg/DEBIAN/postrm +++ b/build/dpkg/DEBIAN/postrm @@ -22,6 +22,10 @@ remove|purge) rm -f /etc/unbound/unbound.conf.d/docker-dns.conf systemctl restart unbound 2>/dev/null || true + # NetworkManager dispatcher — remove BEFORE restarting NM so a restart event + # cannot fire the dispatcher and re-add the nameserver to resolv.conf. + rm -f /etc/NetworkManager/dispatcher.d/docker-dns + # NetworkManager dnsmasq rm -f /etc/NetworkManager/dnsmasq.d/docker-dns.conf systemctl restart NetworkManager 2>/dev/null || true diff --git a/docs/Systemd.md b/docs/Systemd.md index e0326c1..77324b7 100644 --- a/docs/Systemd.md +++ b/docs/Systemd.md @@ -86,31 +86,69 @@ The `~` prefix on `docker` makes it a routing domain, not a search domain. This This is the same mechanism used by VPNs, LXD, and corporate split-DNS setups. -### On systems without systemd-resolved (Debian default) +### On systems with NetworkManager (Debian Desktop) -The postinst appends docker-dns as a nameserver to `/etc/resolv.conf`: +On Debian Desktop, NetworkManager is active but systemd-resolved is not installed. NM runs with `dns=default`, meaning +it writes `/etc/resolv.conf` directly on every connection event, DHCP renewal, or service restart. A one-shot prepend +to `/etc/resolv.conf` would be overwritten the next time NM touches the file. + +The postinst detects this (NM active, resolved not active) and installs a dispatcher script: + +```bash +# /etc/NetworkManager/dispatcher.d/docker-dns +``` + +The dispatcher re-prepends `nameserver 127.0.0.153` to `/etc/resolv.conf` after every NM connection event (`up`, +`down`, `dhcp4-change`, `dhcp6-change`). The postinst also does an immediate one-shot prepend so DNS works between +install and the next NM event. + +The postrm removes the dispatcher script and cleans up `/etc/resolv.conf` on uninstall. + +### On systems without systemd-resolved or NetworkManager (Debian Server) + +On minimal or server installs where neither systemd-resolved nor NetworkManager is active, the postinst prepends +docker-dns as a nameserver to `/etc/resolv.conf`: ``` # Generated by docker-dns nameserver 127.0.0.153 ``` -Since docker-dns handles `.docker` queries itself and forwards everything else to its configured upstream resolvers ( -default: 8.8.8.8, 1.1.1.1, 8.8.4.4), this works for both container resolution and normal DNS. +Since docker-dns handles `.docker` queries itself and forwards everything else to its configured upstream resolvers +(default: 8.8.8.8, 1.1.1.1, 8.8.4.4), this works for both container resolution and normal DNS. The postrm removes this entry on uninstall using the `# Generated by docker-dns` comment as a marker. -### On systems with other resolvers +### Custom DNS Resolvers (Manual Configuration) -The postinst detects the active resolver and writes the appropriate configuration: +docker-dns's automatic integration covers the default DNS stack on all supported Ubuntu and Debian versions (server and +desktop). If you run a custom local resolver (dnsmasq, unbound, Pi-hole, or NM with `dns=dnsmasq`), you already know +your DNS stack — add the forwarding rule manually. -| Resolver | Config file | Directive | -|-------------------------------|-------------------------------------------------|-------------------------------------| -| dnsmasq | `/etc/dnsmasq.d/docker-dns.conf` | `server=/docker/127.0.0.153` | -| unbound | `/etc/unbound/unbound.conf.d/docker-dns.conf` | `stub-zone` pointing to 127.0.0.153 | -| NetworkManager (dnsmasq mode) | `/etc/NetworkManager/dnsmasq.d/docker-dns.conf` | `server=/docker/127.0.0.153` | +**dnsmasq** (standalone or Pi-hole): +``` +# /etc/dnsmasq.d/docker-dns.conf +server=/docker/127.0.0.153 +``` +Then: `sudo systemctl restart dnsmasq` + +**unbound**: +```yaml +# /etc/unbound/unbound.conf.d/docker-dns.conf +stub-zone: + name: "docker" + stub-addr: 127.0.0.153 +``` +Then: `sudo systemctl restart unbound` -All of these are cleaned up by the postrm on uninstall. +**NetworkManager with dns=dnsmasq**: +``` +# /etc/NetworkManager/dnsmasq.d/docker-dns.conf +server=/docker/127.0.0.153 +``` +Then: `sudo systemctl restart NetworkManager` + +Note: the postrm does not clean up manually-created configs. If you uninstall docker-dns, remove these files yourself. --- @@ -187,12 +225,18 @@ systemctl restart dnsmasq 2>/dev/null || true rm -f /etc/unbound/unbound.conf.d/docker-dns.conf systemctl restart unbound 2>/dev/null || true +# NetworkManager dispatcher — removed before NM restart so a restart event +# cannot fire the dispatcher and re-add the nameserver to resolv.conf +rm -f /etc/NetworkManager/dispatcher.d/docker-dns + # NetworkManager dnsmasq rm -f /etc/NetworkManager/dnsmasq.d/docker-dns.conf systemctl restart NetworkManager 2>/dev/null || true -# Plain resolv.conf: remove the appended entry -sed -i '/# Generated by docker-dns/,+1d' /etc/resolv.conf +# Plain resolv.conf: remove the prepended entry +awk '/# Generated by docker-dns/{skip=1; next} skip{skip=0; next} 1' \ + /etc/resolv.conf > /tmp/resolv.tmp \ + && cat /tmp/resolv.tmp > /etc/resolv.conf && rm -f /tmp/resolv.tmp ``` Each `systemctl restart` is guarded with `2>/dev/null || true` so it silently skips services that aren't installed. @@ -205,14 +249,22 @@ The systemd integration tests (`make test-systemd OS=`) verify the ful lifecycle on each target. Both the direct path (`dig @127.0.0.153`) and the system-level path (`dig` without `@`, going through whatever the OS uses for DNS) are tested. -| OS | Version | DNS method | Assertions | -|----------|--------------|-----------------------------------|------------| -| Noble | Ubuntu 24.04 | systemd-resolved + routing domain | 13 | -| Jammy | Ubuntu 22.04 | systemd-resolved + routing domain | 13 | -| Focal | Ubuntu 20.04 | systemd-resolved + routing domain | 13 | -| Trixie | Debian 13 | plain resolv.conf | 12 | -| Bookworm | Debian 12 | plain resolv.conf | 12 | -| Bullseye | Debian 11 | plain resolv.conf | 12 | +| OS | Version | Variant | DNS Method | Assertions | +|----------|--------------|---------|-------------------------------------------------|------------| +| Noble | Ubuntu 24.04 | Server | systemd-resolved + routing domain | 13 | +| Noble | Ubuntu 24.04 | Desktop | systemd-resolved + NM (dns=systemd-resolved) | 14 | +| Jammy | Ubuntu 22.04 | Server | systemd-resolved + routing domain | 13 | +| Jammy | Ubuntu 22.04 | Desktop | systemd-resolved + NM (dns=systemd-resolved) | 14 | +| Focal | Ubuntu 20.04 | Server | systemd-resolved + routing domain | 13 | +| Focal | Ubuntu 20.04 | Desktop | systemd-resolved + NM (dns=systemd-resolved) | 14 | +| Trixie | Debian 13 | Server | plain resolv.conf | 12 | +| Trixie | Debian 13 | Desktop | NM dispatcher + resolv.conf | 15 | +| Bookworm | Debian 12 | Server | plain resolv.conf | 12 | +| Bookworm | Debian 12 | Desktop | NM dispatcher + resolv.conf | 15 | +| Bullseye | Debian 11 | Server | plain resolv.conf | 12 | +| Bullseye | Debian 11 | Desktop | NM dispatcher + resolv.conf | 15 | + +Desktop variants additionally test that docker-dns resolution survives a full `systemctl restart NetworkManager` cycle. Each test runs inside a privileged Docker container with systemd as PID 1, Docker-in-Docker for container resolution, and full DNS chain verification including post-uninstall cleanup. diff --git a/index.html b/index.html index f6cd8c3..96a0f22 100644 --- a/index.html +++ b/index.html @@ -227,7 +227,7 @@

Features

  • Health & Metrics — HTTP server on :8080 exposes /health and /metrics (cache stats, query counts, error rates).
  • UDP + TCP — full DNS protocol support with EDNS0 handling and proper truncation.
  • Debian Package.deb package with automatic systemd integration and clean uninstall.
  • -
  • Tested on 6 Distros — full install → resolve → uninstall lifecycle CI on Ubuntu 20.04/22.04/24.04 and Debian 11/12/13.
  • +
  • Tested on 12 Configurations — full install → resolve → uninstall lifecycle CI on Ubuntu 20.04/22.04/24.04 and Debian 11/12/13, both server and desktop variants.
  • Lightweight — single Go binary, minimal resource footprint.
  • @@ -254,13 +254,12 @@

    Download and Install

    SystemIntegration Ubuntu (systemd-resolved)Drop-in config at /etc/systemd/resolved.conf.d/docker-dns.conf with routing domains (~docker) - Debian (plain resolv.conf)Prepends nameserver 127.0.0.153 to /etc/resolv.conf - dnsmasqserver=/docker/127.0.0.153 in /etc/dnsmasq.d/docker-dns.conf - unboundstub-zone in /etc/unbound/unbound.conf.d/docker-dns.conf - NetworkManager (dnsmasq)server=/docker/127.0.0.153 in /etc/NetworkManager/dnsmasq.d/docker-dns.conf + Debian Desktop (NetworkManager)Dispatcher script at /etc/NetworkManager/dispatcher.d/docker-dns re-prepends nameserver after every NM event + Debian Server (plain resolv.conf)Prepends nameserver 127.0.0.153 to /etc/resolv.conf +

    If you use a custom resolver (dnsmasq, unbound, Pi-hole), see the Systemd & DNS documentation for manual integration examples.

    Verify

    systemctl status docker-dns
    @@ -337,13 +336,27 @@

    CLI Help

    -ip string IP address the DNS server listens on (default "127.0.0.153") -tld string - Comma-separated managed top-level domains (default "docker") + Comma-separated managed top-level domains for container resolution (default "docker") -ttl int TTL in seconds for cache entries and DNS responses (default 300) -resolvers string Comma-separated fallback DNS resolver IPs (default "8.8.8.8,1.1.1.1,8.8.4.4") -forward-timeout duration - Per-resolver timeout for forwarded DNS queries (default 2s) + Per-resolver timeout for forwarded DNS queries (default 2s) + -rate-limit float + Max queries/sec per client IP; 0 disables rate limiting (default 100) + -rate-burst int + Burst allowance for per-IP rate limiting (default 50) + -max-cache-size int + Max DNS cache entries; 0 = unlimited (default 10000) + -http-addr string + Address for the health/metrics HTTP server; empty to disable (default ":8080") + -docker-timeout duration + Timeout for Docker API calls (default 5s) + -docker-host string + Docker host override (empty = use DOCKER_HOST env / socket default) + -log-level string + Log level: debug | info | warn | error (default "info")

    Note: sudo (or root) is required because port 53 is a privileged port. The .deb package uses CAP_NET_BIND_SERVICE instead.

    @@ -357,14 +370,20 @@

    Systemd & DNS Integration

    Tested Configurations

    - + - - - - - - + + + + + + + + + + + +
    OSVersionDNS Method
    OSVersionVariantDNS Method
    NobleUbuntu 24.04systemd-resolved + routing domain
    JammyUbuntu 22.04systemd-resolved + routing domain
    FocalUbuntu 20.04systemd-resolved + routing domain
    TrixieDebian 13plain resolv.conf
    BookwormDebian 12plain resolv.conf
    BullseyeDebian 11plain resolv.conf
    NobleUbuntu 24.04Serversystemd-resolved + routing domain
    NobleUbuntu 24.04Desktopsystemd-resolved + NM (dns=systemd-resolved)
    JammyUbuntu 22.04Serversystemd-resolved + routing domain
    JammyUbuntu 22.04Desktopsystemd-resolved + NM (dns=systemd-resolved)
    FocalUbuntu 20.04Serversystemd-resolved + routing domain
    FocalUbuntu 20.04Desktopsystemd-resolved + NM (dns=systemd-resolved)
    TrixieDebian 13Serverplain resolv.conf
    TrixieDebian 13DesktopNM dispatcher + resolv.conf
    BookwormDebian 12Serverplain resolv.conf
    BookwormDebian 12DesktopNM dispatcher + resolv.conf
    BullseyeDebian 11Serverplain resolv.conf
    BullseyeDebian 11DesktopNM dispatcher + resolv.conf
    diff --git a/test/systemd/Dockerfile.bookworm-desktop b/test/systemd/Dockerfile.bookworm-desktop new file mode 100644 index 0000000..6b2dcc9 --- /dev/null +++ b/test/systemd/Dockerfile.bookworm-desktop @@ -0,0 +1,64 @@ +FROM debian:12 + +# Real Debian Desktop (Bookworm) has NetworkManager but NOT systemd-resolved and +# NOT resolvconf — NM uses its default dns=default plugin to write /etc/resolv.conf +# directly. Installing resolvconf would switch NM to the dns=resolvconf plugin, +# a different code path that doesn't match real-world Debian Desktop. +# +# This variant reproduces the gap: NM overwrites /etc/resolv.conf on restart, +# dropping the nameserver line that docker-dns's postinst prepended. +# +# NM-in-container caveats: --privileged (already used by test-systemd.sh), dbus +# (installed below), dummy kmod on the host kernel. NetworkManager-wait-online +# is masked — with only a dummy interface it would block boot indefinitely. + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + systemd \ + systemd-sysv \ + dbus \ + dnsutils \ + iproute2 \ + libcap2-bin \ + ca-certificates \ + network-manager \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Docker bind-mounts /etc/resolv.conf, which blocks NM's atomic rename() writes. +# Unmount the bind-mount and seed a placeholder before NetworkManager starts. +COPY setup-resolvconf-nm.sh /usr/local/bin/setup-resolvconf-nm.sh +RUN chmod +x /usr/local/bin/setup-resolvconf-nm.sh +COPY setup-resolvconf-nm.service /etc/systemd/system/setup-resolvconf-nm.service +RUN systemctl enable setup-resolvconf-nm.service + +# NM by default leaves dummy* interfaces unmanaged. Explicitly mark dns-test0 +# as managed so NM activates the connection profile below and drives DNS. +RUN mkdir -p /etc/NetworkManager/conf.d && \ + printf '[main]\nplugins=keyfile\n[device]\nmatch-device=interface-name:dns-test0\nmanaged=true\n[keyfile]\nunmanaged-devices=none\n' \ + > /etc/NetworkManager/conf.d/10-docker-dns-test.conf + +# type=dummy → NM creates the interface itself on activation. +# dns=8.8.8.8;1.1.1.1; → NM has real DNS servers to push on restart — this is +# what clobbers docker-dns's prepended nameserver and reproduces the gap. +# Profile must be mode 0600 or NM refuses to load it. +RUN mkdir -p /etc/NetworkManager/system-connections +COPY dns-test0.nmconnection /etc/NetworkManager/system-connections/dns-test0.nmconnection +RUN chmod 600 /etc/NetworkManager/system-connections/dns-test0.nmconnection && \ + chown root:root /etc/NetworkManager/system-connections/dns-test0.nmconnection + +RUN systemctl enable NetworkManager && \ + systemctl mask NetworkManager-wait-online.service || true + +RUN systemctl mask \ + systemd-firstboot.service \ + systemd-udevd.service \ + systemd-modules-load.service \ + getty@.service \ + serial-getty@.service \ + console-getty.service \ + systemd-logind.service + +STOPSIGNAL SIGRTMIN+3 + +CMD ["/lib/systemd/systemd"] diff --git a/test/systemd/Dockerfile.bullseye-desktop b/test/systemd/Dockerfile.bullseye-desktop new file mode 100644 index 0000000..8e7810e --- /dev/null +++ b/test/systemd/Dockerfile.bullseye-desktop @@ -0,0 +1,48 @@ +FROM debian:11 + +# Real Debian Desktop (Bullseye) has NetworkManager but NOT systemd-resolved and +# NOT resolvconf — see Dockerfile.bookworm-desktop for rationale and NM-in-container +# notes. This variant reproduces the same NM-overwrites-resolv.conf gap. + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + systemd \ + systemd-sysv \ + dbus \ + dnsutils \ + iproute2 \ + libcap2-bin \ + ca-certificates \ + network-manager \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY setup-resolvconf-nm.sh /usr/local/bin/setup-resolvconf-nm.sh +RUN chmod +x /usr/local/bin/setup-resolvconf-nm.sh +COPY setup-resolvconf-nm.service /etc/systemd/system/setup-resolvconf-nm.service +RUN systemctl enable setup-resolvconf-nm.service + +RUN mkdir -p /etc/NetworkManager/conf.d && \ + printf '[main]\nplugins=keyfile\n[device]\nmatch-device=interface-name:dns-test0\nmanaged=true\n[keyfile]\nunmanaged-devices=none\n' \ + > /etc/NetworkManager/conf.d/10-docker-dns-test.conf + +RUN mkdir -p /etc/NetworkManager/system-connections +COPY dns-test0.nmconnection /etc/NetworkManager/system-connections/dns-test0.nmconnection +RUN chmod 600 /etc/NetworkManager/system-connections/dns-test0.nmconnection && \ + chown root:root /etc/NetworkManager/system-connections/dns-test0.nmconnection + +RUN systemctl enable NetworkManager && \ + systemctl mask NetworkManager-wait-online.service || true + +RUN systemctl mask \ + systemd-firstboot.service \ + systemd-udevd.service \ + systemd-modules-load.service \ + getty@.service \ + serial-getty@.service \ + console-getty.service \ + systemd-logind.service + +STOPSIGNAL SIGRTMIN+3 + +CMD ["/lib/systemd/systemd"] diff --git a/test/systemd/Dockerfile.focal-desktop b/test/systemd/Dockerfile.focal-desktop new file mode 100644 index 0000000..18c2513 --- /dev/null +++ b/test/systemd/Dockerfile.focal-desktop @@ -0,0 +1,62 @@ +FROM ubuntu:20.04 + +# Real Ubuntu Desktop (Focal) ships BOTH NetworkManager AND systemd-resolved. +# NM delegates DNS to resolved, so docker-dns's ~docker routing domain should +# survive an NM restart. See Dockerfile.noble-desktop for NM-in-container notes. + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + systemd \ + systemd-sysv \ + dbus \ + dnsutils \ + iproute2 \ + libcap2-bin \ + ca-certificates \ + network-manager \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# On 20.04 systemd-resolved is part of the systemd package, not separate. +RUN systemctl enable systemd-resolved + +RUN mkdir -p /etc/systemd/resolved.conf.d && \ + printf '[Resolve]\nFallbackDNS=8.8.8.8 1.1.1.1\nDNS=8.8.8.8\n' \ + > /etc/systemd/resolved.conf.d/fallback.conf + +COPY setup-resolvconf.sh /usr/local/bin/setup-resolvconf.sh +RUN chmod +x /usr/local/bin/setup-resolvconf.sh +COPY setup-resolvconf.service /etc/systemd/system/setup-resolvconf.service +RUN systemctl enable setup-resolvconf.service + +RUN mkdir -p /etc/NetworkManager/conf.d && \ + printf '[main]\nplugins=keyfile\n[device]\nmatch-device=interface-name:dns-test0\nmanaged=true\n[keyfile]\nunmanaged-devices=none\n' \ + > /etc/NetworkManager/conf.d/10-docker-dns-test.conf + +# Real Ubuntu Desktop ships this config so NM delegates DNS to systemd-resolved +# instead of writing /etc/resolv.conf itself (Debian behavior). Without it, the +# test would accidentally exercise the Debian code path while resolved happens +# to still be running in the background. +RUN printf '[main]\ndns=systemd-resolved\n' \ + > /etc/NetworkManager/conf.d/10-dns-resolved.conf + +RUN mkdir -p /etc/NetworkManager/system-connections +COPY dns-test0.nmconnection /etc/NetworkManager/system-connections/dns-test0.nmconnection +RUN chmod 600 /etc/NetworkManager/system-connections/dns-test0.nmconnection && \ + chown root:root /etc/NetworkManager/system-connections/dns-test0.nmconnection + +RUN systemctl enable NetworkManager && \ + systemctl mask NetworkManager-wait-online.service || true + +RUN systemctl mask \ + systemd-firstboot.service \ + systemd-udevd.service \ + systemd-modules-load.service \ + getty@.service \ + serial-getty@.service \ + console-getty.service \ + systemd-logind.service + +STOPSIGNAL SIGRTMIN+3 + +CMD ["/lib/systemd/systemd"] diff --git a/test/systemd/Dockerfile.jammy-desktop b/test/systemd/Dockerfile.jammy-desktop new file mode 100644 index 0000000..1ea96bd --- /dev/null +++ b/test/systemd/Dockerfile.jammy-desktop @@ -0,0 +1,62 @@ +FROM ubuntu:22.04 + +# Real Ubuntu Desktop (Jammy) ships BOTH NetworkManager AND systemd-resolved. +# NM delegates DNS to resolved, so docker-dns's ~docker routing domain should +# survive an NM restart. See Dockerfile.noble-desktop for NM-in-container notes. + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + systemd \ + systemd-sysv \ + dbus \ + dnsutils \ + iproute2 \ + libcap2-bin \ + ca-certificates \ + network-manager \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# On 22.04 systemd-resolved is part of the systemd package, not separate. +RUN systemctl enable systemd-resolved + +RUN mkdir -p /etc/systemd/resolved.conf.d && \ + printf '[Resolve]\nFallbackDNS=8.8.8.8 1.1.1.1\nDNS=8.8.8.8\n' \ + > /etc/systemd/resolved.conf.d/fallback.conf + +COPY setup-resolvconf.sh /usr/local/bin/setup-resolvconf.sh +RUN chmod +x /usr/local/bin/setup-resolvconf.sh +COPY setup-resolvconf.service /etc/systemd/system/setup-resolvconf.service +RUN systemctl enable setup-resolvconf.service + +RUN mkdir -p /etc/NetworkManager/conf.d && \ + printf '[main]\nplugins=keyfile\n[device]\nmatch-device=interface-name:dns-test0\nmanaged=true\n[keyfile]\nunmanaged-devices=none\n' \ + > /etc/NetworkManager/conf.d/10-docker-dns-test.conf + +# Real Ubuntu Desktop ships this config so NM delegates DNS to systemd-resolved +# instead of writing /etc/resolv.conf itself (Debian behavior). Without it, the +# test would accidentally exercise the Debian code path while resolved happens +# to still be running in the background. +RUN printf '[main]\ndns=systemd-resolved\n' \ + > /etc/NetworkManager/conf.d/10-dns-resolved.conf + +RUN mkdir -p /etc/NetworkManager/system-connections +COPY dns-test0.nmconnection /etc/NetworkManager/system-connections/dns-test0.nmconnection +RUN chmod 600 /etc/NetworkManager/system-connections/dns-test0.nmconnection && \ + chown root:root /etc/NetworkManager/system-connections/dns-test0.nmconnection + +RUN systemctl enable NetworkManager && \ + systemctl mask NetworkManager-wait-online.service || true + +RUN systemctl mask \ + systemd-firstboot.service \ + systemd-udevd.service \ + systemd-modules-load.service \ + getty@.service \ + serial-getty@.service \ + console-getty.service \ + systemd-logind.service + +STOPSIGNAL SIGRTMIN+3 + +CMD ["/lib/systemd/systemd"] diff --git a/test/systemd/Dockerfile.noble-desktop b/test/systemd/Dockerfile.noble-desktop new file mode 100644 index 0000000..743eec6 --- /dev/null +++ b/test/systemd/Dockerfile.noble-desktop @@ -0,0 +1,72 @@ +FROM ubuntu:24.04 + +# Real Ubuntu Desktop (Noble) ships BOTH NetworkManager AND systemd-resolved. +# NM delegates DNS to resolved, so docker-dns's ~docker routing domain should +# survive an NM restart. This Dockerfile mirrors that stack. +# +# NM-in-container caveats (container must be --privileged, which test-systemd.sh +# already uses): NM needs dbus (installed below) and the dummy kmod on the host +# kernel. NetworkManager-wait-online.service is masked — with only a dummy +# interface it would block boot indefinitely. + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + systemd \ + systemd-sysv \ + systemd-resolved \ + dbus \ + dnsutils \ + iproute2 \ + libcap2-bin \ + ca-certificates \ + network-manager \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN systemctl enable systemd-resolved + +RUN mkdir -p /etc/systemd/resolved.conf.d && \ + printf '[Resolve]\nFallbackDNS=8.8.8.8 1.1.1.1\nDNS=8.8.8.8\n' \ + > /etc/systemd/resolved.conf.d/fallback.conf + +COPY setup-resolvconf.sh /usr/local/bin/setup-resolvconf.sh +RUN chmod +x /usr/local/bin/setup-resolvconf.sh +COPY setup-resolvconf.service /etc/systemd/system/setup-resolvconf.service +RUN systemctl enable setup-resolvconf.service + +# NM by default leaves dummy* interfaces unmanaged. Explicitly mark dns-test0 +# as managed so NM activates the connection profile below and drives DNS. +RUN mkdir -p /etc/NetworkManager/conf.d && \ + printf '[main]\nplugins=keyfile\n[device]\nmatch-device=interface-name:dns-test0\nmanaged=true\n[keyfile]\nunmanaged-devices=none\n' \ + > /etc/NetworkManager/conf.d/10-docker-dns-test.conf + +# Real Ubuntu Desktop ships this config so NM delegates DNS to systemd-resolved +# instead of writing /etc/resolv.conf itself (which is what Debian does). Without +# this file, NM runs with dns=default and the test would accidentally exercise +# the Debian code path while resolved happens to still be running in the background. +RUN printf '[main]\ndns=systemd-resolved\n' \ + > /etc/NetworkManager/conf.d/10-dns-resolved.conf + +# type=dummy → NM creates the interface itself on activation. +# dns=8.8.8.8;1.1.1.1; → NM has real DNS servers to push on restart. +# Profile must be mode 0600 or NM refuses to load it. +RUN mkdir -p /etc/NetworkManager/system-connections +COPY dns-test0.nmconnection /etc/NetworkManager/system-connections/dns-test0.nmconnection +RUN chmod 600 /etc/NetworkManager/system-connections/dns-test0.nmconnection && \ + chown root:root /etc/NetworkManager/system-connections/dns-test0.nmconnection + +RUN systemctl enable NetworkManager && \ + systemctl mask NetworkManager-wait-online.service || true + +RUN systemctl mask \ + systemd-firstboot.service \ + systemd-udevd.service \ + systemd-modules-load.service \ + getty@.service \ + serial-getty@.service \ + console-getty.service \ + systemd-logind.service + +STOPSIGNAL SIGRTMIN+3 + +CMD ["/lib/systemd/systemd"] diff --git a/test/systemd/Dockerfile.trixie-desktop b/test/systemd/Dockerfile.trixie-desktop new file mode 100644 index 0000000..ca67abb --- /dev/null +++ b/test/systemd/Dockerfile.trixie-desktop @@ -0,0 +1,48 @@ +FROM debian:trixie + +# Real Debian Desktop (Trixie) has NetworkManager but NOT systemd-resolved and +# NOT resolvconf — see Dockerfile.bookworm-desktop for rationale and NM-in-container +# notes. This variant reproduces the same NM-overwrites-resolv.conf gap. + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + systemd \ + systemd-sysv \ + dbus \ + dnsutils \ + iproute2 \ + libcap2-bin \ + ca-certificates \ + network-manager \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY setup-resolvconf-nm.sh /usr/local/bin/setup-resolvconf-nm.sh +RUN chmod +x /usr/local/bin/setup-resolvconf-nm.sh +COPY setup-resolvconf-nm.service /etc/systemd/system/setup-resolvconf-nm.service +RUN systemctl enable setup-resolvconf-nm.service + +RUN mkdir -p /etc/NetworkManager/conf.d && \ + printf '[main]\nplugins=keyfile\n[device]\nmatch-device=interface-name:dns-test0\nmanaged=true\n[keyfile]\nunmanaged-devices=none\n' \ + > /etc/NetworkManager/conf.d/10-docker-dns-test.conf + +RUN mkdir -p /etc/NetworkManager/system-connections +COPY dns-test0.nmconnection /etc/NetworkManager/system-connections/dns-test0.nmconnection +RUN chmod 600 /etc/NetworkManager/system-connections/dns-test0.nmconnection && \ + chown root:root /etc/NetworkManager/system-connections/dns-test0.nmconnection + +RUN systemctl enable NetworkManager && \ + systemctl mask NetworkManager-wait-online.service || true + +RUN systemctl mask \ + systemd-firstboot.service \ + systemd-udevd.service \ + systemd-modules-load.service \ + getty@.service \ + serial-getty@.service \ + console-getty.service \ + systemd-logind.service + +STOPSIGNAL SIGRTMIN+3 + +CMD ["/lib/systemd/systemd"] diff --git a/test/systemd/dns-test0.nmconnection b/test/systemd/dns-test0.nmconnection new file mode 100644 index 0000000..6c4570a --- /dev/null +++ b/test/systemd/dns-test0.nmconnection @@ -0,0 +1,13 @@ +[connection] +id=dns-test0 +type=dummy +interface-name=dns-test0 +autoconnect=true + +[ipv4] +method=manual +address1=10.99.99.2/24 +dns=8.8.8.8;1.1.1.1; + +[ipv6] +method=disabled diff --git a/test/systemd/setup-resolvconf-nm.service b/test/systemd/setup-resolvconf-nm.service new file mode 100644 index 0000000..6945382 --- /dev/null +++ b/test/systemd/setup-resolvconf-nm.service @@ -0,0 +1,12 @@ +[Unit] +Description=Unmount Docker's bind-mounted /etc/resolv.conf so NetworkManager can manage it +DefaultDependencies=no +Before=NetworkManager.service +After=local-fs.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/setup-resolvconf-nm.sh + +[Install] +WantedBy=sysinit.target diff --git a/test/systemd/setup-resolvconf-nm.sh b/test/systemd/setup-resolvconf-nm.sh new file mode 100644 index 0000000..8dcdddb --- /dev/null +++ b/test/systemd/setup-resolvconf-nm.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Unmount Docker's bind-mounted /etc/resolv.conf so NetworkManager can manage it. +# NM writes resolv.conf via a temp file + rename(); rename() over a bind-mount +# target fails with EBUSY, so NM's writes would silently fail without this. +# +# After unmounting, seed a placeholder resolv.conf so early boot DNS works; NM +# will overwrite it with DNS servers from its managed connection profile once +# NetworkManager.service starts. + +umount /etc/resolv.conf 2>/dev/null || true +rm -f /etc/resolv.conf +printf "nameserver 8.8.8.8\nnameserver 1.1.1.1\n" > /etc/resolv.conf diff --git a/test/systemd/test-systemd.sh b/test/systemd/test-systemd.sh index ccf01c9..7f1c2cd 100755 --- a/test/systemd/test-systemd.sh +++ b/test/systemd/test-systemd.sh @@ -66,16 +66,22 @@ assert_dig_retry() { return 1 } -# Assert a dig query returns empty / NXDOMAIN (no match). +# Assert a dig query does NOT resolve to an IP. +# dig +short prints timeout/connection-error diagnostics (e.g. +# ";; communications error to 127.0.0.53#53: timed out") to stdout, so a simple +# emptiness check would misread those as a resolution. Only a bare IPv4 line +# counts as "resolved" — everything else means "not resolving," which is what +# this helper is checking for. assert_dig_empty() { local dig_args="$1" local description="$2" result=$(docker exec "$CONTAINER_NAME" dig $dig_args +short 2>/dev/null || true) - if [ -z "$result" ]; then - pass "$description -> empty (as expected)" + ips=$(echo "$result" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || true) + if [ -z "$ips" ]; then + pass "$description -> not resolving (as expected)" else - fail "$description (expected empty, got '$result')" + fail "$description (expected no IP, got '$ips')" fi } @@ -234,32 +240,53 @@ else fail "docker-dns service is NOT enabled" fi -# Config assertion — branch on whether systemd-resolved is active -HAS_RESOLVED=false +# Config assertion — detect which of the three postinst branches was taken. +# Mirrors postinst's logic: resolved first, then NetworkManager, then plain resolv.conf. +MODE=resolvconf if docker exec "$CONTAINER_NAME" systemctl is-active --quiet systemd-resolved 2>/dev/null; then - HAS_RESOLVED=true + MODE=resolved +elif docker exec "$CONTAINER_NAME" systemctl is-active --quiet NetworkManager 2>/dev/null; then + MODE=nm fi - -if [ "$HAS_RESOLVED" = true ]; then - if docker exec "$CONTAINER_NAME" test -f /etc/systemd/resolved.conf.d/docker-dns.conf; then - pass "resolved drop-in config exists" - if docker exec "$CONTAINER_NAME" grep -q 'Domains=.*~docker' /etc/systemd/resolved.conf.d/docker-dns.conf; then - pass "resolved drop-in contains Domains=~docker" +echo "Detected resolver-integration mode: $MODE" + +case "$MODE" in + resolved) + if docker exec "$CONTAINER_NAME" test -f /etc/systemd/resolved.conf.d/docker-dns.conf; then + pass "resolved drop-in config exists" + if docker exec "$CONTAINER_NAME" grep -q 'Domains=.*~docker' /etc/systemd/resolved.conf.d/docker-dns.conf; then + pass "resolved drop-in contains Domains=~docker" + else + fail "resolved drop-in missing Domains=~docker" + docker exec "$CONTAINER_NAME" cat /etc/systemd/resolved.conf.d/docker-dns.conf + fi else - fail "resolved drop-in missing Domains=~docker" - docker exec "$CONTAINER_NAME" cat /etc/systemd/resolved.conf.d/docker-dns.conf + fail "resolved drop-in config does NOT exist" fi - else - fail "resolved drop-in config does NOT exist" - fi -else - if docker exec "$CONTAINER_NAME" grep -q 'nameserver 127.0.0.153' /etc/resolv.conf; then - pass "/etc/resolv.conf contains nameserver 127.0.0.153" - else - fail "/etc/resolv.conf missing nameserver 127.0.0.153" - docker exec "$CONTAINER_NAME" cat /etc/resolv.conf - fi -fi + ;; + nm) + if docker exec "$CONTAINER_NAME" test -x /etc/NetworkManager/dispatcher.d/docker-dns; then + pass "NM dispatcher script exists and is executable" + else + fail "NM dispatcher script missing or not executable" + docker exec "$CONTAINER_NAME" ls -l /etc/NetworkManager/dispatcher.d/ 2>/dev/null || true + fi + if docker exec "$CONTAINER_NAME" grep -q 'nameserver 127.0.0.153' /etc/resolv.conf; then + pass "/etc/resolv.conf contains nameserver 127.0.0.153" + else + fail "/etc/resolv.conf missing nameserver 127.0.0.153" + docker exec "$CONTAINER_NAME" cat /etc/resolv.conf + fi + ;; + resolvconf) + if docker exec "$CONTAINER_NAME" grep -q 'nameserver 127.0.0.153' /etc/resolv.conf; then + pass "/etc/resolv.conf contains nameserver 127.0.0.153" + else + fail "/etc/resolv.conf missing nameserver 127.0.0.153" + docker exec "$CONTAINER_NAME" cat /etc/resolv.conf + fi + ;; +esac # Start a test Docker container for DNS resolution tests echo "" @@ -282,6 +309,24 @@ echo "--- System-level resolution tests (dig without @) ---" assert_dig_retry "test-nginx.docker" "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$" \ "System: dig test-nginx.docker resolves through system resolver" +# ===================================================================== +# Phase 5b: NetworkManager survivability (desktop variants only) +# ===================================================================== +# On real Debian Desktop, NetworkManager writes /etc/resolv.conf directly (dns=default +# plugin) and overwrites docker-dns's prepended nameserver on restart/DHCP-renewal. +# On Ubuntu Desktop, NM delegates DNS to systemd-resolved, so the ~docker routing +# domain survives. We exercise the system resolver (bare `dig`, no @127.0.0.153) so +# this assertion fails precisely when NM has clobbered docker-dns from resolv.conf. +if docker exec "$CONTAINER_NAME" systemctl is-active --quiet NetworkManager 2>/dev/null; then + echo "" + echo "=== Phase 5b: NetworkManager survivability ===" + echo "NetworkManager is active — restarting it and re-checking system resolver" + docker exec "$CONTAINER_NAME" systemctl restart NetworkManager + sleep 3 + assert_dig_retry "test-nginx.docker" "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$" \ + "docker-dns resolution survives NetworkManager restart" +fi + # ===================================================================== # Phase 6: Uninstall the .deb package # ===================================================================== @@ -320,21 +365,37 @@ else fail "docker-dns service is still active after uninstall" fi -# DNS config cleaned -if [ "$HAS_RESOLVED" = true ]; then - if docker exec "$CONTAINER_NAME" test ! -f /etc/systemd/resolved.conf.d/docker-dns.conf; then - pass "resolved drop-in config removed" - else - fail "resolved drop-in config still exists after uninstall" - fi -else - if ! docker exec "$CONTAINER_NAME" grep -q 'nameserver 127.0.0.153' /etc/resolv.conf 2>/dev/null; then - pass "nameserver 127.0.0.153 removed from /etc/resolv.conf" - else - fail "nameserver 127.0.0.153 still in /etc/resolv.conf after uninstall" - docker exec "$CONTAINER_NAME" cat /etc/resolv.conf - fi -fi +# DNS config cleaned — branch on the same MODE detected pre-install. +case "$MODE" in + resolved) + if docker exec "$CONTAINER_NAME" test ! -f /etc/systemd/resolved.conf.d/docker-dns.conf; then + pass "resolved drop-in config removed" + else + fail "resolved drop-in config still exists after uninstall" + fi + ;; + nm) + if docker exec "$CONTAINER_NAME" test ! -e /etc/NetworkManager/dispatcher.d/docker-dns; then + pass "NM dispatcher script removed" + else + fail "NM dispatcher script still exists after uninstall" + fi + if ! docker exec "$CONTAINER_NAME" grep -q 'nameserver 127.0.0.153' /etc/resolv.conf 2>/dev/null; then + pass "nameserver 127.0.0.153 removed from /etc/resolv.conf" + else + fail "nameserver 127.0.0.153 still in /etc/resolv.conf after uninstall" + docker exec "$CONTAINER_NAME" cat /etc/resolv.conf + fi + ;; + resolvconf) + if ! docker exec "$CONTAINER_NAME" grep -q 'nameserver 127.0.0.153' /etc/resolv.conf 2>/dev/null; then + pass "nameserver 127.0.0.153 removed from /etc/resolv.conf" + else + fail "nameserver 127.0.0.153 still in /etc/resolv.conf after uninstall" + docker exec "$CONTAINER_NAME" cat /etc/resolv.conf + fi + ;; +esac # System DNS still works after uninstall echo ""