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:

We'll be replacing it with:

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:

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.

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.
{
  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:

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 resolvectl status 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.