monotux.tech


NixOS for system configuration 🩷

NixOS, nix

I know that developers (and operators) likes nix due to it’s capabilities as a package manager, and that is also where I started my journey. It is a lovely package manager!

But coming back to NixOS after a hiatus, I’ve managed to learn some nix and finally really started to appreciate how amazing it is for setting up machines as well!

The secret for me was to learn a bit of the dsl nix uses. It wasn’t really hard, but it unlocked endless possibilities for making my system configuration nearly uncomprehensible, which current Oscar loves and future Oscar hates. :-)

In my previous blog entry covering CoreRAD, I described my configuration like this:

{ config, lib, ... }:
{
  services.corerad.enable = false;

  services.corerad.settings = {
    interfaces = [
      {
        name = "eth1";
        advertise = true;
        prefix = [{ prefix = "2001:db8::/64"; }];
      }
      {
        name = "eth2";
        advertise = true;
        prefix = [{ prefix = "2001:db8:1::/64"; }];
      }
    ];

    debug = {
      address = "localhost:9430";
      prometheus = true;
    };
  };
}

That works well for a few interfaces, but what if you have like 10, and you don’t want to type these definitions again and again? Well, nix to the rescue!

{ config, lib, ... }:
let
  gen = builtins.fromJSON (builtins.readFile ../interfaces.json);
  int_names = lib.catAttrs "name" gen.interfaces;

  # Remove address to create prefix...
  guessPrefix = (addr: "${(lib.strings.removeSuffix "1/64" addr)}/64");
  prefixAttr = (prefix: { prefix = prefix; });

  intDefs = ({ name, ipv6, advertise ? true, ... }: {
    name = name;
    advertise = advertise;
    prefix = builtins.map (addr: prefixAttr (guessPrefix addr)) (builtins.attrValues ipv6);
  });
in
{
  services.corerad.enable = false;

  services.corerad.settings = {
    interfaces = builtins.map intDefs int_names;
  };
}

The above reads in a json file which contains my interface definitions, does some transformations of the data, and then just generates the interface configurations necessary for my home network.

The interfaces.json1 file looks something like this:

{
    "interfaces": [
        {
            "name": "example",
            "vid": 100,
            "ipv4": "192.168.0.1/24",
            "ipv6": {
                "ula": "fd0b:7c3c:2594::1/64",
                "gua": "2001:db8:3333:4444:5555:6666:7777:8888"
            },
            "subnet": "192.168.0.0/24",
            "dhcp-range": "192.168.0.100-192.168.0.200",
            "domain": "home.arpa"
        }
    ]
}

And what if I want to use the same source to generate my dnsmasq (for IPv4) configuration? I can just use the same data:

{ config, lib, ... }:
let
  secretsFile = "/var/lib/dnsmasq/secrets.conf";
  gen = builtins.fromJSON (builtins.readFile ../interfaces.json);
  interfaces = lib.catAttrs "name" gen.interfaces;
  dhcp-ranges = lib.catAttrs "dhcp-range" gen.interfaces;
  domains = lib.catAttrs "domain" gen.interfaces;
  domain_maps = builtins.map ({domain, subnet, ...}: "domain=${domain},${subnet}") gen.interfaces;
in
{
  age.secrets.dnsmasq = {
    file = ../secrets/dnsmasq.age;
    path = secretsFile;
    mode = "0600";
    owner = "root";
    group = "root";
  };

  services.dnsmasq = {
    enable = false;
    resolveLocalQueries = false;
    settings = {
      dhcp-authoritative = true;
      bind-interfaces = true;
      domain-needed = true;
      expand-hosts = true;
      bogus-priv = true;
      no-resolv = true;
      no-hosts = true;
      log-dhcp = true;
      no-poll = true;
      port = 4053;
      interface = interfaces;
      dhcp-range = dhcp-ranges;
      local = domains;
      domain = domain_maps;
      conf-file = secretsFile;
    };
  };
}

This also contains a system specific, secret file containing all my machines MAC addresses (as they are top secret!), plus generates all static DHCP leases, subnet definitions, and so forth. It’s pretty neat.

I’m far from an expert, but I’ve rediscovered just how nice NixOS really is. The nix DSL is a bit quirky, and I still love writing & using Ansible, but nix will probably take up a lot of my time going forward. :-)


  1. Why json and not straight up nix? I plan to use my NetBox as the single source of truth and build this json file automagically! ↩︎