NetworkManager is the standard Linux daemon for managing network connections — WiFi, Ethernet, VPN, mobile broadband. It abstracts over lower-level tools (wpa_supplicant, dhcpcd, iproute2) and exposes a D-Bus API that desktop environments and applications subscribe to.

How it works

NM maintains a registry of connections (stored in /etc/NetworkManager/system-connections/) and devices (physical or virtual interfaces). When a device’s state changes — new IP assigned, link goes up/down, interface appears or disappears — NM fires a NetworkChanged D-Bus signal.

Browsers (Chrome, Firefox) listen for this signal to detect when the underlying network has changed and they should retry or refresh. This is normally useful but becomes noisy when many virtual interfaces are created and destroyed constantly.

The k3s / container interface problem

For background on veth pairs, TAP, TUN, and other virtual network interfaces see Linux Networking.

k3s and container runtimes create and destroy virtual network interfaces (veth*, flannel*, cni*) on every pod lifecycle event. Each creation/deletion triggers a NetworkChanged signal, which causes Chrome/Firefox to show:

“A network change was detected.” — ERR_NETWORK_CHANGED

The fix is to tell NM to leave those interfaces unmanaged:

# /etc/NetworkManager/conf.d/unmanaged.conf
[keyfile]
unmanaged-devices=interface-name:veth*;interface-name:flannel*;interface-name:cni*

In NixOS:

networking.networkmanager.unmanaged = [
  "interface-name:veth*"
  "interface-name:flannel*"
  "interface-name:cni*"
];

MAC address randomization

See Network Privacy Extensions for why WiFi scanning uses a randomized MAC and how both MAC randomization and IPv6 temporary addresses work.

NM randomizes the MAC address of the WiFi interface on every scan by default (to prevent tracking). This can cause the DHCP server to assign a new IP mid-session, triggering another NetworkChanged event. Disable it for stability:

networking.networkmanager.wifi.scanRandMacAddress = false;

IPv6 privacy/temporary addresses

IPv6 privacy extensions (RFC 4941) cause the kernel to generate temporary addresses with randomized interface identifiers that rotate on a schedule (typically every few hours). On a busy network you can end up with 2–3 active IPv6 addresses at once.

Each rotation fires a NetworkChanged D-Bus signal — enough to kill long-lived connections like WebSockets. You can see them with:

nmcli dev show wlp1s0 | grep IP6
# IP6.ADDRESS[1]: 2601:...::afc1/128   ← stable
# IP6.ADDRESS[2]: 2601:...ca3b:.../64  ← temporary, will rotate
# IP6.ADDRESS[3]: 2601:...6c7f:.../64  ← another temporary

Disable temporary addresses in NixOS:

networking.tempAddresses = "disabled";

This keeps a single stable IPv6 address derived from your MAC — no more rotation events.

WiFi power saving

In 802.11 (WiFi), the access point (AP) continuously broadcasts a beacon frame — a short management packet sent at a fixed interval (the beacon interval, typically every ~100 ms) that announces the network and lists which sleeping clients have frames buffered for them.

When power save mode is active, the WiFi adapter tells the AP “I’m going to sleep”, then wakes only at each beacon to check whether the AP has buffered anything for it. If it does, the adapter sends a PS-Poll frame to retrieve the queued frames, then may sleep again.

Linux WiFi drivers default to power save mode. During a doze window, incoming packets queue at the access point and are only delivered on the next wake. This can cause:

  • 100–500 ms packet delays
  • Dropped WebSocket connections (server-side timeout fires during a doze)
  • Intermittent NetworkChanged events as the driver reports link quality changes

Disable it via NM (persists across reboots, unlike iwconfig):

# imperative — lost on reboot
iwconfig wlp1s0 power off
 
# via NM connection profile (persistent)
nmcli connection modify <SSID> 802-11-wireless.powersave 2

In NixOS:

networking.networkmanager.wifi.powersave = false;

Tip

Power saving is the first thing to check when WebSocket connections (GitLab ActionCable, Slack, etc.) drop unpredictably on WiFi even though the connection looks fine.

Tip

Use nmcli device status to see all devices NM knows about and their management state. Unmanaged devices will show unmanaged in the STATE column.

NM DNS management and the dnsmasq gap

dnsmasq is a local DNS forwarder — it caches DNS queries and forwards them to upstream servers. See that note for what it is and why you’d run it.

By default NM writes upstream DNS servers to /etc/dnsmasq-resolv.conf (or /etc/resolv.conf) on every DHCP renewal or network event. If you run a standalone dnsmasq alongside NM, this creates a race:

  1. NM clears the file
  2. dnsmasq notices — logs no servers found in /etc/dnsmasq-resolv.conf, will retry
  3. All DNS resolution fails for ~5 seconds
  4. NM rewrites the file with the new servers

During that window every DNS-dependent request fails silently: browser API calls, WebSocket reconnects, CLI tools. On a webapp like GitLab this surfaces as “An error occurred while fetching pending comments” or “error getting MR counts” — errors that look like connectivity problems but are actually DNS gaps.

Diagnosis:

journalctl -fu dnsmasq | grep "no servers"
# dnsmasq: no servers found in /etc/dnsmasq-resolv.conf, will retry

Fix: tell NM to stop managing DNS entirely, and give dnsmasq static upstream servers:

# /etc/NetworkManager/conf.d/dns.conf
[main]
dns=none
# /etc/dnsmasq.conf
no-resolv
server=1.1.1.1
server=8.8.8.8

In NixOS:

networking.networkmanager.dns = "none";
 
services.dnsmasq.settings = {
  no-resolv = true;
  server = [ "1.1.1.1" "8.8.8.8" ];
};

Tip

This is easy to miss because the gap is only ~5 seconds and the errors it causes look like random UI bugs, not DNS failures. Always check journalctl -fu dnsmasq when debugging intermittent browser errors on a machine running both NM and dnsmasq.

The p2p-dev WiFi Direct interface

Modern WiFi cards expose a p2p-dev-wlp1s0 interface alongside the main wlp1s0. This is the WiFi Direct (P2P) management device — used for features like Miracast and AirPlay. It shares the same radio as the main interface.

When NM manages this device it periodically tries to configure it (set IPv4 forwarding, trigger P2P peer scans). Because the radio is shared, these operations cause the main wlp1s0 connection to briefly deauthenticate — visible in wpa_supplicant as:

wlp1s0: CTRL-EVENT-DISCONNECTED bssid=... reason=3 locally_generated=1

reason=3 means the laptop itself sent the deauth frame (not the AP). The giveaway is that this appears at the exact same millisecond as NM touching the p2p device:

# same timestamp — the smoking gun
NM:             device (p2p-dev-wlp1s0): error setting IPv4 forwarding to '1'
wpa_supplicant: wlp1s0: CTRL-EVENT-DISCONNECTED reason=3 locally_generated=1

This causes periodic spontaneous WiFi drops every ~12-16 minutes — the interval at which NM’s P2P management cycle fires. The disconnects are brief (3-6 seconds) but long enough to kill WebSocket connections.

Fix: mark the p2p device unmanaged so NM never touches it.

networking.networkmanager.unmanaged = [
  "interface-name:p2p-dev-*"
];

PCIe ASPM and silent WiFi connection kills (MediaTek MT7922)

PCIe Active State Power Management (ASPM) puts the PCIe link between the CPU and the WiFi card into low-power states (L0s, L1) between packets. When a new packet arrives the link must wake before the WiFi chip can transmit. On some cards — particularly MediaTek MT7922 / MT7921 — this wakeup latency causes brief firmware-level resets that sever TCP connections without triggering a wpa_supplicant DISCONNECTED event.

This is the hardest class of WiFi bug to diagnose: you see broken connections in Chrome, dropped WebSockets, and GitLab UI errors, but:

  • journalctl shows no CTRL-EVENT-DISCONNECTED
  • NM logs show nothing
  • ping to the gateway succeeds immediately after

The driver for MT7922 is mt7921e (part of the mt76 kernel driver family). Check which driver is in use:

readlink /sys/class/net/wlp1s0/device/driver
# → ../../../../bus/pci/drivers/mt7921e  (MediaTek)
# → ../../../../bus/pci/drivers/iwlwifi  (Intel)

Check current ASPM state:

cat /sys/module/mt7921e/parameters/disable_aspm
# N = ASPM active (bad), 1 = ASPM disabled (stable)

Fix in NixOS:

boot.extraModprobeConfig = ''
  options mt7921e disable_aspm=1
'';

Warning

boot.extraModprobeConfig targets the driver, not the hardware brand. The Intel AX210 uses iwlmvm/iwlwifi; the MediaTek MT7922 uses mt7921e. Putting Intel options on a MediaTek machine (or vice versa) is a silent no-op — the module is never loaded. Always verify with readlink before writing modprobe options.

Requires a reboot to take effect (modprobe params are applied at module load time, not at nixos-rebuild switch).

MicroVM TAP interface crash-loops and NM dispatcher noise

See Linux Networking for an explanation of TAP interfaces and how VMs use them.

When a microVM fails to start (e.g. a required host path is missing), systemd restarts it every few seconds. Each restart cycle:

  1. Creates the TAP interface (vm-<name>)
  2. QEMU exits immediately
  3. Tears down the TAP interface
  4. Repeat

Even with the vm-* pattern in the NM unmanaged list, this creates a constant stream of kernel netlink interface-change events. NM receives these, confirms the interface is unmanaged, but still activates the NM dispatcher service briefly to process the event. Avahi joins and leaves mDNS groups on every cycle. This is harmless at 1 restart/min but at systemd’s default fast-restart rate (~5s) it becomes continuous background noise.

Diagnosis:

systemctl status microvm@<name>
# "activating (auto-restart)" every few seconds → crash loop
 
journalctl -b | grep "microvm@<name>" | grep "Failed" | tail -5
# exit-code every 5s → immediate crash

Common cause: A host path needed by the VM (e.g. a secrets directory bind-mounted via 9p) doesn’t exist. QEMU exits before the guest boots.

Fix in NixOS: create the missing directory declaratively so QEMU can mount it (even if empty):

systemd.tmpfiles.rules = [
  "d /var/secrets/<vm-name> 0700 root root -"
];

The VM will start successfully; services inside that depend on the missing secrets will fail cleanly rather than causing an infinite QEMU restart loop.

Useful commands

nmcli device status                        # list all interfaces and their state
nmcli dev show wlp1s0                      # detailed info: IPs, DNS, power save
nmcli connection show                      # list saved connection profiles
nmcli radio wifi                           # show wifi radio state
journalctl -u NetworkManager               # NM logs, useful for debugging reconnects
cat /etc/NetworkManager/NetworkManager.conf  # active NM configuration

See also