codefionn

NixOS router with Intel J4125 and i225

I bought one of those cheap firewall-computers with an Intel Celeron J4125 and i225 2.5G LAN chipset. Then used NixOS to operate it.

Front of the black fanless router with four Ethernet ports, ETH0 to ETH3, all plugged in, and a 12 V power jack
J4125 Router with i225 networking
J4125 router power consumption is about 9 W/h
J4125 Router power consumption
NixOS logo
NixOS

enp1s0 will be the port that is connected to a switch/home-router.

enp2s0, eno1 and enp4s0 are the ports for other devices, which connect to this router.

This configuration uses kea as a DHCP-Server and the DNS-Server unbound. The router itself does encrypted DNS requests with DoT.

The configuration supports both IPv4 and IPv6.

I have a basic settings file /etc/nixos/settings.nix:

rec {
  allowedSSHKeys = [
    "ssh-rsa XXXXXXXXXXX user@nixos"
    "ssh-rsa YYYYYYYYYYY user@nixos"
  ];

  # Rename them so they're easier to handle
  eth1 = "enp1s0";
  eth2 = "enp2s0";
  eth3 = "eno1";
  eth4 = "enp4s0";

  wanInterface = "${eth1}";
  lanInterfaces = [ "${eth2}" "${eth3}" "${eth4}" ];

  # What IPs to use
  baseInternalIp = "192.168.9";
  internalIp = "${baseInternalIp}.1";
  baseInternalIp6 = "fd03:1234:fde0:0000";
  internalIp6 = "${baseInternalIp6}::1";
  internalDomain = "lan";

  # Devices with reserved IPs and domains
  mac-nixos = "70:85:c2:93:c0:cb";
  mac-raspberrypi = "e4:5f:01:98:9d:7b";
  ip-nixos = "${baseInternalIp}.10";
  ip6-nixos = "${baseInternalIp6}::0010";
  ip-raspberrypi = "${baseInternalIp}.11";
  ip6-raspberrypi = "${baseInternalIp6}::0011";

  hostName = "fionn-router";

  # All reserved domains an their IPs
  domain-and-ips = builtins.map (x: { domain = "${x.domain}.${internalDomain}"; ip = "${x.ip}"; }) [
    { domain = "earth"; ip = "${ip-nixos}"; }
    { domain = "nixos"; ip = "${ip-nixos}"; }
    { domain = "raspberrypi"; ip = "${ip-raspberrypi}"; }
    { domain = "fionn-router"; ip = "${internalIp}"; }
  ];
}

This defines all basics we need for the router (so we don't always have to go into /etc/nixos/configuration.nix to tweak something).

Then there's the /etc/nixos/configuration.nix:

# Edit this configuration file to define what should be installed on
# your system.  Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).

{ config, pkgs, ... }:

let
  settings = import ./settings.nix;
in
{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];

  nix.settings.auto-optimise-store = true;

  # Bootloader.
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
  boot.loader.efi.efiSysMountPoint = "/boot/efi";

  # Reduce writes onto disk
  services.journald.extraConfig =
  ''
    Storage=volatile
  '';

  networking.hostName = "${settings.hostName}"; # Define your hostname.

  # Allow NAT stuff ...
  boot.kernel.sysctl = {
    "net.ipv4.conf.all.forwarding" = true;
    "net.ipv6.conf.all.forwarding" = true;

    # source: https://github.com/mdlayher/homelab/blob/master/nixos/routnerr-2/configuration.nix#L52
    # By default, not automatically configure any IPv6 addresses.
    "net.ipv6.conf.all.accept_ra" = 0;
    "net.ipv6.conf.all.autoconf" = 0;
    "net.ipv6.conf.all.use_tempaddr" = 0;

    # On WAN, allow IPv6 autoconfiguration and tempory address use.
    "net.ipv6.conf.${settings.wanInterface}.accept_ra" = 2;
    "net.ipv6.conf.${settings.wanInterface}.autoconf" = 1;
  };

  # Configure network proxy if necessary
  # networking.proxy.default = "http://user:password@proxy:port/";
  # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";

  # Set your time zone.
  time.timeZone = "Europe/Berlin";

  # Select internationalisation properties.
  i18n.defaultLocale = "en_US.utf8";

  # Allow unfree packages
  nixpkgs.config.allowUnfree = true;

  environment.variables = { EDITOR = "vim"; };

  # List packages installed in system profile. To search, run:
  # $ nix search wget // Comment: Doesn't work (nix search)
  environment.systemPackages = with pkgs; [
    wget
    traceroute dig iftop ethtool btop
    pciutils usbutils
    fd
    wakelan
    htop
    tmux
  ] ++ (with pkgs.vimPlugins; [ vim-nix ]);

  # Maybe use nano instead?
  programs.vim = {
    defaultEditor = true;
    package = ((pkgs.vim_configurable.override {}).customize {
      name = "vim";
      vimrcConfig.packages.myplugins = with pkgs.vimPlugins; {
        start = [ vim-nix ];
        opt = [];
      };
      vimrcConfig.customRC = ''
        set nocompatible
        set expandtab
        set ts=2
        set sw=2
        syntax on
      '';
    });
  };

  users.mutableUsers = false;
  users.users.root = {
    openssh.authorizedKeys.keys = settings.allowedSSHKeys;
    hashedPassword = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  };

  # Define a user account
  users.users.user = {
    openssh.authorizedKeys.keys = settings.allowedSSHKeys;
    isNormalUser = true;
    description = "Fionn Langhans";
    extraGroups = [ "wheel" ];
    hashedPassword = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
    packages = with pkgs; [];
  };

  # List services that you want to enable:

  # Enable the OpenSSH daemon.
  services.openssh = {
    enable = true;
    passwordAuthentication = false;
  };

  services.kea.dhcp4 = {
    enable = true;
    settings = {
      interfaces-config = {
        interfaces = [ "br0" ];
      };
      lease-database = {
        name = "/var/lib/kea/dhcp4.leases";
        persist = true;
        type = "memfile";
      };
      option-data = [
        {
          name = "domain-name-servers";
          data = settings.internalIp;
          always-send = true;
        }
        {
          name = "routers";
          data = settings.internalIp;
        }
        {
          name = "domain-name";
          data = "${config.networking.hostName}.${settings.internalDomain}";
        }
      ];

      rebind-timer = 2000;
      renew-timer = 1000;
      valid-lifetime = 4000;

      subnet4 = [
        {
          pools = [
            {
              pool = "${settings.baseInternalIp}.100 - ${settings.baseInternalIp}.230";
            }
          ];
          subnet = "${settings.baseInternalIp}.0/24";
          reservations = [
            {
              hw-address = settings.mac-nixos;
              ip-address = settings.ip-nixos;
            }
            {
              hw-address = settings.mac-raspberrypi;
              ip-address = settings.ip-raspberrypi;
            }
          ];
        }
      ];
    };
  };

  services.kea.dhcp6 = {
    enable = true;
    settings = {
      interfaces-config = {
        interfaces = [ "br0" ];
      };
      lease-database = {
        name = "/var/lib/kea/dhcp6.leases";
        persist = true;
        type = "memfile";
      };
      option-data = [
        {
          name = "dns-servers";
          data = settings.internalIp6;
        }
        {
          name = "unicast";
          data = settings.internalIp6;
        }
      ];

      preferred-lifetime = 3000;
      rebind-timer = 2000;
      renew-timer = 1000;
      valid-lifetime = 4000;

      subnet6 = [
        {
          pools = [
            {
              pool = "${settings.baseInternalIp6}::1000-${settings.baseInternalIp6}::EFFF";
            }
          ];
          subnet = "${settings.baseInternalIp6}::0/64";
          reservations = [
            {
              hw-address = settings.mac-nixos;
              ip-addresses = [ settings.ip6-nixos ];
            }
            {
              hw-address = settings.mac-raspberrypi;
              ip-addresses = [ settings.ip6-raspberrypi ];
            }
          ];
        }
      ];
    };
  };

  services.radvd = {
    enable = true;
    config = ''
      interface br0 {
        AdvSendAdvert on;
        prefix ${settings.baseInternalIp6}::/64 {
          AdvOnLink on;
          AdvAutonomous on;
          AdvRouterAddr on;
        };
        route ${settings.internalIp6}/128 {
        };
        RDNSS ${settings.internalIp6} {
        };
      };
    '';
  };

  # This is not really secure, but some games need it.
  services.miniupnpd = {
      enable = true;
      externalInterface = "${settings.wanInterface}";
      internalIPs = [ "br0" ];
  };

  # DNS-Server
  services.unbound = {
    enable = true;
    settings = {
      server = {
        interface = [ "127.0.0.1" "${settings.internalIp}" "${settings.internalIp6}" ];
        tls-system-cert = true;
        access-control = [
          "0.0.0.0/0 refuse"
          "127.0.0.0/8 allow"
          "${settings.baseInternalIp}.0/24 allow"
          "${settings.baseInternalIp6}::0/64 allow"
        ];

        prefer-ip6 = true;

        private-domain = [ "local" "${settings.internalDomain}" ];
        private-address = [
          "${settings.baseInternalIp}.0/24"
          "${settings.baseInternalIp6}::0/64"
        ];
        unblock-lan-zones = true;
        insecure-lan-zones = true;
        
        local-zone = builtins.map (x: "\"${x.domain}.\" static") settings.domain-and-ips;
        local-data = builtins.concatLists (builtins.map
          (domain-and-ip: [
            "\"${domain-and-ip.domain}. 3600 IN A ${domain-and-ip.ip}\""
          ]) settings.domain-and-ips);
        local-data-ptr = builtins.map (x: "\"${x.ip} ${x.domain}\"") settings.domain-and-ips;
      };
      forward-zone = [
        {
          name = ".";
          forward-tls-upstream = true;
          forward-addr = [
            "2620:fe::fe@853#quad9.net"
            "2606:4700:4700::1111@853#cloudflare-dns.com"
            "2001:4860:4860::8888@853#dns.google"
            "9.9.9.9@853#quad9.net"
            "1.1.1.1@853#cloudflare-dns.com"
            "8.8.8.8@853:dns.google"
          ];
        }
        {
          name = "onion.";
        }
      ];
      remote-control.control-enable = false;
    };
  };

  # Tailscale, because it's convenient
  services.tailscale = {
    enable = true;
  };
  
  services.avahi = {
    enable = true;
    nssmdns = true;
    interfaces = [ "br0" "tailscale0" ];
  };

  services.syncthing = {
    enable = true;
    user = "user";
    group = "users";
    guiAddress = "${settings.internalIp}:8384";
    dataDir = "/home/user";
    devices = {
      codefionn = {
        addresses = [];
        id = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
      };
      raspberrypi = {
        addresses = [];
        id = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
      };
    };
    folders.default = {
      id = "default";
      label = "Sync";
      path = "/home/user/Sync";
      devices = [ "codefionn" "raspberrypi" ];
    };
  };

  # If you reboot the router often, this his handy
  systemd.services.startup-tune = {
    enable = true;
    path = with pkgs; [ beep kmod ];
    preStart = "modprobe pcspkr";
    script = "beep -f 1000 -l 50 -r 3 -d 1000 -n -d 100 -n -f 500 -d 1000 -l 50 -r 1";
    wants = [ "network-online.target" ];
    after = [ "network-online.target" ];
    wantedBy = [ "multi-user.target" ];
  };

  # Open ports in the firewall.
  # networking.firewall.allowedTCPPorts = [ ... ];
  # networking.firewall.allowedUDPPorts = [ ... ];
  networking.firewall.checkReversePath = "loose";
  networking.firewall.interfaces.br0 = {
    allowedTCPPorts = [ 53 22000 8384 ];
    allowedTCPPortRanges = [
      {
        from = 1714;
        to = 1764;
      }
    ];
    allowedUDPPorts = [ 53 22000 21027 ];
    allowedUDPPortRanges = [
      {
        from = 1714;
        to = 1764;
      }
    ];
  };

  #----------
  # Enables DHCP on each ethernet and wireless interface. In case of scripted networking
  # (the default) this is the recommended approach. When using systemd-networkd it's
  # still possible to use this option, but it's recommended to use it in conjunction
  # with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
  networking = {
    useDHCP = false;
    hosts = {
      "127.0.0.1" = [ "localhost" "${config.networking.hostName}" "${config.networking.hostName}.local" ];
    };
    interfaces = {
      "${settings.wanInterface}" = {
        useDHCP = true;
        # ... Or maybe define it statically
      };
      br0 = {
        ipv4.addresses = [
          { address = "${settings.internalIp}"; prefixLength = 24; }
        ];
        ipv6.addresses = [
          { address = "${settings.internalIp6}"; prefixLength = 64; }
        ];
      };
    };
    
    bridges.br0 = {
      interfaces = settings.lanInterfaces;
    };

    nat = {
      enable = true;
      enableIPv6 = true;
      externalInterface = "${settings.wanInterface}";
      internalInterfaces = [ "br0" ];
    };

    networkmanager.enable = false;
  };

  # Or disable the firewall altogether.
  # networking.firewall.enable = false;

  # This value determines the NixOS release from which the default
  # settings for stateful data, like file locations and database versions
  # on your system were taken. It‘s perfectly fine and recommended to leave
  # this value at the release version of the first install of this system.
  # Before changing this value read the documentation for this option
  # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
  system.stateVersion = "22.05"; # Did you read the comment?
}

Sadly, my Intel AX210 wireless M.2 chipset doesn't work well with both Linux and this router.