Browse Source

base: resync /etc/resolv.conf into Network=host containers

With Network=host, Podman snapshots /etc/resolv.conf at container creation
and bind-mounts the copy, so a later DNS change (DHCP renewal, renumbering,
router replacement) never reaches running containers: they keep querying the
old, now-dead resolvers until recreated, silently.

Add a host-level mechanism in the base cookbook:

- a systemd.path unit watching /etc/resolv.conf (catches any writer, including
  atomic rename-into-place);
- a oneshot service running a dedicated script that rewrites each Network=host
  container's snapshot in place (same inode, so the bind-mount stays valid).

Rewriting the content in place is picked up at runtime with no restart by both
glibc (reloads on mtime change) and musl (stateless, re-reads every lookup),
verified empirically. Scoped to NetworkMode=host only: bridged containers
legitimately point at aardvark-dns and must not be touched.

Fixes #1

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix/network-host-resolv-conf
Nicolas Massé 2 weeks ago
parent
commit
9771f18de7
  1. 5
      cookbooks/base/Makefile
  2. 6
      cookbooks/base/README.md
  3. 43
      cookbooks/base/config/podman-resolv-conf-sync.sh
  4. 12
      cookbooks/base/podman-resolv-conf-sync.path
  5. 8
      cookbooks/base/podman-resolv-conf-sync.service

5
cookbooks/base/Makefile

@ -2,8 +2,13 @@
SYSTEMD_MAIN_UNIT_NAMES := var-lib-virtiofs-data.mount
SYSTEMD_MAIN_UNIT_NAMES += rpm-ostree-install-qemu-guest-agent.service
SYSTEMD_MAIN_UNIT_NAMES += install-tools.service
# .path units are not picked up by common.mk's wildcard (only .service/.target/
# .timer/.mount), so install and enable/start it explicitly. It triggers the
# path-scoped podman-resolv-conf-sync.service oneshot.
SYSTEMD_MAIN_UNIT_NAMES += podman-resolv-conf-sync.path
TARGET_FILES += $(TARGET_CHROOT)/etc/quadlets/base/install-tools.d
TARGET_FILES += $(TARGET_CHROOT)/etc/systemd/system/podman-resolv-conf-sync.path
# Include common Makefile
include ../../scripts/common.mk

6
cookbooks/base/README.md

@ -8,6 +8,12 @@ The base cookbook provides foundational configuration for Fedora CoreOS systems.
- **tmpfiles configuration**: Sets up required directories such as `/var/lib/virtiofs/data`.
- **QEMU guest agent**: Optional installation for better VM integration.
- **SSH key persistence**: Backs up and restores SSH host keys across reboots.
- **resolv.conf resync**: Keeps `Network=host` containers in sync with the host's DNS.
With `Network=host`, Podman snapshots `/etc/resolv.conf` at container creation and bind-mounts
the copy, so a later DNS change (e.g. a DHCP renewal) never reaches running containers. A
`systemd.path` unit watches `/etc/resolv.conf` and a oneshot service rewrites each host-network
container's snapshot **in place** (same inode, no restart); bridged containers are left untouched.
See [issue #1](https://github.com/nmasse-itix/podman-quadlet-cookbook/issues/1).
This cookbook is used as an implicit dependency for other cookbooks that run on Fedora CoreOS.

43
cookbooks/base/config/podman-resolv-conf-sync.sh

@ -0,0 +1,43 @@
#!/bin/bash
#
# Resync the host's /etc/resolv.conf into the frozen snapshot that Podman
# bind-mounts into every Network=host container.
#
# Why: with Network=host, Podman copies /etc/resolv.conf at container *creation*
# time into .../userdata/resolv.conf and bind-mounts that copy. It is a snapshot,
# not a view: when the host's DNS servers change (DHCP renewal, renumbering,
# router replacement, ...), every running container keeps querying the old,
# now-dead resolvers — silently, until it is recreated.
#
# Fix: overwrite the snapshot's *content* in place (same inode, so the bind-mount
# stays valid). Both glibc (reloads when the file's mtime changes) and musl
# (stateless, re-reads on every lookup) then pick up the new resolvers at runtime
# with no restart and no downtime.
#
# Scope: Network=host containers ONLY. Bridged containers legitimately point at
# aardvark-dns (e.g. nameserver 10.89.0.1), which already follows the host's DNS
# live; overwriting their resolv.conf would break inter-container name resolution.
set -Eeuo pipefail
host_resolv=/etc/resolv.conf
for id in $(podman ps -q); do
# Only touch containers that share the host network stack.
mode="$(podman inspect --format '{{.HostConfig.NetworkMode}}' "$id" 2>/dev/null)" || continue
[ "$mode" = host ] || continue
snapshot="$(podman inspect --format '{{.ResolvConfPath}}' "$id" 2>/dev/null)" || continue
[ -n "$snapshot" ] && [ -f "$snapshot" ] || continue
if ! cmp -s "$host_resolv" "$snapshot"; then
name="$(podman inspect --format '{{.Name}}' "$id" 2>/dev/null || echo "$id")"
echo "Refreshing resolv.conf for container ${name} (${snapshot})"
# In-place truncation: preserves the inode the container has bind-mounted.
if ! cat "$host_resolv" > "$snapshot"; then
echo "Failed to refresh resolv.conf for container ${name}!" >&2
fi
fi
done
exit 0

12
cookbooks/base/podman-resolv-conf-sync.path

@ -0,0 +1,12 @@
[Unit]
Description=Watch /etc/resolv.conf and resync it into Network=host containers
Documentation=https://github.com/nmasse-itix/podman-quadlet-cookbook/issues/1
[Path]
# Fires on any writer (NetworkManager, manual edit, ...), including atomic
# rename-into-place, since systemd also watches the parent directory.
PathChanged=/etc/resolv.conf
Unit=podman-resolv-conf-sync.service
[Install]
WantedBy=multi-user.target

8
cookbooks/base/podman-resolv-conf-sync.service

@ -0,0 +1,8 @@
[Unit]
Description=Resync /etc/resolv.conf into running Network=host Podman containers
Documentation=https://github.com/nmasse-itix/podman-quadlet-cookbook/issues/1
# Triggered by podman-resolv-conf-sync.path whenever /etc/resolv.conf changes.
[Service]
Type=oneshot
ExecStart=/etc/quadlets/base/podman-resolv-conf-sync.sh
Loading…
Cancel
Save