Ubuntu Networking, Untangled
netplan, NetworkManager, networkd, and systemd-resolved — and Why Your DNS Comes from the Wrong Place
Ubuntu networking is confusing in a specific way: there are three layers doing the same job, and nobody tells you which one is actually in charge on your box. You edit a file, the change works, you reboot, and it is gone. You set a DNS server, the box ignores it. You join the box to Active Directory, and DNS stops working for the domain you just joined. None of this is your fault. It is the stack, and the stack is explainable — once you know which layer owns what, and which file you are actually supposed to edit.
/etc/netplan/ — usually 50-cloud-init.yaml on a cloud or template install, or a file you write yourself — describe what the network should look like: which interface, DHCP or static, which DNS servers, which routes. netplan itself configures nothing. It is a description, and it generates configuration for the layer below it.The renderer is what applies it. A netplan file names a renderer:
networkd or NetworkManager. On Ubuntu Server the default renderer is networkd (systemd-networkd); on Ubuntu Desktop it is NetworkManager. The renderer reads the generated configuration and owns the interfaces, the addresses, and the DHCP client. Two renderers, one box, and only one of them is in charge — check which with grep renderer /etc/netplan/*.yaml, because editing NetworkManager’s configuration on a networkd box does nothing, and the reverse is equally silent.systemd-resolved is the DNS layer. It is a local caching resolver that listens on 127.0.0.53, and it is what your applications actually ask when they resolve a name. It gets its upstream servers from the renderer (from DHCP, or from the DNS you set in netplan), and it serves answers from its cache. The chain is: application → resolved (127.0.0.53) → upstream DNS → answer. Every DNS problem on Ubuntu lives somewhere in that chain, and the first step in debugging is always finding which link is broken, not changing a setting at random and hoping.
The rule that saves you: edit netplan, never the generated files, and never the files the layers below it own.
netplan apply regenerates the renderer’s configuration and restarts it. If you edited a generated file by hand, the next netplan apply — or the next reboot, or the next cloud-init run — will overwrite it, and your change will vanish without a trace. That is not a bug. That is the design, and it is the design that eats everyone’s afternoon exactly once./etc/resolv.conf is a symlink to /run/systemd/resolve/stub-resolv.conf. Its contents are generated by systemd-resolved, and they say, in effect: “ask 127.0.0.53”. That is correct. The nameserver in that file is not your DNS server; it is the local stub, and the real upstream servers live in resolved’s configuration, per link. Editing resolv.conf directly does nothing durable — resolved regenerates it — and the edit that “worked” for ten minutes worked only until resolved rewrote the file.Where DNS actually comes from, in order: the DNS servers you set in netplan (per interface, in the YAML), then the DNS servers DHCP hands out (if the interface uses DHCP and you did not override them), then the global fallback in
/etc/systemd/resolved.conf. The per-link setting wins over DHCP; DHCP wins over the global fallback. If you want a fixed DNS server on a DHCP interface, set it in netplan under that interface — do not set it in resolved.conf and wonder why DHCP still wins, and do not edit resolv.conf and wonder why it reverts.The commands that show the truth:
resolvectl status — every link, its DNS servers, its search domains, and which one is the default route for DNS.resolvectl query example.com — ask resolved, and see which server answered.ls -l /etc/resolv.conf — if it is not a symlink to the stub, something else is managing DNS on this box, and that something is the first thing to find.The two modes of resolv.conf, and why they matter: the stub (
/run/systemd/resolve/stub-resolv.conf, nameserver 127.0.0.53) gives you resolved’s caching, per-link routing, and split DNS. The alternative, /run/systemd/resolve/resolv.conf, lists the upstream servers directly and bypasses the stub — useful when an application does not work through the stub, but you lose the per-link behaviour with it. If a legacy application cannot resolve through 127.0.0.53, pointing the symlink at the upstream file is a legitimate fix, and it is a fix you make deliberately, once, not an edit you keep re-applying.The two DHCP settings that matter in netplan:
dhcp4: true — take the address, gateway, DNS, and search domain from DHCP.dhcp4-overrides:
use-dns: false — take the address and gateway from DHCP, but not the DNS servers. This is the setting for a box whose DNS you manage yourself, on a network whose DHCP hands out DNS you do not want. Set it, set your DNS explicitly in the same netplan file, and the DHCP server stops overwriting your resolvers.The lease, when it goes stale: a box that keeps an old lease after a network change — a clone, a moved VM, a DHCP server that changed its scope — will keep the old DNS servers until the lease renews. Force it with a renderer restart (
systemctl restart systemd-networkd, or nmcli connection down/up under NetworkManager), and check resolvectl status afterwards to confirm the new servers arrived. A lease is a cache, and caches go stale.The hostname from DHCP, and why it matters: a DHCP server can hand a box a hostname, and a box that accepts it will rename itself on every lease renewal. If your hostname keeps changing, that is why — and the fix is the same overrides block, with
use-hostname: false. A server whose name changes by itself is a server whose monitoring, logging, and Kerberos identity all break at once.networkctl status, and there is no GUI, no applet, and no interactive state to drift. It is the right renderer for a server, a VM, and anything you manage with configuration management. Its weakness is interactivity: it does not handle Wi-Fi roaming, VPNs that come and go, or a laptop moving between networks, because that was never its job.NetworkManager is the desktop and laptop default. It handles Wi-Fi, VPNs, mobile broadband, and connections that appear and disappear, and it remembers them per-user and per-connection. It is the right renderer for a machine a human carries around. Its weakness on a server is exactly its strength on a laptop: it has interactive state, and a server whose network depends on interactive state is a server that can surprise you after a reboot.
Switching renderers, safely: change the renderer in the netplan YAML, run
netplan apply, and verify with networkctl status or nmcli general status — whichever you switched to should now own the interfaces, and the other should show nothing. Do it from the console or from a second session, the same way you would a firewall change: a renderer switch that goes wrong takes the network with it, and you do not want to be locked out of the box you just reconfigured. And never run both at once on the same interface — two renderers fighting over one interface is a configuration that works until it does not, and the failure looks like a network problem, not a configuration problem.Why it happens: resolved sends a query to the DNS servers it knows about, and the servers it knows about are the DHCP ones, which know nothing about the AD zone. The AD DNS zone is a private zone, served only by the DCs, and a public or ISP resolver will answer “that domain does not exist” — confidently, and wrongly. The fix is not to make the DCs the only DNS servers for everything; it is to tell resolved that the AD domain belongs to the DCs, and everything else belongs to the existing servers. That is split DNS, and resolved does it natively, per link.
Fix one — pin DNS per link in netplan (the durable fix):
network:
version: 2
ethernets:
eth0:
dhcp4: true
dhcp4-overrides:
use-dns: false
nameservers:
addresses: [10.0.0.10, 10.0.0.11]
search: [ad.example.com, example.com]Then
netplan apply and resolvectl status to confirm the DCs are the link’s DNS servers and the AD domain is in the search list. The DHCP server still hands out the address and gateway; it no longer hands out DNS. The DCs answer for the AD zone, and they forward everything else upstream, which is what a DC’s DNS is configured to do.Fix two — routing domains in resolved (the surgical fix): when the box needs the DCs for the AD domain and the existing DNS for everything else, set a routing domain on the link:
resolvectl domain eth0 ad.example.com (or the equivalent in netplan, nameservers: search: with the domain, plus the DC as a per-link DNS server). Queries for ad.example.com go to the DCs; everything else goes to the existing servers. Verify with resolvectl query dc01.ad.example.com and resolvectl query example.com — the first should answer from a DC, the second from the normal path, and resolvectl status should show the routing domain on the link.The sssd side, briefly: sssd needs to resolve the DC and its SRV records (_kerberos._tcp, _ldap._tcp) to find the domain, and it needs the clock right — Kerberos fails on a clock skew of minutes, and a box whose DNS is wrong will fail Kerberos in a way that looks like a clock problem. Check DNS first, with resolvectl, before touching sssd or Kerberos. And check
chronyc tracking second. The number of AD join failures that are actually DNS or time, not AD, is embarrassingly high, and I have paid that tax myself.The search domain, and why short names fail: if
ping dc01 fails but ping dc01.ad.example.com works, the search domain is missing, not the DNS server. The search list comes from DHCP or from netplan’s nameservers: search: — and a DHCP server that hands out no search domain leaves the box unable to complete short names. Set it explicitly in netplan, and the short names start working.grep renderer /etc/netplan/*.yaml — and networkctl status or nmcli general status to confirm it owns the interfaces.2. What does the link have?
ip addr and ip route — address, gateway, and whether the interface is up at all. No address means the problem is DHCP or the link, not DNS.3. What does resolved believe?
resolvectl status — DNS servers, search domains, routing domains, per link. If the servers listed are not the ones you set, find which layer set them: netplan, DHCP, or the global fallback.4. Ask resolved directly.
resolvectl query host.example.com — if it fails, ask the upstream directly with dig @10.0.0.10 host.example.com to separate a resolved problem from an upstream problem. If dig works and resolvectl does not, the problem is in resolved’s configuration. If dig fails too, the problem is the upstream server or the network to it.5. Check the symlink.
ls -l /etc/resolv.conf — if it is not the stub, something else owns DNS, and that something is the first thing to identify before changing anything else.netplan apply — never the generated files, never resolv.confgrep renderer /etc/netplan/*.yaml; server = networkd, desktop/laptop = NetworkManagerresolvectl statusdhcp4-overrides: use-dns: false, then set nameservers in netplandig @<server> vs resolvectl queryCommands verified against Ubuntu 22.04/24.04 behaviour (netplan, systemd-networkd, NetworkManager, systemd-resolved) and Active Directory join practice with realmd/sssd, November 7, 2026. Renderer defaults and file locations vary by Ubuntu release and install type — check the renderer on the box in front of you before editing. Example addresses and domains (10.0.0.10, ad.example.com) are illustrative — substitute your own.
