You’re Debugging DNS in the Wrong Namespace
Months of debugging DNS inside a WireGuard tunnel, only to find out my resolv.conf fix never left the host namespace. The tunnel was up, routes looked right, but the application kept timing out on name resolution. I checked the resolver IP, the firewall rules, even the WireGuard handshake stats. Everything pointed to a working configuration. The problem was simpler and more annoying: the application was resolving DNS in its own namespace, not the one I was debugging.
Network namespaces give you isolated network stacks. That’s the point. The problem is that most modern Linux environments create them whether you asked for one or not. Docker, Kubernetes, certain systemd services, some VPN setups. Your application lands in a namespace, and every network interface, every routing table entry, every DNS resolver inside that namespace is invisible from the outside unless you know where to look.
Which namespace is that process even in?
Start with the process, not the namespace. Every Linux process has a /proc/[PID]/ns/net symlink that points to its network namespace. Read it.
ls -la /proc/$(pgrep -f myapp)/ns/net
The inode number in the output tells you which namespace the process belongs to. Two processes sharing the same inode are on the same network stack. Two processes with different inodes cannot see each other’s interfaces, cannot reach each other’s loopback, and resolve DNS through completely separate resolv.conf files.
Compare it against your shell’s namespace:
# Your shell's namespace
readlink /proc/self/ns/net
# Target process's namespace
readlink /proc/$(pgrep -f myapp)/ns/net
# Are they the same?
if [ "$(readlink /proc/self/ns/net)" = "$(readlink /proc/$(pgrep -f myapp)/ns/net)" ]; then
echo "same namespace"
else
echo "different namespace"
fi
If they’re different, stop debugging DNS from your terminal. You’re looking at the wrong network stack.
Drop into the namespace and test from there
nsenter gets you into the target namespace without restarting anything. Find the PID, then:
nsenter -t $(pgrep -f myapp) -n bash
The -n flag enters the network namespace only. You now share the process’s network interfaces, routing table, and DNS configuration. Test from here:
# What DNS server am I using?
cat /etc/resolv.conf
# Does resolution work?
host example.com
# What UDP port 53 traffic looks like
tcpdump -i any port 53 -n
Common finding: the namespace has a different resolv.conf than the host. Docker containers default to their own. systemd services with PrivateNetwork=yes get an empty or stub config. VPN namespaces configured by certain daemons inherit nothing. Your host-level fix never propagated because the namespace never sees host files.
The enumeration problem
Sometimes you don’t know which namespace to enter. List them all:
# Show all unique network namespaces
ls -la /proc/*/ns/net 2>/dev/null | awk '{print $NF}' | sort -u
# Count processes per namespace
for ns in $(ls -la /proc/*/ns/net 2>/dev/null | awk '{print $NF}' | sort -u); do
count=$(ls -la /proc/*/ns/net 2>/dev/null | awk '{print $NF}' | grep -c "$ns")
echo "$count processes in $ns"
done
You’ll typically see three or four: the host, one per Docker daemon, maybe one per VPN tunnel. If you see fifteen, something is creating namespaces without cleaning them up.
DNS inside namespaces: the actual path
Once inside the right namespace, trace the actual resolution path. Don’t assume glibc is the only resolver in play. nsswitch.conf decides what gets queried and in what order. Some namespaced environments inject systemd-resolved stubs that send queries to a local socket, not a remote DNS server.
# Check what NSS is doing
getent ahosts example.com
# If systemd-resolved is involved
resolvectl status
# Direct UDP query bypassing everything
dig @$(awk '/^nameserver/{print $2; exit}' /etc/resolv.conf) example.com
If dig works but your application doesn’t, the resolver is right but the application’s NSS configuration is wrong. Check /etc/nsswitch.conf inside the namespace for broken or missing entries in the hosts: line.
Docker assigns containers a namespace-local
resolv.confcopied from the host at creation time. If you change the host’s DNS after the container starts, the container keeps the old config. Restart the container, or use--dnsto override.
The takeaway
Stop debugging DNS from outside the namespace. The host network stack and the namespace network stack don’t share state. A route that works from your terminal doesn’t exist inside the container. A DNS server that responds to dig on the host may be unreachable from the namespace. The fastest path to the answer is nsenter, cat /etc/resolv.conf, and a direct query from inside.
For Docker specifically, docker exec -it container_name bash drops you in the right namespace automatically. For systemd services, use nsenter -t $(pgrep -f service) -n. For WireGuard tunnels, the namespace depends on how the tunnel was configured. If you’re using a userspace implementation like wireguard-go, it runs in your namespace. If you’re using the kernel module, it depends on whether a separate namespace was created for the tunnel interface.