Nothing in the logs looks wrong: the interface is up, the resolver reports the VPN's DNS server, and the file on disk lists it. Yet an internal hostname still resolves through the local router. The three statements are not contradictory, because the file, the per-link state and the lookup path are different objects that happen to share a name. What follows is the path a query actually takes on a systemd-resolved host.

Three Objects Called resolv.conf

On a resolved host, /etc/resolv.conf is usually a symlink to /run/systemd/resolve/stub-resolv.conf. That target holds the address of a local stub listener, 127.0.0.53, plus the search domains resolved computed. It is a view of state rather than the state itself.

The state lives in resolved's per-link configuration, which is why resolvectl status can disagree with the file and still be right. A link carries its own servers and its own routing domains, and the stub consults that per-link data when a query arrives. The systemd-resolved manual describes the generated files and the per-link model; the split between view and state is the part worth keeping. Editing the symlink target changes what traditional clients read, not what the resolver knows.

A third object, /run/systemd/resolve/resolv.conf, flattens every link into one server list. A program pointed at that file sends queries straight to the listed servers and skips the stub, so per-link policy no longer applies to it. Two programs on the same machine can therefore answer the same name differently, and neither is broken.

Trace One Lookup Through NSS

glibc reads /etc/nsswitch.conf to decide who answers a name. A hosts line containing resolve routes the question to nss-resolve, which asks resolved over D-Bus and receives the per-link answer. A line with only files dns sends the query to whatever address /etc/resolv.conf holds.

That one line explains most disagreements between a browser and dig. dig reads resolv.conf directly and never touches NSS, so it tests the stub or an upstream server rather than the path the application uses. getent hosts internal.example goes through NSS, and comparing its answer with dig's is the first useful measurement.

Applications that bundle a resolver, or speak DNS over HTTPS to a fixed endpoint, bypass both paths. Nothing in resolv.conf or in the resolved state constrains them.

Check Who Owns the File and Where It Points

Read the link chain before changing anything, because ownership decides which file is authoritative. Every command below only reads state.

stat -c '%N' /etc/resolv.conf
resolvectl status --no-pager
grep '^hosts' /etc/nsswitch.conf
ls -l /run/systemd/resolve/

The first line prints the link and its target together, and a plain file owned by a VPN script means the resolver never treats that write as input. The status output lists per-link servers plus the routing domains that decide where each query goes. The nsswitch line says whether the stub is consulted at all, so a stub address in the file with no resolve there means queries reach the local listener while nothing manages the split. That last listing matters too, because a host without stub-resolv.conf is not running the arrangement described here.

If the chain ends at a file another package manages, the VPN client and that package are the two owners to reconcile. Replacing the link discards one owner's work and leaves the other's in place.

Repair at the Link Owner

A VPN connection should register its servers and its domains on its own link, then withdraw them on disconnect. NetworkManager's resolved integration and a client's own helper both work this way; a script that only rewrites resolv.conf does not.

On a link that already exists, runtime settings show the mechanism. resolvectl dns tun0 10.40.0.53 attaches a server, and resolvectl domain tun0 '~corp.example' attaches a routing domain. The tilde is the load-bearing character: it confines queries for that suffix to this link and leaves everything else alone, while the same value without it becomes a search domain and the split evaporates. Those addresses are lab examples, not a persistent configuration. Confirm with resolvectl query host.corp.example and read which link answered.

A registered server still needs a route. When the query reaches the right link and times out, the fault is reachability through the tunnel rather than resolver ownership.

What a Working State Looks Like

A healthy result is consistent across all three views: the per-link servers match what the VPN handed out, the routing domains cover only internal suffixes, and the application that matters resolves an internal name while a public name still follows the local path. One successful query proves less than that agreement does.

Then disconnect and repeat both queries. A client that leaves its domains registered after teardown produces a machine that fails internal lookups hours later, with no tunnel open and no obvious cause.

Allakkat attuumassuteqartut