Setting DNS rewrote the connection profile and stopped there, so the
running device kept resolving through whatever it had before. Nothing
noticed while something else always activated the device afterwards: the
profile was written first and NetworkManager picked the resolvers up
when it got round to the interface on its own.
Turning a DHCP client off now applies the profile when the device is not
activated, which moved that activation ahead of the DNS write. A caller
that sets an address, disables DHCP, and then sets resolvers -- the
order a static configuration is naturally applied in -- ends with the
device activated on a profile that had no DNS in it yet, and the write
that follows never reaches the system. The profile carries the
resolvers, resolv.conf carries none, and ipv4.ignore-auto-dns has by
then ruled out the lease's resolvers as well: an interface holding an
address, a route, and no way to resolve a name.
Reapply the device after the change. Unlike turning a client off, this
risks nothing for a caller connected over the interface -- reapply
changes the device in place and does not tear the link down -- so it is
not conditioned on the device being unactivated, which is exactly the
case that needs it.
Turning a client off only rewrote the profile, deliberately, so a caller
connected over an existing lease is not cut off by a call meant to change
the next boot. A device that never got a lease has no such caller and is
in a worse spot: NetworkManager goes on retrying an activation it cannot
complete, and each failure deconfigures the interface, taking with it any
address configured underneath it. The retry then waits out the profile's
autoconnect timer, which at the default is five minutes -- long enough for
whatever was reached over that address to have given up.
That is what a static address applied while NetworkManager was still
hunting a lease ran into: the address, gateway, and static profile all
landed, and thirty-five seconds later the doomed DHCP activation failed
and flushed the running configuration it knew nothing about.
Reapply the profile after a change that turns a client off, when the
device is not activated. An activated device is left exactly as before,
so the promise that disabling a client does not tear down a lease already
held is unchanged.
Two ways a NetworkManager host could end up with nothing written to disk,
both hit at once by an EL10 image whose only backend is NetworkManager.
Backend detection waits for the daemon to finish starting before it will
register the backend, and gave up on it when that wait ran out. The
Startup property stays true for as long as a device is still working
through its initial activation, so a VM whose DHCP request nothing
answers holds it true indefinitely -- the state a host is in precisely
when it is about to be given a static address. Detection then reported a
NetworkManager host as having no network configuration backend at all,
which on an image with no network-scripts, networkd, or netplan is fatal.
The wait still runs, since a daemon that has settled will not rewrite the
change as it finishes starting, but a daemon that owns its bus name is
configured either way.
The write paths then matched a profile to an interface only by
connection.interface-name. NetworkManager's own default wired connection
-- the one it creates for a device with no profile of its own, which is
exactly the case on an image whose baked profile does not match the VM's
hardware address -- names no interface at all, so every write skipped it
and silently persisted nothing. When no profile names the interface, the
profile the device is actually running and any profile pinned to its
hardware address are matched instead. Profiles bound by name are matched
first and alone, so a host configured the ordinary way is unaffected and
pays neither lookup.
The systemd service backend imported github.com/coreos/go-systemd while
calling v22-only API, and compiled only because of a local replace onto
the v22 module. Replace directives are ignored outside the main module,
so every consumer had to mirror that replace, and a consumer that also
requires go-systemd/v22 directly could not build at all: Go rejects one
module serving two import paths.
Import github.com/coreos/go-systemd/v22/dbus directly and drop the
replace.
Bring an administratively down interface up in AddAddress before writing
its addresses and routes. The kernel installs an address's connected
route only while its link is up, so adding a gateway to a down interface
was rejected as unreachable. A link raised this way is part of the
pre-change state AddAddress restores, so it is returned to down on any
path that does not complete.
Also fix the rollback masking its own error: deleting the address it
added takes the default route with it, since that route resolved its
nexthop through the address's connected subnet. The following RouteDel
then answered ESRCH and was reported in place of "aborted operation due
to loss of internet".