refactor(wireguard): extract cross-host aggregation functions into extraLib

This commit is contained in:
oddlama 2023-04-14 14:32:17 +02:00
parent 6cffccd75c
commit d522a46f1d
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
6 changed files with 239 additions and 195 deletions

View file

@ -37,6 +37,7 @@ This is my personal nix config.
- `dev-shell.nix` Environment setup for `nix develop` for using this flake - `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 - `extra-builtins.nix` Extra builtins via nix-plugins to support transparent repository-wide secrets
- `hosts.nix` Wrapper that extracts all defined hosts from `hosts/` - `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 - `rage-decrypt.sh` Auxiliary script for repository-wide secrets
- `secrets.nix` Helper to access repository-wide secrets, used by colmena.nix - `secrets.nix` Helper to access repository-wide secrets, used by colmena.nix
- `secrets/` Global secrets and age identities - `secrets/` Global secrets and age identities
@ -57,6 +58,12 @@ This is my personal nix config.
- fill net.nix - fill net.nix
- todo: hostid (move to nodeSecrets) - todo: hostid (move to nodeSecrets)
- generate-initrd-keys - 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 #### New secret

View file

@ -22,11 +22,13 @@
}; };
}; };
extra.wireguard.networks.vms = { extra.wireguard.vms = {
server = {
enable = true;
port = 51822;
openFirewall = true;
};
address = ["10.0.0.1/24"]; address = ["10.0.0.1/24"];
listen = true;
listenPort = 51822;
openFirewall = true;
externalPeers = { externalPeers = {
test1 = ["10.0.0.91/32"]; test1 = ["10.0.0.91/32"];
test2 = ["10.0.0.92/32"]; test2 = ["10.0.0.92/32"];

View file

@ -11,88 +11,50 @@
(lib) (lib)
any any
attrNames attrNames
attrValues
concatMap concatMap
concatMapStrings concatMapStrings
concatStringsSep concatStringsSep
filter filterAttrs
flatten
foldl'
genAttrs
head head
mapAttrs'
mapAttrsToList mapAttrsToList
mdDoc mdDoc
mergeAttrs mergeAttrs
mkIf mkIf
mkOption mkOption
nameValuePair mkEnableOption
optional optionalAttrs
recursiveUpdate
splitString splitString
types types
; ;
inherit (extraLib) duplicates; inherit
(extraLib)
concatAttrs
duplicates
;
cfg = config.extra.wireguard; cfg = config.extra.wireguard;
sortedPeers = peerA: peerB:
if peerA < peerB
then {
peer1 = peerA;
peer2 = peerB;
}
else {
peer1 = peerB;
peer2 = peerA;
};
configForNetwork = wgName: wg: let configForNetwork = wgName: wg: let
peerPublicKey = peerName: builtins.readFile (../secrets/wireguard + "/${wgName}/keys/${peerName}.pub"); inherit
peerPrivateKeyFile = peerName: ../secrets/wireguard + "/${wgName}/keys/${peerName}.age"; (extraLib.wireguard wgName)
peerPrivateKeySecret = peerName: "wireguard-${wgName}-priv-${peerName}"; allPeers
peerPresharedKeyPath
peerPresharedKeySecret
peerPrivateKeyPath
peerPrivateKeySecret
peerPublicKeyPath
;
peerPresharedKeyFile = peerA: peerB: let otherPeers = filterAttrs (n: _: n != nodeName) (allPeers nodes);
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;
};
};
in { in {
inherit nodesWithThisNetwork wgName;
secrets = secrets =
foldl' mergeAttrs { concatAttrs (map (other: {
${peerPrivateKeySecret nodeName}.file = peerPrivateKeyFile nodeName; ${peerPresharedKeySecret nodeName other}.file = peerPresharedKeyPath nodeName other;
} (map (peerName: { }) (attrNames otherPeers))
${peerPresharedKeySecret nodeName peerName}.file = peerPresharedKeyFile nodeName peerName; // {
}) (attrNames peers)); ${peerPrivateKeySecret nodeName}.file = peerPrivateKeyPath nodeName;
};
netdevs."${wg.priority}-${wgName}" = { netdevs."${wg.priority}-${wgName}" = {
netdevConfig = { netdevConfig = {
@ -100,11 +62,26 @@
Name = "${wgName}"; Name = "${wgName}";
Description = "Wireguard network ${wgName}"; Description = "Wireguard network ${wgName}";
}; };
wireguardConfig = { wireguardConfig =
PrivateKeyFile = config.rekey.secrets.${peerPrivateKeySecret nodeName}.path; {
ListenPort = wg.listenPort; PrivateKeyFile = config.rekey.secrets.${peerPrivateKeySecret nodeName}.path;
}; }
wireguardPeers = mapAttrsToList peerDefinition peers; // 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}" = { networks."${wg.priority}-${wgName}" = {
@ -113,95 +90,88 @@
}; };
}; };
in { in {
options = { options.extra.wireguard = mkOption {
extra.wireguard.networks = mkOption { default = {};
default = {}; description = "Configures wireguard networks via systemd-networkd.";
description = "Configures wireguard networks via systemd-networkd."; type = types.attrsOf (types.submodule {
type = types.attrsOf (types.submodule { options = {
options = { server = {
address = mkOption { enable = mkEnableOption (mdDoc "wireguard server");
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.
'';
};
listen = mkOption { port = 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 {
default = 51820; default = 51820;
type = types.int; type = types.port;
description = mdDoc "The port to listen on, if {option}`listen` is `true`."; 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 { openFirewall = mkOption {
default = false; default = false;
type = types.bool; type = types.bool;
description = mdDoc "Whether to open the firewall for the specified `listenPort`, if {option}`listen` is `true`."; 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 config = mkIf (cfg != {}) (let
networkCfgs = mapAttrsToList configForNetwork cfg.networks; networkCfgs = mapAttrsToList configForNetwork cfg;
collectAttrs = x: foldl' mergeAttrs {} (map (y: y.${x}) networkCfgs); collectAllNetworkAttrs = x: concatAttrs (map (y: y.${x}) networkCfgs);
in { in {
assertions = assertions = concatMap (wgName: let
concatMap (netCfg: let inherit
inherit (netCfg) wgName; (extraLib.wireguard wgName)
externalPeers = concatMap (n: attrNames nodes.${n}.config.extra.wireguard.networks.${wgName}.externalPeers) netCfg.nodesWithThisNetwork; externalPeerNamesRaw
duplicatePeers = duplicates externalPeers; usedAddresses
usedAddresses = associatedNodes
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;
networking.firewall.allowedUDPPorts = mkIf (cfg.listen && cfg.openFirewall) [cfg.listenPort]; duplicatePeers = duplicates (externalPeerNamesRaw nodes);
rekey.secrets = collectAttrs "secrets"; 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 = { systemd.network = {
netdevs = collectAttrs "netdevs"; netdevs = collectAllNetworkAttrs "netdevs";
networks = collectAttrs "networks"; networks = collectAllNetworkAttrs "networks";
}; };
}); });
} }

View file

@ -2,7 +2,7 @@
self, self,
pkgs, pkgs,
... ...
}: let } @ inputs: let
inherit inherit
(pkgs.lib) (pkgs.lib)
attrNames attrNames
@ -11,10 +11,12 @@
concatStringsSep concatStringsSep
escapeShellArg escapeShellArg
filter filter
removeSuffix
substring substring
unique unique
; ;
extraLib = import ../lib.nix inputs;
isAbsolutePath = x: substring 0 1 x == "/"; isAbsolutePath = x: substring 0 1 x == "/";
masterIdentityArgs = concatMapStrings (x: ''-i ${escapeShellArg x} '') self.secrets.masterIdentities; masterIdentityArgs = concatMapStrings (x: ''-i ${escapeShellArg x} '') self.secrets.masterIdentities;
extraEncryptionPubkeys = extraEncryptionPubkeys =
@ -26,57 +28,55 @@
) )
self.secrets.extraEncryptionPubkeys; self.secrets.extraEncryptionPubkeys;
sortedPeers = peerA: peerB:
if peerA < peerB
then {
peer1 = peerA;
peer2 = peerB;
}
else {
peer1 = peerB;
peer2 = peerA;
};
nodeNames = attrNames self.nodes; 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) 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);
externalPeers = wgName: concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard.networks.${wgName}.externalPeers) (nodesWithNet wgName); generateNetworkKeys = wgName: let
peers = wgName: nodesWithNet wgName ++ externalPeers wgName; inherit
(extraLib.wireguard wgName)
allPeers
associatedNodes
peerPresharedKeyFile
peerPrivateKeyFile
peerPublicKeyFile
;
peerKeyBasename = wgName: peerName: "./secrets/wireguard/${wgName}/keys/${peerName}"; nodesWithNet = associatedNodes self.nodes;
generatePeerKeys = wgName: peerName: let peers = attrNames (allPeers self.nodes);
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
'';
generatePeerPsks = wgName: nodePeerName: generatePeerKeys = peerName: let
concatStringsSep "\n" (map (peerName: let keyBasename = escapeShellArg ("./" + removeSuffix ".pub" (peerPublicKeyFile peerName));
inherit (sortedPeers nodePeerName peerName) peer1 peer2; pubkeyFile = escapeShellArg ("./" + peerPublicKeyFile peerName);
pskFile = "./secrets/wireguard/${wgName}/psks/${peer1}-${peer2}.age"; privkeyFile = escapeShellArg ("./" + peerPrivateKeyFile peerName);
in '' in ''
if [[ ! -e ${pskFile} ]]; then if [[ ! -e ${privkeyFile} ]] || [[ ! -e ${pubkeyFile} ]]; then
mkdir -p $(dirname ${pskFile}) mkdir -p $(dirname ${privkeyFile})
echo "Generating "${pskFile}"" echo "Generating "${keyBasename}".{age,pub}"
psk=$(${pkgs.wireguard-tools}/bin/wg genpsk) privkey=$(${pkgs.wireguard-tools}/bin/wg genkey)
${pkgs.rage}/bin/rage -e ${masterIdentityArgs} ${extraEncryptionPubkeys} <<< "$psk" > ${pskFile} \ echo "$privkey" | ${pkgs.wireguard-tools}/bin/wg pubkey > ${pubkeyFile}
|| { echo "error: Failed to encrypt wireguard psk for peers ${peer1} and ${peer2} on network ${wgName}!" >&2; exit 1; } ${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 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 in
pkgs.writeShellScript "generate-wireguard-keys" '' pkgs.writeShellScript "generate-wireguard-keys" ''
set -euo pipefail set -euo pipefail
${concatStringsSep "\n" (concatMap (wgName: map (generatePeerKeys wgName) (peers wgName)) wireguardNetworks)} ${concatStringsSep "\n" (concatMap generateNetworkKeys wireguardNetworks)}
${concatStringsSep "\n" (concatMap (wgName: map (generatePeerPsks wgName) (nodesWithNet wgName)) wireguardNetworks)}
'' ''

View file

@ -2,7 +2,7 @@
self, self,
pkgs, pkgs,
... ...
}: let } @ inputs: let
inherit inherit
(pkgs.lib) (pkgs.lib)
attrNames attrNames
@ -13,14 +13,18 @@
unique unique
; ;
extraLib = import ../lib.nix inputs;
nodeNames = attrNames self.nodes; 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) 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); externalPeersForNet = wgName:
externalPeers = concatMap (net: map (peer: {inherit net peer;}) (externalPeersForNet net)) wireguardNetworks; map (peer: {inherit wgName peer;})
(attrNames ((extraLib.wireguard wgName).externalPeers self.nodes));
allExternalPeers = concatMap externalPeersForNet wireguardNetworks;
in in
# TODO generate "classic" config and run qrencode # TODO generate "classic" config and run qrencode
pkgs.writeShellScript "show-wireguard-qr" '' pkgs.writeShellScript "show-wireguard-qr" ''
set -euo pipefail 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
'' ''

View file

@ -1,8 +1,16 @@
{nixpkgs, ...}: let {nixpkgs, ...}: let
inherit inherit
(nixpkgs.lib) (nixpkgs.lib)
attrNames
attrValues
concatMap
filter filter
flatten
foldl' foldl'
genAttrs
mapAttrs'
mergeAttrs
nameValuePair
unique unique
; ;
in rec { in rec {
@ -20,4 +28,57 @@ in rec {
occurrences = countOccurrences xs; occurrences = countOccurrences xs;
in in
unique (filter (x: occurrences.${x} > 1) xs); 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;
};
} }