You add a second address to an interface, everything works, and then you reboot. The address is gone.

So you add it again. Then a package upgrade restarts systemd-networkd, and it is gone again. Eventually you put the ip addr add in a script, the script runs before the interface exists, and now you have a race condition on top of a configuration problem.

The reason is not that Linux forgot. It is that something else owns that interface. systemd-networkd manages links for which it finds a matching .network file, and when it brings such a link up it flushes the existing addresses and routes first. Your manual ip command is not configuration to networkd. It is debris that gets cleaned up on the next reconfigure.

This post is about replacing that debris with files networkd actually owns: which file does what, why the wrong file sometimes wins, and the handful of settings whose defaults surprise people.

Three file types, three different jobs

The names look interchangeable. They are not.

  • .link files are processed by systemd-udevd, before networkd gets involved. They set device naming, MAC address policy, MTU, and similar low-level properties.
  • .netdev files tell networkd to create a virtual device: a bridge, bond, VLAN, VXLAN, veth, and so on.
  • .network files match an existing link and assign the IP layer to it: addresses, routes, DNS, DHCP behaviour.

A physical NIC needs a .network file. A bridge needs both a .netdev file (create it) and a .network file (give it an address). The bridge ports need .network files that attach them to the bridge.

If you only remember one thing: your problem is almost always in the matching step, not the address syntax.

Where the files live, and why that matters

networkd reads from four directories:

  • /usr/lib/systemd/network — packages
  • /usr/local/lib/systemd/network — local installs
  • /run/systemd/network — volatile, often generated at boot
  • /etc/systemd/network — local administration

All files are collected and sorted by filename across all four directories. Files with the same filename replace each other, with /etc winning over /run, and /run winning over /usr. A zero-byte file, or a symlink to /dev/null, masks a file of the same name completely.

That masking rule is how you neutralize generated configuration without editing it. If networkctl status eth0 says:

Network File: /run/systemd/network/10-netplan-eth0.network 

then a drop-in is not enough if you want to replace the whole file. Put your own file at /etc/systemd/network/10-netplan-eth0.network with the same name, or mask it with:

sudo ln -s /dev/null /etc/systemd/network/10-netplan-eth0.network 

and write a differently named file that matches the same interface.

The upstream man page recommends a filename prefix below 70. That is not decoration. Files are applied in filename order, and the first matching .network file wins. Later matches are ignored, even if they also match.

The matching contest

systemd.network(5) is explicit: "The first (in alphanumeric order) of the network files that matches a given interface is applied, all later files are ignored, even if they match as well."

So this pair is a bug:

# /etc/systemd/network/50-all.network [Match] Name=* [Network] DHCP=yes 
# /etc/systemd/network/90-eth0.network [Match] Name=eth0 [Network] Address=192.0.2.10/24 

50-all.network matches eth0 first, so 90-eth0.network never applies. The static address in the "more specific" file loses to the generic file purely because of the number.

Two habits prevent this:

  1. Give specific files lower numbers than generic fallbacks.
  2. Verify with networkctl status <if>, which prints the Network File: that actually won.

Also note that a [Match] section with no valid settings matches every interface and produces a warning. If you really mean everything, write Name=* so the intent is explicit.

One more modern escape hatch: if a udev property named ID_NET_MANAGED_BY is set on a device, networkd will not match it at all unless the value is io.systemd.Network. That is a deliberate ownership marker for systems running more than one network manager.

A static configuration that survives reboots

Here is the smallest complete example for a server with a fixed address.

# /etc/systemd/network/10-eth0.network [Match] Name=eth0 [Network] Address=192.0.2.10/24 Gateway=192.0.2.1 DNS=192.0.2.53 Domains=internal.example.com 

Address= in [Network] is shorthand for an [Address] section containing only Address=. Gateway= is shorthand for a [Route] section containing only Gateway=. The long forms are useful when you need more control:

# /etc/systemd/network/10-eth0.network [Match] Name=eth0 [Network] DNS=192.0.2.53 Domains=internal.example.com [Address] Address=192.0.2.10/24 Label=eth0 [Route] Destination=0.0.0.0/0 Gateway=192.0.2.1 Metric=100 

Three details worth knowing:

  • DNS= is read by systemd-resolved, not by networkd's own resolver. If resolved is not running, setting DNS= here will not change what glibc uses.
  • Domains= entries are search domains. A leading tilde makes one routing-only: Domains=~internal.example.com sends queries for that suffix to this link's DNS servers without appending it as a search suffix. Domains=~. is the catch-all routing domain.
  • Metric= on a route is how you choose between two default routes. Lower wins.

Apply it with:

sudo networkctl reload sudo networkctl reconfigure eth0 networkctl status eth0 

networkctl reload re-reads .netdev and .network files and reconfigures interfaces that matched a changed file. networkctl reconfigure re-applies the already loaded configuration; it does not re-read the files. If you edited a file, you need reload first (or just reload, which reconfigures matched links itself).

The part that bites remote admins

Reconfiguring the interface carrying your SSH session can drop the session. Do this on a console, a VM snapshot, or a host where you have a second management path. A typo in a default route is not something you can fix by reconnecting.

DHCP, but with opinions

The usual client config is three lines:

# /etc/systemd/network/20-eth0.network [Match] Name=eth0 [Network] DHCP=yes 

DHCP= accepts yes, no, ipv4, or ipv6, and defaults to no. yes means both families. DHCPv6 can also be triggered by a Router Advertisement even when you did not enable it here, so "I only enabled IPv4" is not always what happens on a dual-stack LAN.

The defaults that matter:

  • [DHCPv4] RouteMetric= defaults to 1024. Routes learned from DHCP, including the default route, get that metric. If a VPN or a second NIC also installs a default route, this number decides the winner.
  • [DHCPv4] UseRoutes= defaults to true, so the server's static routes are installed too, also at metric 1024 unless you change it.
  • [DHCPv4] UseDomains= defaults to no, so the DHCP-provided domain is not used as a search domain. Set UseDomains=route to use it for query routing only, which is usually what you want on a network you do not fully trust.
  • [IPv6AcceptRA] RouteMetric= defaults to 512:1024:2048, mapped to high, medium, and low router preference. Raising it above your wired DHCP metric makes IPv6 politely lose to the wired default route.

A wired-primary, Wi-Fi-backup client looks like this:

# /etc/systemd/network/20-wired.network [Match] Name=eth0 [Network] DHCP=yes [DHCPv4] RouteMetric=100 UseDomains=route [IPv6AcceptRA] RouteMetric=100 
# /etc/systemd/network/30-wifi.network [Match] Name=wlan0 [Network] DHCP=yes [DHCPv4] RouteMetric=600 UseDomains=route [IPv6AcceptRA] RouteMetric=600 

Now the wired default route wins while it exists, and Wi-Fi takes over when the cable is pulled. networkd handles the IP layer; association itself is still the job of wpa_supplicant or your distribution's Wi-Fi stack.

LinkLocalAddressing= is worth a look too. It defaults to ipv6 in the normal case, but to no when the link is a bridge port or has KeepMaster= set. That default explains why a bridge port sometimes has no link-local address even though IPv6 is "enabled."

Creating a bridge and handing out addresses

A bridge needs a .netdev file to exist, then a .network file to get an address. Ports get their own .network files that name the bridge.

# /etc/systemd/network/10-br0.netdev [NetDev] Name=br0 Kind=bridge [Bridge] STP=yes 
# /etc/systemd/network/20-br0.network [Match] Name=br0 [Network] Address=192.0.2.1/24 DHCPServer=yes [DHCPServer] PoolOffset=10 PoolSize=50 EmitDNS=yes DNS=192.0.2.1 
# /etc/systemd/network/30-eth1.network [Match] Name=eth1 [Network] Bridge=br0 

DHCPServer=yes requires an IPv4 address on the interface; networkd uses it as the server address. PoolOffset= and PoolSize= carve a contiguous pool out of that subnet, and the server address is reserved automatically. Default lease time is one hour, maximum is twelve.

If this bridge is your LAN and you want those clients to reach the internet, the uplink interface needs forwarding and NAT, not the bridge:

# /etc/systemd/network/10-wan.network [Match] Name=eth0 [Network] DHCP=ipv4 IPMasquerade=ipv4 

IPMasquerade=ipv4 implies the per-interface forwarding setting and the global one unless you have explicitly set them elsewhere. For IPv6, read the man page carefully: per-interface IPv6Forwarding= does not by itself make packets forward between interfaces; the global setting has to be on.

Reading the state instead of guessing

networkctl list gives you the operational state and, more usefully, the setup state:

IDX LINK TYPE OPERATIONAL SETUP 1 lo loopback carrier unmanaged 2 eth0 ether routable configured 3 virbr0 ether no-carrier unmanaged 
  • configured — networkd applied a matching file successfully.
  • configuring — still waiting on DHCP, RA, duplicate address detection, or carrier.
  • unmanaged — no matching .network file, Unmanaged=yes, or an ownership property told networkd to stay away.
  • failed — configuration was attempted and failed; the journal has the reason.

Then drill in:

networkctl status eth0 journalctl -u systemd-networkd -b --no-pager 

A trimmed status view, with documentation addresses substituted:

● 2: eth0 State: routable (configured) Address: 192.0.2.10 Gateway: 192.0.2.1 Network File: /etc/systemd/network/10-eth0.network Activation Policy: up Required For Online: yes 

That Network File: line answers the "which file won?" question directly.

networkctl cat @eth0 shows the configuration associated with the interface, including drop-ins; networkctl edit @eth0 opens it for editing, creating a file under /etc if none exists. With --drop-in=, it edits a drop-in instead. Note that link-level changes from .link files need a udev event to take effect; reloading alone does not re-apply them.

Boot ordering

systemd-networkd-wait-online waits for all managed links to be configured or failed, and for at least one link to be online. "Online" means the operational state is at least degraded by default. Useful options:

# Wait for one specific interface, not the whole world systemd-networkd-wait-online --interface=eth0 --timeout=60 # Succeed as soon as any managed interface is online systemd-networkd-wait-online --any --timeout=120 

The default timeout is 120 seconds. On a laptop with an unplugged dock, the default "wait for all" behaviour is exactly how you get a two-minute boot delay. RequiredForOnline=no on the irrelevant link is the declarative fix, and it can also take an operational-state range such as degraded:routable.

Failure modes and what they usually mean

Symptom Likely cause
SETUP=unmanaged No matching .network; an earlier file masks yours; Unmanaged=yes; or ID_NET_MANAGED_BY is set
Address vanishes after restart networkd flushed the link; put the address in a .network file, or set KeepConfiguration=
configuring forever DHCP/RA has no answer, DAD is stuck, or there is no carrier
Static config ignored on a dead link ConfigureWithoutCarrier= is false by default; enabling it requires disabling DHCP/RA and DAD to reach configured
Wrong file applied A lower-sorted .network matches first; check Network File:
DNS setting has no effect systemd-resolved is not running, or UseDNS=/UseDomains= is disabled
Default route is on the wrong NIC DHCPv4 metric 1024 vs RA 512:1024:2048 vs another link's metric
networkctl reload did not apply an edit Run networkctl reconfigure <if>; some .netdev properties (for example VLAN ID) need the interface recreated

KeepConfiguration= is newer than most of this article's other settings: it arrived in systemd 257. It accepts static, dynamic-on-stop, and dynamic. The default is no on a normal system, dynamic-on-stop in the initrd, and yes when the root filesystem is on the network. It is a deliberate escape hatch for the case where losing the address is worse than ignoring DHCP lease expiry, not a general fix for "my config does not apply."

Coexistence, not combat

Two network managers on one link produce symptoms that look like driver bugs: addresses appear and disappear, routes flip, and each tool blames the other. Pick an owner per link.

With networkd, the per-link opt-out is in the [Link] section:

# /etc/systemd/network/10-tailscale0.network [Match] Name=tailscale0 [Link] Unmanaged=yes 

That prevents networkd (and later matching files) from bringing the link up or configuring it. It is the right pattern for interfaces owned by Tailscale, libvirt, container runtimes, or anything else that creates and configures its own device.

A rollout checklist

  1. Read the current state: networkctl list, then networkctl status <if>.
  2. Find the file that currently wins: the Network File: line.
  3. Write new files in /etc/systemd/network with a prefix below 70, specific files first.
  4. Keep the old file until the new one is proven; mask generated files by name rather than editing them.
  5. Run networkctl reload, then networkctl status <if> and ip route show.
  6. Test a reboot in a VM or on a console. A network change that has not survived a reboot is not finished.
  7. Remove the old file only after the new path works.

The appeal of ip addr add is that it is immediate. The catch is that it is not a configuration system, and something with a configuration system is going to notice and undo it. Once the address lives in a .network file, reboots stop being an event.

Sources

Commands and defaults in this article were checked against systemd 257.13 on Debian 13. The example addresses use the documentation ranges from RFC 5737 and RFC 3849; substitute your own.