From d522a46f1dd9e2f6a601dde645187db8607a4dc5 Mon Sep 17 00:00:00 2001 From: oddlama Date: Fri, 14 Apr 2023 14:32:17 +0200 Subject: [PATCH] refactor(wireguard): extract cross-host aggregation functions into extraLib --- README.md | 7 + hosts/ward/net.nix | 10 +- modules/wireguard.nix | 250 ++++++++++++--------------- nix/apps/generate-wireguard-keys.nix | 90 +++++----- nix/apps/show-wireguard-qr.nix | 16 +- nix/lib.nix | 61 +++++++ 6 files changed, 239 insertions(+), 195 deletions(-) diff --git a/README.md b/README.md index 2993955..9061037 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ This is my personal nix config. - `dev-shell.nix` Environment setup for `nix develop` for using this flake - `extra-builtins.nix` Extra builtins via nix-plugins to support transparent repository-wide secrets - `hosts.nix` Wrapper that extracts all defined hosts from `hosts/` + - `lib.nix` Commonly used functionality or helpers that weren't available in the standard library - `rage-decrypt.sh` Auxiliary script for repository-wide secrets - `secrets.nix` Helper to access repository-wide secrets, used by colmena.nix - `secrets/` Global secrets and age identities @@ -57,6 +58,12 @@ This is my personal nix config. - fill net.nix - todo: hostid (move to nodeSecrets) - generate-initrd-keys +- generate-wireguard-keys + +#### Show QR for external wireguard client + +nix run show-wireguard-qr +then select the host in the fzf menu #### New secret diff --git a/hosts/ward/net.nix b/hosts/ward/net.nix index f07f6e4..5d9c0e3 100644 --- a/hosts/ward/net.nix +++ b/hosts/ward/net.nix @@ -22,11 +22,13 @@ }; }; - extra.wireguard.networks.vms = { + extra.wireguard.vms = { + server = { + enable = true; + port = 51822; + openFirewall = true; + }; address = ["10.0.0.1/24"]; - listen = true; - listenPort = 51822; - openFirewall = true; externalPeers = { test1 = ["10.0.0.91/32"]; test2 = ["10.0.0.92/32"]; diff --git a/modules/wireguard.nix b/modules/wireguard.nix index c423ed0..6b0c78d 100644 --- a/modules/wireguard.nix +++ b/modules/wireguard.nix @@ -11,88 +11,50 @@ (lib) any attrNames - attrValues concatMap concatMapStrings concatStringsSep - filter - flatten - foldl' - genAttrs + filterAttrs head - mapAttrs' mapAttrsToList mdDoc mergeAttrs mkIf mkOption - nameValuePair - optional - recursiveUpdate + mkEnableOption + optionalAttrs splitString types ; - inherit (extraLib) duplicates; + inherit + (extraLib) + concatAttrs + duplicates + ; cfg = config.extra.wireguard; - sortedPeers = peerA: peerB: - if peerA < peerB - then { - peer1 = peerA; - peer2 = peerB; - } - else { - peer1 = peerB; - peer2 = peerA; - }; - configForNetwork = wgName: wg: let - peerPublicKey = peerName: builtins.readFile (../secrets/wireguard + "/${wgName}/keys/${peerName}.pub"); - peerPrivateKeyFile = peerName: ../secrets/wireguard + "/${wgName}/keys/${peerName}.age"; - peerPrivateKeySecret = peerName: "wireguard-${wgName}-priv-${peerName}"; + inherit + (extraLib.wireguard wgName) + allPeers + peerPresharedKeyPath + peerPresharedKeySecret + peerPrivateKeyPath + peerPrivateKeySecret + peerPublicKeyPath + ; - peerPresharedKeyFile = peerA: peerB: let - inherit (sortedPeers peerA peerB) peer1 peer2; - in - ../secrets/wireguard + "/${wgName}/psks/${peer1}-${peer2}.age"; - - peerPresharedKeySecret = peerA: peerB: let - inherit (sortedPeers peerA peerB) peer1 peer2; - in "wireguard-${wgName}-psks-${peer1}-${peer2}"; - - # All peers that are other nodes - nodesWithThisNetwork = filter (n: builtins.hasAttr wgName nodes.${n}.config.extra.wireguard.networks) (attrNames nodes); - nodePeers = genAttrs (filter (n: n != nodeName) nodesWithThisNetwork) (n: nodes.${n}.config.extra.wireguard.networks.${wgName}.address); - # All peers that are defined as externalPeers on any node. Also prepends "external-" to their name. - externalPeers = foldl' recursiveUpdate {} ( - map (n: mapAttrs' (extPeerName: nameValuePair "external-${extPeerName}") nodes.${n}.config.extra.wireguard.networks.${wgName}.externalPeers) - nodesWithThisNetwork - ); - - peers = nodePeers // externalPeers; - - peerDefinition = peerName: peerAllowedIPs: { - wireguardPeerConfig = - { - PublicKey = peerPublicKey wgName peerName; - PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret wgName nodeName peerName}.path; - AllowedIPs = peerAllowedIPs; - } - // optional wg.listen { - PersistentKeepalive = 25; - }; - }; + otherPeers = filterAttrs (n: _: n != nodeName) (allPeers nodes); in { - inherit nodesWithThisNetwork wgName; - secrets = - foldl' mergeAttrs { - ${peerPrivateKeySecret nodeName}.file = peerPrivateKeyFile nodeName; - } (map (peerName: { - ${peerPresharedKeySecret nodeName peerName}.file = peerPresharedKeyFile nodeName peerName; - }) (attrNames peers)); + concatAttrs (map (other: { + ${peerPresharedKeySecret nodeName other}.file = peerPresharedKeyPath nodeName other; + }) (attrNames otherPeers)) + // { + ${peerPrivateKeySecret nodeName}.file = peerPrivateKeyPath nodeName; + }; netdevs."${wg.priority}-${wgName}" = { netdevConfig = { @@ -100,11 +62,26 @@ Name = "${wgName}"; Description = "Wireguard network ${wgName}"; }; - wireguardConfig = { - PrivateKeyFile = config.rekey.secrets.${peerPrivateKeySecret nodeName}.path; - ListenPort = wg.listenPort; - }; - wireguardPeers = mapAttrsToList peerDefinition peers; + wireguardConfig = + { + PrivateKeyFile = config.rekey.secrets.${peerPrivateKeySecret nodeName}.path; + } + // optionalAttrs wg.server.enable { + ListenPort = wg.server.port; + }; + wireguardPeers = + mapAttrsToList (peerName: peerAllowedIPs: { + wireguardPeerConfig = + { + PublicKey = builtins.readFile (peerPublicKeyPath peerName); + PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName peerName}.path; + AllowedIPs = peerAllowedIPs; + } + // optionalAttrs wg.server.enable { + PersistentKeepalive = 25; + }; + }) + otherPeers; }; networks."${wg.priority}-${wgName}" = { @@ -113,95 +90,88 @@ }; }; in { - options = { - extra.wireguard.networks = mkOption { - default = {}; - description = "Configures wireguard networks via systemd-networkd."; - type = types.attrsOf (types.submodule { - options = { - address = mkOption { - type = types.listOf types.str; - description = mdDoc '' - The addresses to configure for this interface. Will automatically be added - as this peer's allowed addresses to all other peers. - ''; - }; + options.extra.wireguard = mkOption { + default = {}; + description = "Configures wireguard networks via systemd-networkd."; + type = types.attrsOf (types.submodule { + options = { + server = { + enable = mkEnableOption (mdDoc "wireguard server"); - listen = mkOption { - type = types.bool; - default = false; - description = mdDoc '' - Enables listening for incoming wireguard connections. - This also causes all other peers to include this as an endpoint in their configuration. - ''; - }; - - listenPort = mkOption { + port = mkOption { default = 51820; - type = types.int; + type = types.port; description = mdDoc "The port to listen on, if {option}`listen` is `true`."; }; - priority = mkOption { - default = "20"; - type = types.str; - description = mdDoc "The order priority used when creating systemd netdev and network files."; - }; - openFirewall = mkOption { default = false; type = types.bool; description = mdDoc "Whether to open the firewall for the specified `listenPort`, if {option}`listen` is `true`."; }; - - externalPeers = mkOption { - type = types.attrsOf (types.listOf types.str); - default = {}; - example = {my-android-phone = ["10.0.0.97/32"];}; - description = mdDoc '' - Allows defining extra set of external peers that should be added to the configuration. - For each external peers you can define one or multiple allowed ips. - ''; - }; }; - }); - }; + + priority = mkOption { + default = "20"; + type = types.str; + description = mdDoc "The order priority used when creating systemd netdev and network files."; + }; + + address = mkOption { + type = types.listOf types.str; + description = mdDoc '' + The addresses to configure for this interface. Will automatically be added + as this peer's allowed addresses to all other peers. + ''; + }; + + externalPeers = mkOption { + type = types.attrsOf (types.listOf types.str); + default = {}; + example = {my-android-phone = ["10.0.0.97/32"];}; + description = mdDoc '' + Allows defining extra set of external peers that should be added to the configuration. + For each external peers you can define one or multiple allowed ips. + ''; + }; + }; + }); }; - config = mkIf (cfg.networks != {}) (let - networkCfgs = mapAttrsToList configForNetwork cfg.networks; - collectAttrs = x: foldl' mergeAttrs {} (map (y: y.${x}) networkCfgs); + config = mkIf (cfg != {}) (let + networkCfgs = mapAttrsToList configForNetwork cfg; + collectAllNetworkAttrs = x: concatAttrs (map (y: y.${x}) networkCfgs); in { - assertions = - concatMap (netCfg: let - inherit (netCfg) wgName; - externalPeers = concatMap (n: attrNames nodes.${n}.config.extra.wireguard.networks.${wgName}.externalPeers) netCfg.nodesWithThisNetwork; - duplicatePeers = duplicates externalPeers; - usedAddresses = - concatMap (n: nodes.${n}.config.extra.wireguard.networks.${wgName}.address) netCfg.nodesWithThisNetwork - ++ flatten (concatMap (n: attrValues nodes.${n}.config.extra.wireguard.networks.${wgName}.externalPeers) netCfg.nodesWithThisNetwork); - duplicateAddrs = duplicates (map (x: head (splitString "/" x)) usedAddresses); - in [ - { - assertion = any (n: nodes.${n}.config.extra.wireguard.networks.${wgName}.listen) netCfg.nodesWithThisNetwork; - message = "Wireguard network '${wgName}': At least one node must be listening."; - } - { - assertion = duplicatePeers == []; - message = "Wireguard network '${wgName}': Multiple definitions for external peer(s):${concatMapStrings (x: " '${x}'") duplicatePeers}"; - } - { - assertion = duplicateAddrs == []; - message = "Wireguard network '${wgName}': Addresses used multiple times: ${concatStringsSep ", " duplicateAddrs}"; - } - ]) - networkCfgs; + assertions = concatMap (wgName: let + inherit + (extraLib.wireguard wgName) + externalPeerNamesRaw + usedAddresses + associatedNodes + ; - networking.firewall.allowedUDPPorts = mkIf (cfg.listen && cfg.openFirewall) [cfg.listenPort]; - rekey.secrets = collectAttrs "secrets"; + duplicatePeers = duplicates (externalPeerNamesRaw nodes); + duplicateAddrs = duplicates (map (x: head (splitString "/" x)) (usedAddresses nodes)); + in [ + { + assertion = any (n: nodes.${n}.config.extra.wireguard.${wgName}.server.enable) (associatedNodes nodes); + message = "Wireguard network '${wgName}': At least one node must be a server."; + } + { + assertion = duplicatePeers == []; + message = "Wireguard network '${wgName}': Multiple definitions for external peer(s):${concatMapStrings (x: " '${x}'") duplicatePeers}"; + } + { + assertion = duplicateAddrs == []; + message = "Wireguard network '${wgName}': Addresses used multiple times: ${concatStringsSep ", " duplicateAddrs}"; + } + ]) (attrNames cfg); + + networking.firewall.allowedUDPPorts = mkIf (cfg.server.enable && cfg.server.openFirewall) [cfg.server.port]; + rekey.secrets = collectAllNetworkAttrs "secrets"; systemd.network = { - netdevs = collectAttrs "netdevs"; - networks = collectAttrs "networks"; + netdevs = collectAllNetworkAttrs "netdevs"; + networks = collectAllNetworkAttrs "networks"; }; }); } diff --git a/nix/apps/generate-wireguard-keys.nix b/nix/apps/generate-wireguard-keys.nix index 8342cc1..5efdeb8 100644 --- a/nix/apps/generate-wireguard-keys.nix +++ b/nix/apps/generate-wireguard-keys.nix @@ -2,7 +2,7 @@ self, pkgs, ... -}: let +} @ inputs: let inherit (pkgs.lib) attrNames @@ -11,10 +11,12 @@ concatStringsSep escapeShellArg filter + removeSuffix substring unique ; + extraLib = import ../lib.nix inputs; isAbsolutePath = x: substring 0 1 x == "/"; masterIdentityArgs = concatMapStrings (x: ''-i ${escapeShellArg x} '') self.secrets.masterIdentities; extraEncryptionPubkeys = @@ -26,57 +28,55 @@ ) self.secrets.extraEncryptionPubkeys; - sortedPeers = peerA: peerB: - if peerA < peerB - then { - peer1 = peerA; - peer2 = peerB; - } - else { - peer1 = peerB; - peer2 = peerA; - }; - nodeNames = attrNames self.nodes; - nodesWithNet = wgName: filter (n: builtins.hasAttr wgName self.nodes.${n}.config.extra.wireguard.networks) nodeNames; - wireguardNetworks = unique (concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard.networks) nodeNames); - externalPeersForNet = wgName: concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard.networks.${wgName}.externalPeers) (nodesWithNet wgName); + wireguardNetworks = unique (concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard) nodeNames); - externalPeers = wgName: concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard.networks.${wgName}.externalPeers) (nodesWithNet wgName); - peers = wgName: nodesWithNet wgName ++ externalPeers wgName; + generateNetworkKeys = wgName: let + inherit + (extraLib.wireguard wgName) + allPeers + associatedNodes + peerPresharedKeyFile + peerPrivateKeyFile + peerPublicKeyFile + ; - peerKeyBasename = wgName: peerName: "./secrets/wireguard/${wgName}/keys/${peerName}"; - generatePeerKeys = wgName: peerName: let - keyBasename = peerKeyBasename wgName peerName; - privkeyFile = escapeShellArg "${keyBasename}.age"; - pubkeyFile = escapeShellArg "${keyBasename}.pub"; - in '' - if [[ ! -e ${privkeyFile} ]] || [[ ! -e ${pubkeyFile} ]]; then - mkdir -p $(dirname ${privkeyFile}) - echo "Generating "${escapeShellArg keyBasename}".{age,pub}" - privkey=$(${pkgs.wireguard-tools}/bin/wg genkey) - echo "$privkey" | ${pkgs.wireguard-tools}/bin/wg pubkey > ${pubkeyFile} - ${pkgs.rage}/bin/rage -e ${masterIdentityArgs} ${extraEncryptionPubkeys} <<< "$privkey" > ${pubkeyFile} \ - || { echo "error: Failed to encrypt wireguard private key for peer ${peerName} on network ${wgName}!" >&2; exit 1; } - fi - ''; + nodesWithNet = associatedNodes self.nodes; + peers = attrNames (allPeers self.nodes); - generatePeerPsks = wgName: nodePeerName: - concatStringsSep "\n" (map (peerName: let - inherit (sortedPeers nodePeerName peerName) peer1 peer2; - pskFile = "./secrets/wireguard/${wgName}/psks/${peer1}-${peer2}.age"; + generatePeerKeys = peerName: let + keyBasename = escapeShellArg ("./" + removeSuffix ".pub" (peerPublicKeyFile peerName)); + pubkeyFile = escapeShellArg ("./" + peerPublicKeyFile peerName); + privkeyFile = escapeShellArg ("./" + peerPrivateKeyFile peerName); in '' - if [[ ! -e ${pskFile} ]]; then - mkdir -p $(dirname ${pskFile}) - echo "Generating "${pskFile}"" - psk=$(${pkgs.wireguard-tools}/bin/wg genpsk) - ${pkgs.rage}/bin/rage -e ${masterIdentityArgs} ${extraEncryptionPubkeys} <<< "$psk" > ${pskFile} \ - || { echo "error: Failed to encrypt wireguard psk for peers ${peer1} and ${peer2} on network ${wgName}!" >&2; exit 1; } + if [[ ! -e ${privkeyFile} ]] || [[ ! -e ${pubkeyFile} ]]; then + mkdir -p $(dirname ${privkeyFile}) + echo "Generating "${keyBasename}".{age,pub}" + privkey=$(${pkgs.wireguard-tools}/bin/wg genkey) + echo "$privkey" | ${pkgs.wireguard-tools}/bin/wg pubkey > ${pubkeyFile} + ${pkgs.rage}/bin/rage -e ${masterIdentityArgs} ${extraEncryptionPubkeys} <<< "$privkey" > ${privkeyFile} \ + || { echo "error: Failed to encrypt wireguard private key for peer ${peerName} on network ${wgName}!" >&2; exit 1; } fi - '') (filter (x: x != nodePeerName) (peers wgName))); + ''; + + generatePeerPsks = nodePeerName: + map (peerName: let + pskFile = escapeShellArg ("./" + peerPresharedKeyFile nodePeerName peerName); + in '' + if [[ ! -e ${pskFile} ]]; then + mkdir -p $(dirname ${pskFile}) + echo "Generating "${pskFile}"" + psk=$(${pkgs.wireguard-tools}/bin/wg genpsk) + ${pkgs.rage}/bin/rage -e ${masterIdentityArgs} ${extraEncryptionPubkeys} <<< "$psk" > ${pskFile} \ + || { echo "error: Failed to encrypt wireguard psk for peers ${nodePeerName} and ${peerName} on network ${wgName}!" >&2; exit 1; } + fi + '') (filter (x: x != nodePeerName) peers); + in + ["echo ==== ${wgName} ===="] + ++ map generatePeerKeys peers + ++ concatMap generatePeerPsks nodesWithNet; in pkgs.writeShellScript "generate-wireguard-keys" '' set -euo pipefail - ${concatStringsSep "\n" (concatMap (wgName: map (generatePeerKeys wgName) (peers wgName)) wireguardNetworks)} - ${concatStringsSep "\n" (concatMap (wgName: map (generatePeerPsks wgName) (nodesWithNet wgName)) wireguardNetworks)} + ${concatStringsSep "\n" (concatMap generateNetworkKeys wireguardNetworks)} '' diff --git a/nix/apps/show-wireguard-qr.nix b/nix/apps/show-wireguard-qr.nix index 756bbb9..0a5da1f 100644 --- a/nix/apps/show-wireguard-qr.nix +++ b/nix/apps/show-wireguard-qr.nix @@ -2,7 +2,7 @@ self, pkgs, ... -}: let +} @ inputs: let inherit (pkgs.lib) attrNames @@ -13,14 +13,18 @@ unique ; + extraLib = import ../lib.nix inputs; + nodeNames = attrNames self.nodes; - nodesWithNet = net: filter (n: builtins.hasAttr net self.nodes.${n}.config.extra.wireguard.networks) nodeNames; - wireguardNetworks = unique (concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard.networks) nodeNames); - externalPeersForNet = net: concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard.networks.${net}.externalPeers) (nodesWithNet net); - externalPeers = concatMap (net: map (peer: {inherit net peer;}) (externalPeersForNet net)) wireguardNetworks; + wireguardNetworks = unique (concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard) nodeNames); + + externalPeersForNet = wgName: + map (peer: {inherit wgName peer;}) + (attrNames ((extraLib.wireguard wgName).externalPeers self.nodes)); + allExternalPeers = concatMap externalPeersForNet wireguardNetworks; in # TODO generate "classic" config and run qrencode pkgs.writeShellScript "show-wireguard-qr" '' set -euo pipefail - echo ${escapeShellArg (concatStringsSep "\n" (map (x: "${x.net}.${x.peer}") externalPeers))} | fzf + echo ${escapeShellArg (concatStringsSep "\n" (map (x: "${x.wgName}.${x.peer}") allExternalPeers))} | ${pkgs.fzf}/bin/fzf '' diff --git a/nix/lib.nix b/nix/lib.nix index 20bea90..fa51204 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -1,8 +1,16 @@ {nixpkgs, ...}: let inherit (nixpkgs.lib) + attrNames + attrValues + concatMap filter + flatten foldl' + genAttrs + mapAttrs' + mergeAttrs + nameValuePair unique ; in rec { @@ -20,4 +28,57 @@ in rec { occurrences = countOccurrences xs; in unique (filter (x: occurrences.${x} > 1) xs); + + # Concatenates all given attrsets as if calling a // b in order. + concatAttrs = foldl' mergeAttrs {}; + + # Wireguard related functions that are reused in several files of this flake + wireguard = wgName: rec { + _sortedPeers = peerA: peerB: + if peerA < peerB + then { + peer1 = peerA; + peer2 = peerB; + } + else { + peer1 = peerB; + peer2 = peerA; + }; + + peerPublicKeyFile = peerName: "secrets/wireguard/${wgName}/keys/${peerName}.pub"; + peerPublicKeyPath = peerName: "${../.}/" + peerPublicKeyFile peerName; + + peerPrivateKeyFile = peerName: "secrets/wireguard/${wgName}/keys/${peerName}.age"; + peerPrivateKeyPath = peerName: "${../.}/" + peerPrivateKeyFile peerName; + peerPrivateKeySecret = peerName: "wireguard-${wgName}-priv-${peerName}"; + + peerPresharedKeyFile = peerA: peerB: let + inherit (_sortedPeers peerA peerB) peer1 peer2; + in "secrets/wireguard/${wgName}/psks/${peer1}+${peer2}.age"; + peerPresharedKeyPath = peerA: peerB: "${../.}/" + peerPresharedKeyFile peerA peerB; + peerPresharedKeySecret = peerA: peerB: let + inherit (_sortedPeers peerA peerB) peer1 peer2; + in "wireguard-${wgName}-psks-${peer1}+${peer2}"; + + # All nodes that are part of this network + associatedNodes = nodes: filter (n: builtins.hasAttr wgName nodes.${n}.config.extra.wireguard) (attrNames nodes); + nodePeers = nodes: genAttrs (associatedNodes nodes) (n: nodes.${n}.config.extra.wireguard.${wgName}.address); + # All peers that are defined as externalPeers on any node. + # Prepends "external-" to their name. + externalPeers = nodes: + concatAttrs ( + map (n: mapAttrs' (extPeerName: nameValuePair "external-${extPeerName}") nodes.${n}.config.extra.wireguard.${wgName}.externalPeers) + (associatedNodes nodes) + ); + # Concatenation of all external peer names names without any transformations. + externalPeerNamesRaw = nodes: concatMap (n: attrNames nodes.${n}.config.extra.wireguard.${wgName}.externalPeers) (associatedNodes nodes); + # A list of all occurring addresses. + usedAddresses = nodes: let + nodesWithNet = associatedNodes nodes; + in + concatMap (n: nodes.${n}.config.extra.wireguard.${wgName}.address) nodesWithNet + ++ flatten (concatMap (n: attrValues nodes.${n}.config.extra.wireguard.${wgName}.externalPeers) nodesWithNet); + + allPeers = nodes: nodePeers nodes // externalPeers nodes; + }; }