nix wireguard

I had a problem, I wanted to ensure that I could set up wireguard p2p connections between all my server hosts without having to manually configure each connection. It should be simple:

  • Pick a link subnet
  • Generate all wireguard keypairs
  • Make some servers listen and some servers connect
  • Known public ip for listen servers

hosts.toml

I generated a simple toml file containing hosts, like this

[ampere]
ip = "<public ip>"
bird = "<loopback>"
cost = 256
wg_pubkey = "<public key>"
wg_remotes = ["burrito"]

[burrito]
bird = "<loopback>"
wg_pubkey = "<public key>"

The idea is simple, the file contains hosts, which remotes should connect to the hosts that are listen servers.

wireguard.yaml

This file contains secrets in the form wireguard/hostName/{public,private}. Used to ensure hosts have their private keys.

wireguard:
  hosts:
    ampere:
      private: <private>
      public: <public>
    burrito:
      private: <private>
      public: <public>

wireguard.nix

So for the final nix config, this is the result. Functional and performant.

{ lib, pkgs, config, ... }: with lib; with builtins;
let
  cfg = config.ronnvall.wireguard;
  hostName = config.networking.hostName;
  hostsCfg = fromTOML (readFile ./hosts.toml);
  startPort = 51830;
  prefix = "10.200.101.";
  wgServers = (filter (x: (hasAttr "wg_remotes" hostsCfg."${x}")) (attrNames hostsCfg));
  wgRemotes = flatten (map (x: hostsCfg."${x}".wg_remotes) wgServers);
  wgConnections = flatten (map (x: map (y: { server = x; peer = y; remote_ip = hostsCfg."${x}".ip; }) hostsCfg."${x}".wg_remotes) wgServers);
  wgSubnets = map (i: { listenPort = (startPort + i); server_ip = "${prefix}${toString i}"; peer_ip = "${prefix}${toString (i+1)}"; }) (map (x: x * 2) (range 0 (length wgConnections)));
  wgConnectionsFull = map (x: (elemAt wgConnections x) // (elemAt wgSubnets x)) (range 0 ((length wgConnections) - 1));
  isWireguardServer = (elem hostName wgServers);
  isWireguardRemote = (elem hostName wgRemotes);
  wgConnectionsHost = filter (x: elem hostName [ x.server x.peer ]) wgConnectionsFull;
  listenPorts = map (x: (toString x.listenPort)) (filter (x: elem hostName [ x.server ]) wgConnectionsHost);
in
{
  options.ronnvall.wireguard = {
    enable = mkOption { default = (isWireguardServer || isWireguardRemote); };
  };
  config = mkIf cfg.enable {
    sops.secrets."wireguard/hosts/${hostName}/private".sopsFile = ./wireguard.yaml;
    ronnvall.bird.ospfInterfaces."wg-*".cost = 1024;
    ronnvall.nftables.allowedUDPPorts = listenPorts;
    ronnvall.nftables.allowedInterfaces = attrNames config.networking.wireguard.interfaces;
    networking.wireguard.interfaces = listToAttrs
      (map
        (x:
          if hostName == x.server then {
            name = "wg-${x.peer}";
            value = {
              listenPort = x.listenPort;
              allowedIPsAsRoutes = false;
              ips = [ "${x.server_ip}/31" ];
              privateKeyFile = config.sops.secrets."wireguard/hosts/${hostName}/private".path;
              peers = [{
                publicKey = hostsCfg."${x.peer}".wg_pubkey;
                allowedIPs = [ "0.0.0.0/0" ];
                persistentKeepalive = 10;
              }];
            };
          } else {
            name = "wg-${x.server}";
            value = {
              allowedIPsAsRoutes = false;
              ips = [ "${x.peer_ip}/31" ];
              privateKeyFile = config.sops.secrets."wireguard/hosts/${hostName}/private".path;
              peers = [{
                publicKey = hostsCfg."${x.server}".wg_pubkey;
                allowedIPs = [ "0.0.0.0/0" ];
                endpoint = "${x.remote_ip}:${toString x.listenPort}";
                persistentKeepalive = 10;
              }];
            };
          }
        )
        wgConnectionsHost);
  };
}