1
1
Fork 1
mirror of https://github.com/oddlama/nix-config.git synced 2025-10-10 23:00:39 +02:00

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
- `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

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"];
listen = true;
listenPort = 51822;
openFirewall = true;
externalPeers = {
test1 = ["10.0.0.91/32"];
test2 = ["10.0.0.92/32"];

View file

@ -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";
};
});
}

View file

@ -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)}
''

View file

@ -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
''

View file

@ -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;
};
}