feat: rework wireguard modules using globals

This commit is contained in:
Patrick 2025-02-08 21:27:28 +01:00
parent 93b08971cf
commit 853c9e2a2d
No known key found for this signature in database
GPG key ID: 451F95EFB8BECD0F
6 changed files with 442 additions and 622 deletions

View file

@ -17,6 +17,7 @@ in
These should not include any config only option declaration. These should not include any config only option declaration.
Will be included in the exported nixos Modules from this flake to be included Will be included in the exported nixos Modules from this flake to be included
into the host evaluation. into the host evaluation.
Be aware that at most 1 of these modules can have a default
''; '';
}; };
defModules = mkOption { defModules = mkOption {
@ -38,7 +39,14 @@ in
''; '';
}; };
}; };
config.flake = flakeSubmod: { config = {
globals = {
optModules = [
../modules/wireguardGlobals.nix
];
attrkeys = [ "wireguard" ];
};
flake = flakeSubmod: {
globals = globals =
let let
globalsSystem = lib.evalModules { globalsSystem = lib.evalModules {
@ -71,4 +79,5 @@ in
in in
lib.genAttrs config.globals.attrkeys (x: globalsSystem.config.globals.${x}); lib.genAttrs config.globals.attrkeys (x: globalsSystem.config.globals.${x});
}; };
};
} }

View file

@ -1,178 +0,0 @@
inputs: final: prev: let
inherit
(inputs.nixpkgs.lib)
assertMsg
attrNames
attrValues
concatLists
concatMap
filter
flip
genAttrs
partition
warn
;
inherit
(final.lib)
net
types
;
in {
lib =
prev.lib
// rec {
wireguard.evaluateNetwork = userInputs: wgName: let
inherit (userInputs.self) nodes;
# Returns the given node's wireguard configuration of this network
wgCfgOf = node: nodes.${node}.config.wireguard.${wgName};
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: userInputs.self.outPath + peerPublicKeyFile peerName;
peerPrivateKeyFile = peerName: "/secrets/wireguard/${wgName}/keys/${peerName}.age";
peerPrivateKeyPath = peerName: userInputs.self.outPath + 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: userInputs.self.outPath + 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
participatingNodes = filter (n: builtins.hasAttr wgName nodes.${n}.config.wireguard) (
attrNames nodes
);
# Partition nodes by whether they are servers
_participatingNodes_isServerPartition =
partition (
n: (wgCfgOf n).server.host != null
)
participatingNodes;
participatingServerNodes = _participatingNodes_isServerPartition.right;
participatingClientNodes = _participatingNodes_isServerPartition.wrong;
# Maps all nodes that are part of this network to their addresses
nodePeers = genAttrs participatingNodes (n: (wgCfgOf n).addresses);
# A list of all occurring addresses.
usedAddresses = concatMap (n: (wgCfgOf n).addresses) participatingNodes;
# A list of all occurring addresses, but only includes addresses that
# are not assigned automatically.
explicitlyUsedAddresses = flip concatMap participatingNodes (
n:
filter (x: !types.isLazyValue x) (
concatLists
(nodes.${n}.options.wireguard.type.nestedTypes.elemType.getSubOptions (wgCfgOf n))
.addresses
.definitions
)
);
# The cidrv4 and cidrv6 of the network spanned by all participating peer addresses.
# This also takes into account any reserved address ranges that should be part of the network.
networkAddresses = net.cidr.merge (
usedAddresses ++ concatMap (n: (wgCfgOf n).server.reservedAddresses) participatingServerNodes
);
# The network spanning cidr addresses. The respective cidrv4 and cirdv6 are only
# included if they exist.
networkCidrs = filter (x: x != null) (attrValues networkAddresses);
# The cidrv4 and cidrv6 of the network spanned by all reserved addresses only.
# Used to determine automatically assigned addresses first.
spannedReservedNetwork = net.cidr.merge (
concatMap (n: (wgCfgOf n).server.reservedAddresses) participatingServerNodes
);
# Assigns an ipv4 address from spannedReservedNetwork.cidrv4
# to each participant that has not explicitly specified an ipv4 address.
assignedIpv4Addresses = assert assertMsg (spannedReservedNetwork.cidrv4 != null)
"Wireguard network '${wgName}': At least one participating node must reserve a cidrv4 address via `reservedAddresses` so that ipv4 addresses can be assigned automatically from that network.";
net.cidr.assignIps spannedReservedNetwork.cidrv4
# Don't assign any addresses that are explicitly configured on other hosts
(filter (x: net.cidr.contains x spannedReservedNetwork.cidrv4) (
filter net.ip.isv4 explicitlyUsedAddresses
))
participatingNodes;
# Assigns an ipv6 address from spannedReservedNetwork.cidrv6
# to each participant that has not explicitly specified an ipv6 address.
assignedIpv6Addresses = assert assertMsg (spannedReservedNetwork.cidrv6 != null)
"Wireguard network '${wgName}': At least one participating node must reserve a cidrv6 address via `reservedAddresses` so that ipv6 addresses can be assigned automatically from that network.";
net.cidr.assignIps spannedReservedNetwork.cidrv6
# Don't assign any addresses that are explicitly configured on other hosts
(filter (x: net.cidr.contains x spannedReservedNetwork.cidrv6) (
filter net.ip.isv6 explicitlyUsedAddresses
))
participatingNodes;
# Appends / replaces the correct cidr length to the argument,
# so that the resulting address is in the cidr.
toNetworkAddr = addr: let
relevantNetworkAddr =
if net.ip.isv6 addr
then networkAddresses.cidrv6
else networkAddresses.cidrv4;
in "${net.cidr.ip addr}/${toString (net.cidr.length relevantNetworkAddr)}";
in {
inherit
assignedIpv4Addresses
assignedIpv6Addresses
explicitlyUsedAddresses
networkAddresses
networkCidrs
nodePeers
participatingClientNodes
participatingNodes
participatingServerNodes
peerPresharedKeyFile
peerPresharedKeyPath
peerPresharedKeySecret
peerPrivateKeyFile
peerPrivateKeyPath
peerPrivateKeySecret
peerPublicKeyFile
peerPublicKeyPath
sortedPeers
spannedReservedNetwork
toNetworkAddr
usedAddresses
wgCfgOf
;
};
wireguard.createEvalCache = userInputs: wgNames: genAttrs wgNames (wireguard.evaluateNetwork userInputs);
wireguard.getNetwork = userInputs: wgName:
userInputs.self.wireguardEvalCache.${wgName}
or (warn ''
The calculated information for the wireguard network "${wgName}" is not cached!
This will siginificantly increase evaluation times. Please consider pre-evaluating
this information by exposing it in your flake:
wireguardEvalCache = lib.wireguard.createEvalCache inputs [
"${wgName}"
# all other networks
];
'' (wireguard.evaluateNetwork userInputs wgName));
};
}

View file

@ -10,7 +10,7 @@
./nginx.nix ./nginx.nix
./node.nix ./node.nix
./restic.nix ./restic.nix
./topology-wireguard.nix #./topology-wireguard.nix
./wireguard.nix ./wireguard.nix
]; ];

View file

@ -2,201 +2,187 @@
config, config,
inputs, inputs,
lib, lib,
globals,
... ...
}: let }:
inherit let
(lib) inherit (lib)
any any
concatAttrs attrNames
concatMap concatMapAttrs
concatStringsSep count
duplicates mkMerge
filter filterAttrs
flip flip
head mapAttrs'
listToAttrs
mapAttrsToList mapAttrsToList
mergeToplevelConfigs
mkIf mkIf
mkOption
nameValuePair nameValuePair
attrValues
net net
optional
optionalAttrs optionalAttrs
optionals
stringLength stringLength
types concatLists
; ;
cfg = config.wireguard; memberWG = filterAttrs (
nodeName = config.node.name; _: cfg: any (x: x == config.node.name) (attrNames cfg.hosts)
) globals.wireguard;
in
configForNetwork = wgName: wgCfg: let {
inherit assertions = concatLists (
(lib.wireguard.getNetwork inputs wgName) flip mapAttrsToList memberWG (
networkCidrs networkName: networkCfg:
participatingClientNodes let
participatingNodes assertionPrefix = "While evaluation the wireguard network ${networkName}:";
participatingServerNodes hostCfg = networkCfg.hosts.${config.node.name};
peerPresharedKeyPath
peerPresharedKeySecret
peerPrivateKeyPath
peerPrivateKeySecret
peerPublicKeyPath
toNetworkAddr
usedAddresses
wgCfgOf
;
isServer = wgCfg.server.host != null;
isClient = wgCfg.client.via != null;
filterSelf = filter (x: x != nodeName);
# All nodes that use our node as the via into the wireguard network
ourClientNodes = optionals isServer (
filter (n: (wgCfgOf n).client.via == nodeName) participatingClientNodes
);
# The list of peers for which we have to know the psk.
neededPeers =
if isServer
then
# Other servers in the same network
filterSelf participatingServerNodes
# Our clients
++ ourClientNodes
else [wgCfg.client.via];
# Figure out if there are duplicate addresses so we can make an assertion later.
duplicateAddrs = duplicates usedAddresses;
# Adds context information to the assertions for this network
assertionPrefix = "Wireguard network '${wgName}' on '${nodeName}'";
# Calculates the allowed ips for another server from our perspective.
# Usually we just want to allow other peers to route traffic
# for our "children" through us, additional to traffic to us of course.
# If a server exposes additional network access (global, lan, ...),
# these can be added aswell.
# TODO (do that)
serverAllowedIPs = serverNode: let
snCfg = wgCfgOf serverNode;
in in
map (net.cidr.make 128) ( [
# The server accepts traffic to it's own address {
snCfg.addresses assertion = networkCfg.cidrv4 != null || networkCfg.cidrv6 != null;
# plus traffic for any client that is connected via that server message = "${assertionPrefix}: At least one of cidrv4 or cidrv6 has to be set.";
++ concatMap (n: (wgCfgOf n).addresses) ( }
filter (n: (wgCfgOf n).client.via == serverNode) participatingClientNodes {
assertion = (count (x: x.server) (attrValues networkCfg.hosts)) == 1;
message = "${assertionPrefix}: You have to declare exactly 1 server node.";
}
{
assertion = (count (x: x.id == hostCfg.id) (attrValues networkCfg.hosts)) == 1;
message = "${assertionPrefix}: More than one host with id ${toString hostCfg.id}";
}
{
assertion = stringLength hostCfg.linkName < 16;
message = "${assertionPrefix}: The specified linkName '${hostCfg.linkName}' is too long (must be max 15 characters).";
}
]
) )
); );
in { networking.firewall.allowedUDPPorts = mkMerge (
assertions = [ flip mapAttrsToList memberWG (
_: networkCfg:
let
hostCfg = networkCfg.hosts.${config.node.name};
in
optional (hostCfg.server && networkCfg.openFirewall) networkCfg.port
)
);
networking.nftables.firewall.zones = mkMerge (
flip mapAttrsToList memberWG (
_: networkCfg:
let
hostCfg = networkCfg.hosts.${config.node.name};
peers = filterAttrs (name: _: name != config.node.name) networkCfg.hosts;
in
{ {
assertion = any (n: (wgCfgOf n).server.host != null) participatingNodes; # Parent zone for the whole network
message = "${assertionPrefix}: At least one node in a network must be a server."; "wg-${hostCfg.linkName}".interfaces = [ hostCfg.linkName ];
} }
{ // (flip mapAttrs' peers (
assertion = duplicateAddrs == []; name: cfg:
message = "${assertionPrefix}: Addresses used multiple times: ${concatStringsSep ", " duplicateAddrs}"; nameValuePair "wg-${hostCfg.linkName}-node-${name}" {
parent = "wg-${hostCfg.linkName}";
ipv4Addresses = optional (cfg.ipv4 != null) cfg.ipv4;
ipv6Addresses = optional (cfg.ipv6 != null) cfg.ipv6;
} }
{ ))
assertion = isServer != isClient; )
message = "${assertionPrefix}: A node must either be a server (define server.host) or a client (define client.via)."; );
} networking.nftables.firewall.rules = mkMerge (
{ flip mapAttrsToList memberWG (
assertion = isClient -> ((wgCfgOf wgCfg.client.via).server.host != null); _: networkCfg:
message = "${assertionPrefix}: The specified via node '${wgCfg.client.via}' must be a wireguard server."; let
}
{
assertion = stringLength wgCfg.linkName < 16;
message = "${assertionPrefix}: The specified linkName '${wgCfg.linkName}' is too long (must be max 15 characters).";
}
];
# Open the udp port for the wireguard endpoint in the firewall
networking.firewall.allowedUDPPorts = mkIf (isServer && wgCfg.server.openFirewall) [
wgCfg.server.port
];
# If requested, create firewall rules for the network / specific participants and open ports.
networking.nftables.firewall = let
inherit (config.networking.nftables.firewall) localZoneName; inherit (config.networking.nftables.firewall) localZoneName;
in { hostCfg = networkCfg.hosts.${config.node.name};
zones = peers = filterAttrs (name: _: name != config.node.name) networkCfg.hosts;
{
# Parent zone for the whole interface
"wg-${wgCfg.linkName}".interfaces = [wgCfg.linkName];
}
// listToAttrs (
flip map participatingNodes (
peer: let
peerCfg = wgCfgOf peer;
in in
# Subzone to specifically target the peer
nameValuePair "wg-${wgCfg.linkName}-node-${peer}" {
parent = "wg-${wgCfg.linkName}";
ipv4Addresses = [peerCfg.ipv4];
ipv6Addresses = [peerCfg.ipv6];
}
)
);
rules =
{ {
# Open ports for whole network "wg-${hostCfg.linkName}-to-${localZoneName}" = {
"wg-${wgCfg.linkName}-to-${localZoneName}" = { from = [ "wg-${hostCfg.linkName}" ];
from = ["wg-${wgCfg.linkName}"]; to = [ localZoneName ];
to = [localZoneName];
ignoreEmptyRule = true; ignoreEmptyRule = true;
inherit inherit (hostCfg.firewallRuleForAll)
(wgCfg.firewallRuleForAll)
allowedTCPPorts allowedTCPPorts
allowedUDPPorts allowedUDPPorts
; ;
}; };
} }
# Open ports for specific nodes network // (flip mapAttrs' peers (
// listToAttrs ( name: _:
flip map participatingNodes ( nameValuePair "wg-${hostCfg.linkName}-node-${name}-to-${localZoneName}" (
peer: mkIf (hostCfg.firewallRuleForNode ? ${name}) {
nameValuePair "wg-${wgCfg.linkName}-node-${peer}-to-${localZoneName}" ( from = [ "wg-${hostCfg.linkName}-node-${name}" ];
mkIf (wgCfg.firewallRuleForNode ? ${peer}) { to = [ localZoneName ];
from = ["wg-${wgCfg.linkName}-node-${peer}"];
to = [localZoneName];
ignoreEmptyRule = true; ignoreEmptyRule = true;
inherit inherit (hostCfg.firewallRuleForNode.${name})
(wgCfg.firewallRuleForNode.${peer})
allowedTCPPorts allowedTCPPorts
allowedUDPPorts allowedUDPPorts
; ;
} }
) )
))
) )
); );
age.secrets = flip concatMapAttrs memberWG (
networkName: networkCfg:
let
serverNode = filterAttrs (_: cfg: cfg.server) networkCfg.hosts;
connectedPeers = if hostCfg.server then peers else serverNode;
hostCfg = networkCfg.hosts.${config.node.name};
peers = filterAttrs (name: _: name != config.node.name) networkCfg.hosts;
sortedPeers =
peerA: peerB:
if peerA < peerB then
{
peer1 = peerA;
peer2 = peerB;
}
else
{
peer1 = peerB;
peer2 = peerA;
}; };
age.secrets = peerPrivateKeyFile = peerName: "/secrets/wireguard/${networkName}/keys/${peerName}.age";
concatAttrs ( peerPrivateKeyPath = peerName: inputs.self.outPath + peerPrivateKeyFile peerName;
map (other: { peerPrivateKeySecret = peerName: "wireguard-${networkName}-priv-${peerName}";
${peerPresharedKeySecret nodeName other} = { peerPresharedKeyFile =
rekeyFile = peerPresharedKeyPath nodeName other; peerA: peerB:
let
inherit (sortedPeers peerA peerB) peer1 peer2;
in
"/secrets/wireguard/${networkName}/psks/${peer1}+${peer2}.age";
peerPresharedKeyPath = peerA: peerB: inputs.self.outPath + peerPresharedKeyFile peerA peerB;
peerPresharedKeySecret =
peerA: peerB:
let
inherit (sortedPeers peerA peerB) peer1 peer2;
in
"wireguard-${networkName}-psks-${peer1}+${peer2}";
in
flip mapAttrs' connectedPeers (
name: _:
nameValuePair (peerPresharedKeySecret config.node.name name) {
rekeyFile = peerPresharedKeyPath config.node.name name;
owner = "systemd-network"; owner = "systemd-network";
generator.script = {pkgs, ...}: "${pkgs.wireguard-tools}/bin/wg genpsk"; generator.script = { pkgs, ... }: "${pkgs.wireguard-tools}/bin/wg genpsk";
}; }
})
neededPeers
) )
// { // {
${peerPrivateKeySecret nodeName} = { ${peerPrivateKeySecret config.node.name} = {
rekeyFile = peerPrivateKeyPath nodeName; rekeyFile = peerPrivateKeyPath config.node.name;
owner = "systemd-network"; owner = "systemd-network";
generator.script = { generator.script =
{
pkgs, pkgs,
file, file,
... ...
}: '' }:
''
priv=$(${pkgs.wireguard-tools}/bin/wg genkey) priv=$(${pkgs.wireguard-tools}/bin/wg genkey)
${pkgs.wireguard-tools}/bin/wg pubkey <<< "$priv" > ${ ${pkgs.wireguard-tools}/bin/wg pubkey <<< "$priv" > ${
lib.escapeShellArg (lib.removeSuffix ".age" file + ".pub") lib.escapeShellArg (lib.removeSuffix ".age" file + ".pub")
@ -204,254 +190,90 @@
echo "$priv" echo "$priv"
''; '';
}; };
}
);
systemd.network.netdevs = flip mapAttrs' memberWG (
networkName: networkCfg:
let
serverNode = filterAttrs (_: cfg: cfg.server) networkCfg.hosts;
hostCfg = networkCfg.hosts.${config.node.name};
peers = filterAttrs (name: _: name != config.node.name) networkCfg.hosts;
sortedPeers =
peerA: peerB:
if peerA < peerB then
{
peer1 = peerA;
peer2 = peerB;
}
else
{
peer1 = peerB;
peer2 = peerA;
}; };
systemd.network.netdevs."${wgCfg.unitConfName}" = { peerPublicKeyFile = peerName: "/secrets/wireguard/${networkName}/keys/${peerName}.pub";
peerPublicKeyPath = peerName: inputs.self.outPath + peerPublicKeyFile peerName;
peerPrivateKeySecret = peerName: "wireguard-${networkName}-priv-${peerName}";
peerPresharedKeySecret =
peerA: peerB:
let
inherit (sortedPeers peerA peerB) peer1 peer2;
in
"wireguard-${networkName}-psks-${peer1}+${peer2}";
in
nameValuePair "${hostCfg.unitConfName}" {
netdevConfig = { netdevConfig = {
Kind = "wireguard"; Kind = "wireguard";
Name = wgCfg.linkName; Name = hostCfg.linkName;
Description = "Wireguard network ${wgName}"; Description = "Wireguard network ${networkName}";
}; };
wireguardConfig = wireguardConfig =
{ {
PrivateKeyFile = config.age.secrets.${peerPrivateKeySecret nodeName}.path; PrivateKeyFile = config.age.secrets.${peerPrivateKeySecret config.node.name}.path;
} }
// optionalAttrs isServer { // optionalAttrs hostCfg.server {
ListenPort = wgCfg.server.port; ListenPort = networkCfg.port;
}; };
wireguardPeers = wireguardPeers =
if isServer if hostCfg.server then
then
# Always include all other server nodes.
map (
serverNode: let
snCfg = wgCfgOf serverNode;
in {
PublicKey = builtins.readFile (peerPublicKeyPath serverNode);
PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName serverNode}.path;
AllowedIPs = serverAllowedIPs serverNode;
Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}";
}
) (filterSelf participatingServerNodes)
# All client nodes that have their via set to us. # All client nodes that have their via set to us.
++ map ( mapAttrsToList (clientName: clientCfg: {
clientNode: let PublicKey = builtins.readFile (peerPublicKeyPath clientName);
clientCfg = wgCfgOf clientNode; PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret config.node.name clientName}.path;
in { AllowedIPs =
PublicKey = builtins.readFile (peerPublicKeyPath clientNode); (optional (clientCfg.ipv4 != null) (net.cidr.make 32 clientCfg.ipv4))
PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName clientNode}.path; ++ (optional (clientCfg.ipv6 != null) (net.cidr.make 128 clientCfg.ipv6));
AllowedIPs = map (net.cidr.make 128) clientCfg.addresses; }) peers
}
)
ourClientNodes
else else
# We are a client node, so only include our via server. # We are a client node, so only include our via server.
[ mapAttrsToList (
( serverName: _:
let
snCfg = wgCfgOf wgCfg.client.via;
in
{ {
PublicKey = builtins.readFile (peerPublicKeyPath wgCfg.client.via); PublicKey = builtins.readFile (peerPublicKeyPath serverName);
PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName wgCfg.client.via}.path; PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret config.node.name serverName}.path;
Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}"; Endpoint = "${networkCfg.host}:${toString networkCfg.port}";
# Access to the whole network is routed through our entry node. # Access to the whole network is routed through our entry node.
AllowedIPs = networkCidrs; AllowedIPs =
(optional (networkCfg.cidrv4 != null) networkCfg.cidrv4)
++ (optional (networkCfg.cidrv6 != null) networkCfg.cidrv6);
} }
// optionalAttrs wgCfg.client.keepalive { // optionalAttrs hostCfg.keepalive {
PersistentKeepalive = 25; PersistentKeepalive = 25;
} }
) ) serverNode;
];
};
systemd.network.networks.${wgCfg.unitConfName} = {
matchConfig.Name = wgCfg.linkName;
address = map toNetworkAddr wgCfg.addresses;
};
};
in {
options.wireguard = mkOption {
default = {};
description = "Configures wireguard networks via systemd-networkd.";
type = types.lazyAttrsOf (
types.submodule (
{
config,
name,
options,
...
}: {
options = {
server = {
host = mkOption {
default = null;
type = types.nullOr types.str;
description = "The hostname or ip address which other peers can use to reach this host. No server functionality will be activated if set to null.";
};
port = mkOption {
default = 51820;
type = types.port;
description = "The port to listen on.";
};
openFirewall = mkOption {
default = false;
type = types.bool;
description = "Whether to open the firewall for the specified {option}`port`.";
};
reservedAddresses = mkOption {
type = types.listOf types.net.cidr;
default = [];
example = [
"10.0.0.0/24"
"fd00:cafe::/64"
];
description = ''
Allows defining extra CIDR network ranges that shall be reserved for this network.
Reservation means that those address spaces will be guaranteed to be included in
the spanned network, but no rules will be enforced as to who in the network may use them.
By default, this module will try to allocate the smallest address space that includes
all network peers.
'';
};
};
client = {
via = mkOption {
default = null;
type = types.nullOr types.str;
description = ''
The server node via which to connect to the network.
No client functionality will be activated if set to null.
'';
};
keepalive = mkOption {
default = true;
type = types.bool;
description = "Whether to keep this connection alive using PersistentKeepalive. Set to false only for networks where client and server IPs are stable.";
};
};
priority = mkOption {
default = 40;
type = types.int;
description = "The order priority used when creating systemd netdev and network files.";
};
linkName = mkOption {
default = name;
type = types.str;
description = "The name for the created network interface.";
};
unitConfName = mkOption {
default = "${toString config.priority}-${config.linkName}";
readOnly = true;
type = types.str;
description = ''
The name used for unit configuration files. This is a read-only option.
Access this if you want to add additional settings to the generated systemd units.
'';
};
ipv4 = mkOption {
type = types.lazyOf types.net.ipv4;
default = types.lazyValue (lib.wireguard.getNetwork inputs name).assignedIpv4Addresses.${nodeName};
description = ''
The ipv4 address for this machine. If you do not set this explicitly,
a semi-stable ipv4 address will be derived automatically based on the
hostname of this machine. At least one participating server must reserve
a big-enough space of addresses by setting `reservedAddresses`.
See `net.cidr.assignIps` for more information on the algorithm.
'';
};
ipv6 = mkOption {
type = types.lazyOf types.net.ipv6;
default = types.lazyValue (lib.wireguard.getNetwork inputs name).assignedIpv6Addresses.${nodeName};
description = ''
The ipv6 address for this machine. If you do not set this explicitly,
a semi-stable ipv6 address will be derived automatically based on the
hostname of this machine. At least one participating server must reserve
a big-enough space of addresses by setting `reservedAddresses`.
See `net.cidr.assignIps` for more information on the algorithm.
'';
};
addresses = mkOption {
type = types.listOf (types.lazyOf types.net.ip);
default = [
(head options.ipv4.definitions)
(head options.ipv6.definitions)
];
description = ''
The ip addresses (v4 and/or v6) to use for this machine.
The actual network cidr will automatically be derived from all network participants.
By default this will just include {option}`ipv4` and {option}`ipv6` as configured.
'';
};
firewallRuleForAll = mkOption {
default = {};
description = ''
Allows you to set specific firewall rules for traffic originating from any participant in this
wireguard network. A corresponding rule `wg-<network-name>-to-<local-zone-name>` will be created to easily expose
services to the network.
'';
type = types.submodule {
options = {
allowedTCPPorts = mkOption {
type = types.listOf types.port;
default = [];
description = "Convenience option to open specific TCP ports for traffic from the network.";
};
allowedUDPPorts = mkOption {
type = types.listOf types.port;
default = [];
description = "Convenience option to open specific UDP ports for traffic from the network.";
};
};
};
};
firewallRuleForNode = mkOption {
default = {};
description = ''
Allows you to set specific firewall rules just for traffic originating from another network node.
A corresponding rule `wg-<network-name>-node-<node-name>-to-<local-zone-name>` will be created to easily expose
services to that node.
'';
type = types.attrsOf (
types.submodule {
options = {
allowedTCPPorts = mkOption {
type = types.listOf types.port;
default = [];
description = "Convenience option to open specific TCP ports for traffic from another node.";
};
allowedUDPPorts = mkOption {
type = types.listOf types.port;
default = [];
description = "Convenience option to open specific UDP ports for traffic from another node.";
};
};
} }
); );
}; systemd.network.networks = flip mapAttrs' memberWG (
}; _: networkCfg:
let
hostCfg = networkCfg.hosts.${config.node.name};
in
nameValuePair hostCfg.unitConfName {
matchConfig.Name = hostCfg.linkName;
address =
(optional (networkCfg.cidrv4 != null) (net.cidr.hostCidr hostCfg.id networkCfg.cidrv4))
++ (optional (networkCfg.cidrv6 != null) (net.cidr.hostCidr hostCfg.id networkCfg.cidrv6));
} }
)
);
};
config = mkIf (cfg != {}) (
mergeToplevelConfigs ["assertions" "age" "networking" "systemd"] (
mapAttrsToList configForNetwork cfg
)
); );
} }

View file

@ -0,0 +1,171 @@
{ lib, ... }:
let
inherit (lib)
mkOption
types
importJSON
;
in
{
options.globals = mkOption {
type = types.submodule {
options = {
wireguard = mkOption {
default = { };
type = types.attrsOf (
types.submodule (
{ name, config, ... }:
let
wgConf = config;
wgName = name;
in
{
options = {
host = mkOption {
type = types.str;
description = "The host name or IP addresse for reaching the server node.";
};
idFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "A json file containing a mapping from hostname to id.";
};
cidrv4 = mkOption {
type = types.nullOr types.net.cidrv4;
default = null;
description = "The server host of this wireguard";
};
cidrv6 = mkOption {
type = types.nullOr types.net.cidrv6;
default = null;
description = "The server host of this wireguard";
};
port = mkOption {
default = 51820;
type = types.port;
description = "The port the server listens on";
};
openFirewall = mkOption {
default = false;
type = types.bool;
description = "Whether to open the servers firewall for the specified {option}`port`. Has no effect for client nodes.";
};
hosts = mkOption {
default = { };
description = "Attrset of hostName to host specific config";
type = types.attrsOf (
types.submodule (
{ config, name, ... }:
{
config.id =
let
inherit (wgConf) idFile;
in
if (idFile == null) then
null
else
(
let
conf = importJSON idFile;
in
conf.${name} or null
);
options = {
server = mkOption {
default = false;
type = types.bool;
description = "Whether this host acts as the server and relay for the network. Has to be set for exactly 1 host.";
};
linkName = mkOption {
default = wgName;
type = types.str;
description = "The name of the created network interface. Has to be less than 15 characters.";
};
unitConfName = mkOption {
default = "40-${config.linkName}";
type = types.str;
description = "The name of the generated systemd unit configuration files.";
readOnly = true;
};
id = mkOption {
type = types.int;
description = "The unique id of this host. Used to derive its IP addresses. Has to be smaller than the size of the Subnet.";
};
ipv4 = mkOption {
type = types.nullOr types.net.ipv4;
default = if (wgConf.cidrv4 == null) then null else lib.net.cidr.host config.id wgConf.cidrv4;
readOnly = true;
description = "The IPv4 of this host. Automatically computed from the {option}`id`";
};
ipv6 = mkOption {
type = types.nullOr types.net.ipv6;
default = if (wgConf.cidrv4 == null) then null else lib.net.cidr.host config.id wgConf.cidrv6;
readOnly = true;
description = "The IPv4 of this host. Automatically computed from the {option}`id`";
};
keepalive = mkOption {
default = true;
type = types.bool;
description = "Whether to keep the connection alive using PersistentKeepalive. Has no effect for server nodes.";
};
firewallRuleForAll = mkOption {
default = { };
description = ''
Allows you to set specific firewall rules for traffic originating from any participant in this
wireguard network. A corresponding rule `wg-<network-name>-to-<local-zone-name>` will be created to easily expose
services to the network.
'';
type = types.submodule {
options = {
allowedTCPPorts = mkOption {
type = types.listOf types.port;
default = [ ];
description = "Convenience option to open specific TCP ports for traffic from the network.";
};
allowedUDPPorts = mkOption {
type = types.listOf types.port;
default = [ ];
description = "Convenience option to open specific UDP ports for traffic from the network.";
};
};
};
};
firewallRuleForNode = mkOption {
default = { };
description = ''
Allows you to set specific firewall rules just for traffic originating from another network node.
A corresponding rule `wg-<network-name>-node-<node-name>-to-<local-zone-name>` will be created to easily expose
services to that node.
'';
type = types.attrsOf (
types.submodule {
options = {
allowedTCPPorts = mkOption {
type = types.listOf types.port;
default = [ ];
description = "Convenience option to open specific TCP ports for traffic from another node.";
};
allowedUDPPorts = mkOption {
type = types.listOf types.port;
default = [ ];
description = "Convenience option to open specific UDP ports for traffic from another node.";
};
};
}
);
};
};
}
)
);
};
};
}
)
);
};
};
};
};
}

View file

@ -11,12 +11,8 @@ prev.lib.composeManyExtensions (
./lib/disko.nix ./lib/disko.nix
# Requires misc # Requires misc
./lib/net.nix ./lib/net.nix
# Requires misc, types
./lib/wireguard.nix
]) ])
++ [ ++ [
(import ./pkgs) (import ./pkgs)
] ]
) ) final prev
final
prev