The default networking stack on NixOS uses NetworkManager. While it's a great choice for many users, I prefer a lighter setup built around systemd-networkd, systemd-resolved, and iwd. It integrates well with the rest of the systemd stack, has fewer moving pieces, and exposes powerful networking features without an additional network management daemon.
This article focuses on laptops that connect to wifi. If you're configuring a server, you probably don't need iwd at all; systemd-networkd and systemd-resolved are usually sufficient.
The default stack
If not configured otherwise, NixOS uses the following stack:
- NetworkManager for wifi
- dhcpcd for DHCP
- resolvconf for DNS
- a "legacy script based system" for network configuration[1]
We'll be replacing it with:
- iwd for wifi
- systemd-networkd for DHCP and network configuration
- systemd-resolved for DNS
This has a few benefits. Firstly, switching to resolved allows you to enable features like encrypted DNS over TLS (DoT) and DNSSEC validation, which reduces the potential for DNS spoofing and man-in-the-middle attacks[2]. Additionally, the default stack uses 3 extra packages (NM, dhcpcd, resolvconf) and some bash scripts, while this setup only uses one extra package (iwd). Also, I'm kind of a systemd nerd, and this is a way to dig into systemd a bit more.
So the first thing we can do is disable all of the default stuff:
{
networking = {
networkmanager .enable = false ;
dhcpcd .enable = false ;
resolvconf .enable = false ;
networking .useDHCP = false ;
};
}
And let's get to configuring!
iwd
First up is iwd. The important parts of the configuration are delegating network configuration to networkd, delegating name resolution (DNS) to resolved, and configuring MAC randomization for some extra privacy.
{
networking .wireless .iwd = {
enable = true ;
settings = {
General = {
EnableNetworkConfiguration = false ; # delegated to networkd
AddressRandomization = "once" ;
AddressRandomizationRange = "full" ;
};
Network .NameResolvingService = "systemd" ; # use systemd-resolved
};
};
}
At this point, we don't have any network configuration yet, so internet access won't work. However, you can verify that iwd is functioning correctly:
: Scans for new wifi networksiwctl station wlan0 scan : lists the networks that it foundiwctl station wlan0 get-networks : connects to the network, prompting for a PSK if necessaryiwctl station wlan0 connect "<SSID>"
systemd-networkd
Next up is systemd-networkd. Networkd has lots of advanced options for things like static IP config, Wireguard tunnels, VLANs, domain-based DNS routing, and more. However, here we'll just do a basic configuration to enable DHCP and deny DHCP-advertised DNS.
{
systemd .network = {
enable = true ;
networks ."11-default" = {
name = "wl*" ;
networkConfig .DHCP = "yes" ;
# never accept dhcp dns
dhcpV4Config .UseDNS = "no" ;
dhcpV6Config .UseDNS = "no" ;
};
};
networking .useNetworkd = true ;
}
The name = "wl*"
refers to the network interface name that this configuration applies to.
According to the systemd docs, wireless LAN devices should start with the two-character prefix "wl"[3].
Here we use a glob (*) to match all wireless interfaces.
You can verify that your configuration worked with a few commands:
: make sure that there's an IP address associated with your wireless interface. That means DHCP is working.ip addr : make sure there's no significant errors in the logs.networkctl status
systemd-resolved
Now all we have left is DNS configuration, then we're done. This is probably the part where you can nerd out the hardest, but I try to keep it pretty minimal. The important parts are selecting upstream DNS servers that support secure DNS protocols (like DNS over TLS and DNSSEC) and that respect your privacy. For these reasons, I use quad9, but do your own research and find the best upstream DNS server for you! Some other options include Cloudflare DNS, Google's DNS, or AdGuard Public DNS.
{
networking .nameservers = [
"9.9.9.9#dns.quad9.net"
"149.112.112.112#dns.quad9.net"
];
services .resolved = {
enable = true ;
settings .Resolve = {
DNSOverTLS = true ;
DNSSEC = "allow-downgrade" ;
LLMNR = false ;
MulticastDNS = false ;
};
};
}
You can verify your DNS configuration by using the
command or by just opening a website.
P.S. Captive portals
Captive portals are those wifi login pages that pop up when you connect to some public wifi networks. Some captive portals don't behave correctly when you're using an encrypted third-party DNS server instead of the network's advertised resolver. To get around this, I use Captive Browser (nixpkgs), which launches an instance of Chromium through a SOCKS5 proxy, avoiding the system DNS configuration altogether. This allows you to access a captive portal for long enough to log in and gain internet access. Pretty neat!
P.P.S. I miss nm-applet
Probably the biggest downside to this setup is the lack of desktop integration. GNOME and Plasma only support NetworkManager in their network configuration menu, so you're kinda out of luck. For tiling window manager people, I can recommend iwmenu, which is a launcher (dmenu, rofi, fuzzel)-based menu for connecting to wifi networks via iwd.
why disable DHCP-advertised DNS?
By default, most networks advertise their own DNS resolver via DHCP. Since we'll configure our own encrypted upstream DNS servers in the next section, we tell networkd to ignore the ones provided by the network.