diff --git a/deploy_nixos b/deploy_nixos index b716993..a35fec3 100755 --- a/deploy_nixos +++ b/deploy_nixos @@ -7,9 +7,9 @@ copy cluster/$CLUSTER/cluster.nix /etc/nixos/cluster.nix copy cluster/$CLUSTER/node/$NIXHOST.nix /etc/nixos/node.nix copy cluster/$CLUSTER/node/$NIXHOST.site.nix /etc/nixos/site.nix -cmd mkdir -p /var/lib/wgautomesh -write_pass deuxfleurs/cluster/$CLUSTER/wgautomesh_gossip_secret /var/lib/wgautomesh/gossip_secret copy nix/wgautomesh.nix /etc/nixos/wgautomesh.nix +cmd mkdir -p /var/lib/deuxfleurs +write_pass deuxfleurs/cluster/$CLUSTER/wgautomesh_gossip_secret /var/lib/deuxfleurs/wgautomesh_gossip_secret if [ "$CLUSTER" = "staging" ]; then copy nix/nomad-driver-nix2.nix /etc/nixos/nomad-driver-nix2.nix diff --git a/nix/deuxfleurs.nix b/nix/deuxfleurs.nix index 7632486..b174b97 100644 --- a/nix/deuxfleurs.nix +++ b/nix/deuxfleurs.nix @@ -207,18 +207,19 @@ in }; services.wgautomesh = { enable = true; - interface = "wg0"; - gossipPort = 1666; - gossipSecretFile = "/var/lib/wgautomesh/gossip_secret"; - persistFile = "/var/lib/wgautomesh/state"; - upnpForwardPublicPort = - if clusterNodeCfg.endpoint != null then - strings.toInt (lists.last (split ":" clusterNodeCfg.endpoint)) - else null; - peers = attrValues (mapAttrs (hostname: { publicKey, endpoint, address, ... }: { - inherit address endpoint; - pubkey = publicKey; - }) cfg.clusterNodes); + gossipSecretFile = "/var/lib/deuxfleurs/wgautomesh_gossip_secret"; + settings = { + interface = "wg0"; + gossip_port = 1666; + upnp_forward_external_port = + if clusterNodeCfg.endpoint != null then + strings.toInt (lists.last (split ":" clusterNodeCfg.endpoint)) + else null; + peers = attrValues (mapAttrs (hostname: { publicKey, endpoint, address, ... }: { + inherit address endpoint; + pubkey = publicKey; + }) cfg.clusterNodes); + }; }; # Old code for wg-quick, we can use this as a fallback if we fail to make wgautomesh work # systemd.services."wg-quick-wg0".after = [ "unbound.service" ]; diff --git a/nix/wgautomesh.nix b/nix/wgautomesh.nix index 55aa73f..b7125f2 100644 --- a/nix/wgautomesh.nix +++ b/nix/wgautomesh.nix @@ -7,123 +7,159 @@ in { lib, config, pkgs, ... }: with lib; -let +let cfg = config.services.wgautomesh; + settingsFormat = pkgs.formats.toml { }; + configFile = + # Have to remove nulls manually as TOML generator will not just skip key + # if value is null + settingsFormat.generate "wgautomesh-config.toml" + (filterAttrs (k: v: v != null) + (mapAttrs + (k: v: + if k == "peers" + then map (e: filterAttrs (k: v: v != null) e) v + else v) + cfg.settings)); + runtimeConfigFile = + if cfg.enableGossipEncryption + then "/run/wgautomesh/wgautomesh.toml" + else configFile; in - with builtins; - { - options.services.wgautomesh = { - enable = mkEnableOption "wgautomesh"; - logLevel = mkOption { - type = types.enum [ "trace" "debug" "info" "warn" "error" ]; - default = "info"; - description = "wgautomesh log level (trace/debug/info/warn/error)"; - }; - interface = mkOption { - type = types.str; - description = "Wireguard interface to manage"; - }; - gossipPort = mkOption { - type = types.port; - description = "wgautomesh gossip port"; - }; - gossipSecretFile = mkOption { - type = types.nullOr types.str; - description = "File containing the gossip secret encryption key"; - }; - persistFile = mkOption { - type = types.nullOr types.str; - description = "Path where to persist known peer addresses"; - }; - lanDiscovery = mkOption { - type = types.bool; - default = true; - description = "Enable discovery using LAN broadcast"; - }; - openFirewall = mkOption { - type = types.bool; - default = true; - description = "Automatically open gossip port in firewall"; - }; - upnpForwardPublicPort = mkOption { - type = types.nullOr types.port; - default = null; - description = "Public port number to try to redirect to this machine using UPnP IGD"; - }; - peers = mkOption { - type = types.listOf (types.submodule { - options = { - pubkey = mkOption { - type = types.str; - description = "Wireguard public key"; - }; - address = mkOption { - type = types.str; - description = "Wireguard peer address"; - }; - endpoint = mkOption { - type = types.nullOr types.str; - description = "bootstrap endpoint"; - }; +{ + options.services.wgautomesh = { + enable = mkEnableOption (mdDoc "the wgautomesh daemon"); + logLevel = mkOption { + type = types.enum [ "trace" "debug" "info" "warn" "error" ]; + default = "info"; + description = mdDoc "wgautomesh log level."; + }; + enableGossipEncryption = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable encryption of gossip traffic."; + }; + gossipSecretFile = mkOption { + type = types.path; + description = mdDoc '' + File containing the shared secret key to use for gossip encryption. + Required if `enableGossipEncryption` is set. + ''; + }; + enablePersistence = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable persistence of Wireguard peer info between restarts."; + }; + openFirewall = mkOption { + type = types.bool; + default = true; + description = mdDoc "Automatically open gossip port in firewall (recommended)."; + }; + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + interface = mkOption { + type = types.str; + description = mdDoc '' + Wireguard interface to manage (it is NOT created by wgautomesh, you + should use another NixOS option to create it such as + `networking.wireguard.interfaces.wg0 = {...};`). + ''; + example = "wg0"; }; - }); - description = "wgautomesh peer list"; + gossip_port = mkOption { + type = types.port; + description = mdDoc '' + wgautomesh gossip port, this MUST be the same number on all nodes in + the wgautomesh network. + ''; + default = 1666; + }; + lan_discovery = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable discovery of peers on the same LAN using UDP broadcast."; + }; + upnp_forward_external_port = mkOption { + type = types.nullOr types.port; + default = null; + description = mdDoc '' + Public port number to try to redirect to this machine's Wireguard + daemon using UPnP IGD. + ''; + }; + peers = mkOption { + type = types.listOf (types.submodule { + options = { + pubkey = mkOption { + type = types.str; + description = mdDoc "Wireguard public key of this peer."; + }; + address = mkOption { + type = types.str; + description = mdDoc '' + Wireguard address of this peer (a single IP address, multliple + addresses or address ranges are not supported). + ''; + example = "10.0.0.42"; + }; + endpoint = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + Bootstrap endpoint for connecting to this Wireguard peer if no + other address is known or none are working. + ''; + default = null; + example = "wgnode.mydomain.example:51820"; + }; + }; + }); + default = [ ]; + description = mdDoc "wgautomesh peer list."; + }; + }; }; + default = { }; + description = mdDoc "Configuration for wgautomesh."; + }; + }; + + config = mkIf cfg.enable { + services.wgautomesh.settings = { + gossip_secret_file = mkIf cfg.enableGossipEncryption "$CREDENTIALS_DIRECTORY/gossip_secret"; + persist_file = mkIf cfg.enablePersistence "/var/lib/wgautomesh/state"; }; - config = mkIf cfg.enable ( - let - peerDefs = map (peer: - let endpointDef = if peer.endpoint == null then "" - else ''endpoint = "${peer.endpoint}"''; - in - '' - [[peers]] - pubkey = "${peer.pubkey}" - address = "${peer.address}" - ${endpointDef} - '') cfg.peers; - extraDefs = (if cfg.lanDiscovery then ["lan_discovery = true"] else []) - ++ (if (cfg.gossipSecretFile != null) - then [''gossip_secret_file = "${cfg.gossipSecretFile}"''] else []) - ++ (if (cfg.persistFile != null) - then [''persist_file = "${cfg.persistFile}"''] else []) - ++ (if (cfg.upnpForwardPublicPort != null) - then [''upnp_forward_external_port = ${toString cfg.upnpForwardPublicPort}''] else []); - configfile = pkgs.writeText "wgautomesh.toml" '' - interface = "${cfg.interface}" - gossip_port = ${toString cfg.gossipPort} - ${concatStringsSep "\n" extraDefs} + systemd.services.wgautomesh = { + path = [ pkgs.wireguard-tools ]; + environment = { RUST_LOG = "wgautomesh=${cfg.logLevel}"; }; + description = "wgautomesh"; + serviceConfig = { + Type = "simple"; - ${concatStringsSep "\n" peerDefs} - ''; - in { - systemd.services.wgautomesh = { - enable = true; - path = [ pkgs.wireguard-tools ]; - environment = { - RUST_LOG = "wgautomesh=${cfg.logLevel}"; - }; - description = "wgautomesh"; - serviceConfig = { - Type = "simple"; + ExecStart = "${wgautomesh}/bin/wgautomesh ${runtimeConfigFile}"; + Restart = "always"; + RestartSec = "30"; + LoadCredential = mkIf cfg.enableGossipEncryption [ "gossip_secret:${cfg.gossipSecretFile}" ]; - ExecStart = "${wgautomesh}/bin/wgautomesh ${configfile}"; - Restart = "always"; - RestartSec = "30"; + ExecStartPre = mkIf cfg.enableGossipEncryption [ + ''${pkgs.envsubst}/bin/envsubst \ + -i ${configFile} \ + -o ${runtimeConfigFile}'' + ]; - ExecStartPre = [ "+${pkgs.coreutils}/bin/chown wgautomesh /var/lib/wgautomesh/gossip_secret" ]; - - DynamicUser = true; - User = "wgautomesh"; - StateDirectory = "wgautomesh"; - StateDirectoryMode = "0700"; - AmbientCapabilities = "CAP_NET_ADMIN"; - CapabilityBoundingSet = "CAP_NET_ADMIN"; - }; - wantedBy = [ "multi-user.target" ]; + DynamicUser = true; + StateDirectory = "wgautomesh"; + StateDirectoryMode = "0700"; + RuntimeDirectory = "wgautomesh"; + AmbientCapabilities = "CAP_NET_ADMIN"; + CapabilityBoundingSet = "CAP_NET_ADMIN"; }; - networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.gossipPort ]; - }); - } - + wantedBy = [ "multi-user.target" ]; + }; + networking.firewall.allowedUDPPorts = + mkIf cfg.openFirewall [ cfg.settings.gossip_port ]; + }; +}