Summary
When running netclient inside a Docker container with network_mode: host and /etc/resolv.conf bind-mounted from a systemd-resolved host, netclient fails to detect the systemd environment and falls back to fileManager instead of systemdStubManager. This causes DNS for the netmaker interface to never be registered with systemd-resolved, so VPN hostnames (e.g. *.nm.internal) resolve correctly inside the container but not from the host shell.
Environment
- netclient: v1.5.1
- OS: Ubuntu 22.04 (systemd-resolved active, stub mode)
- Deployment: Docker container,
network_mode: host, privileged: true, pid: host
/etc/resolv.conf bind-mounted into container: -v /etc/resolv.conf:/etc/resolv.conf:rw
Steps to Reproduce
- Run netclient in a Docker container with the following compose config:
services:
netclient:
network_mode: host
privileged: true
pid: host
volumes:
- /etc/resolv.conf:/etc/resolv.conf:rw
- /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro
-
The host's /etc/resolv.conf is a symlink to /run/systemd/resolve/stub-resolv.conf (standard Ubuntu 22.04 setup).
-
Netclient joins a network successfully. VPN interface comes up, WireGuard peers establish handshakes.
-
From inside the container: VPN hostnames resolve correctly.
-
From the host shell: VPN IP addresses are reachable (ping 100.64.0.x works), but VPN hostnames fail (nslookup *.nm.internal → NXDOMAIN).
Root Cause
getResolvconfFlavor() in dns/config/manager_linux.go detects the systemd environment exclusively by checking whether /etc/resolv.conf is a symlink:
stat, err := os.Lstat("/etc/resolv.conf")
// ...
if stat.Mode()&os.ModeSymlink == os.ModeSymlink {
target, err := os.Readlink("/etc/resolv.conf")
if strings.HasSuffix(target, "/run/systemd/resolve/stub-resolv.conf") {
return systemdStub, nil
}
// ...
}
// Falls through to resolvconf / file detection
When Docker bind-mounts /etc/resolv.conf, it resolves the symlink on the host and mounts the target file directly. Inside the container, /etc/resolv.conf appears as a plain regular file — the symlink is gone. The symlink check fails, systemd is never detected, and netclient falls back to fileManager.
With fileManager, netclient writes nameserver <vpn_ip> and search nm.internal directly into /etc/resolv.conf. Since this file is the bind-mounted target of the host's symlink (/run/systemd/resolve/stub-resolv.conf), systemd-resolved owns and periodically rewrites it — clobbering netclient's changes on every systemd-resolved restart. The result is that DNS for the VPN interface works intermittently, or only immediately after the container starts.
The correct path — systemdStubManager — would call resolvectl dns netmaker <vpn_ip> and resolvectl domain netmaker ~nm.internal nm.internal, which registers DNS persistently with systemd-resolved and survives restarts.
Confirmed observations:
- Inside the container:
stat /etc/resolv.conf → regular file, Links: 0 (bind-mounted target, not a symlink)
resolvectl status on host → netmaker interface shows Current Scopes: none (no DNS registered)
resolvectl binary inside the container works and communicates with the host's systemd-resolved via the dbus socket
- After container restart, hostname resolution works briefly, then breaks when systemd-resolved next rewrites the stub file
Why the Bind Mount Is Needed
The /etc/resolv.conf bind mount is the standard and necessary deployment pattern for network_mode: host containers that embed netclient. Without it, netclient cannot write DNS config to the host at all — only the container's own ephemeral resolv.conf is modified, which has no effect on the host shell.
The upstream netclient Docker image (gravitl/netclient) avoids this issue because it is Alpine-based and installs openresolv. Inside that container, resolvconf is present and getResolvconfFlavor() detects openresolv — a code path that does not depend on the symlink check at all. This bug only surfaces when netclient is embedded in a non-Alpine container image (e.g. Debian/Ubuntu-based) on a systemd-resolved host, which is a valid and increasingly common deployment pattern.
Suggested Fix
Add a fallback check in getResolvconfFlavor(): if /etc/resolv.conf is not a symlink, also check whether systemd-resolved is actively running before falling through to file/resolvconf detection.
func getResolvconfFlavor() (resolvconfFlavor, error) {
stat, err := os.Lstat("/etc/resolv.conf")
if err != nil {
return unknown, err
}
if stat.Mode()&os.ModeSymlink == os.ModeSymlink {
target, err := os.Readlink("/etc/resolv.conf")
if err != nil {
return unknown, err
}
if strings.HasSuffix(target, "/run/systemd/resolve/stub-resolv.conf") {
return systemdStub, nil
} else if strings.HasSuffix(target, "/run/systemd/resolve/resolv.conf") {
return systemdUplink, nil
}
}
// Fallback: /etc/resolv.conf is not a symlink (e.g. bind-mounted into a container from a
// systemd-resolved host — Docker resolves the symlink and mounts the target file directly).
// Check if systemd-resolved is running before falling through to resolvconf/file detection.
if exec.Command("systemctl", "is-active", "--quiet", "systemd-resolved").Run() == nil {
if isSystemdStubMode() {
return systemdStub, nil
}
return systemdUplink, nil
}
// ... existing resolvconf / file detection unchanged
}
func isSystemdStubMode() bool {
// Read /etc/resolv.conf directly — works on bare hosts (symlink target) and inside
// containers where the file is bind-mounted from the host. Do NOT read
// /run/systemd/resolve/stub-resolv.conf: that path may exist inside the container
// image itself (baked in at build time) and does not reflect the host's DNS mode.
data, err := os.ReadFile("/etc/resolv.conf")
if err != nil {
return false
}
return strings.Contains(string(data), "127.0.0.53")
}
This change is backward-compatible: the symlink check still runs first (no behavior change on non-containerized hosts), and the systemctl fallback only fires when the symlink is absent.
Additional Context
This issue affects any deployment pattern where netclient runs inside a container on a systemd-resolved host with a bind-mounted /etc/resolv.conf — which is the necessary approach for network_mode: host containerized deployments where the host's DNS must be updated.
The release.md for v1.5.1 notes a related DNS limitation:
systemd-resolved DNS limitation: On systems using systemd-resolved in uplink mode, only the first 3 entries in resolv.conf are honored. Stub mode is recommended.
This bug is distinct from that limitation: it affects stub mode as well, and prevents systemd-resolved from being configured at all rather than being a capacity constraint.
Summary
When running netclient inside a Docker container with
network_mode: hostand/etc/resolv.confbind-mounted from a systemd-resolved host, netclient fails to detect the systemd environment and falls back tofileManagerinstead ofsystemdStubManager. This causes DNS for the netmaker interface to never be registered with systemd-resolved, so VPN hostnames (e.g.*.nm.internal) resolve correctly inside the container but not from the host shell.Environment
network_mode: host,privileged: true,pid: host/etc/resolv.confbind-mounted into container:-v /etc/resolv.conf:/etc/resolv.conf:rwSteps to Reproduce
The host's
/etc/resolv.confis a symlink to/run/systemd/resolve/stub-resolv.conf(standard Ubuntu 22.04 setup).Netclient joins a network successfully. VPN interface comes up, WireGuard peers establish handshakes.
From inside the container: VPN hostnames resolve correctly.
From the host shell: VPN IP addresses are reachable (
ping 100.64.0.xworks), but VPN hostnames fail (nslookup *.nm.internal→ NXDOMAIN).Root Cause
getResolvconfFlavor()indns/config/manager_linux.godetects the systemd environment exclusively by checking whether/etc/resolv.confis a symlink:When Docker bind-mounts
/etc/resolv.conf, it resolves the symlink on the host and mounts the target file directly. Inside the container,/etc/resolv.confappears as a plain regular file — the symlink is gone. The symlink check fails, systemd is never detected, and netclient falls back tofileManager.With
fileManager, netclient writesnameserver <vpn_ip>andsearch nm.internaldirectly into/etc/resolv.conf. Since this file is the bind-mounted target of the host's symlink (/run/systemd/resolve/stub-resolv.conf), systemd-resolved owns and periodically rewrites it — clobbering netclient's changes on every systemd-resolved restart. The result is that DNS for the VPN interface works intermittently, or only immediately after the container starts.The correct path —
systemdStubManager— would callresolvectl dns netmaker <vpn_ip>andresolvectl domain netmaker ~nm.internal nm.internal, which registers DNS persistently with systemd-resolved and survives restarts.Confirmed observations:
stat /etc/resolv.conf→ regular file,Links: 0(bind-mounted target, not a symlink)resolvectl statuson host → netmaker interface showsCurrent Scopes: none(no DNS registered)resolvectlbinary inside the container works and communicates with the host's systemd-resolved via the dbus socketWhy the Bind Mount Is Needed
The
/etc/resolv.confbind mount is the standard and necessary deployment pattern fornetwork_mode: hostcontainers that embed netclient. Without it, netclient cannot write DNS config to the host at all — only the container's own ephemeral resolv.conf is modified, which has no effect on the host shell.The upstream netclient Docker image (
gravitl/netclient) avoids this issue because it is Alpine-based and installsopenresolv. Inside that container,resolvconfis present andgetResolvconfFlavor()detectsopenresolv— a code path that does not depend on the symlink check at all. This bug only surfaces when netclient is embedded in a non-Alpine container image (e.g. Debian/Ubuntu-based) on a systemd-resolved host, which is a valid and increasingly common deployment pattern.Suggested Fix
Add a fallback check in
getResolvconfFlavor(): if/etc/resolv.confis not a symlink, also check whether systemd-resolved is actively running before falling through to file/resolvconf detection.This change is backward-compatible: the symlink check still runs first (no behavior change on non-containerized hosts), and the
systemctlfallback only fires when the symlink is absent.Additional Context
This issue affects any deployment pattern where netclient runs inside a container on a systemd-resolved host with a bind-mounted
/etc/resolv.conf— which is the necessary approach fornetwork_mode: hostcontainerized deployments where the host's DNS must be updated.The
release.mdfor v1.5.1 notes a related DNS limitation:This bug is distinct from that limitation: it affects stub mode as well, and prevents systemd-resolved from being configured at all rather than being a capacity constraint.