feat: move wireguard module to nixos-extra-modules

This commit is contained in:
oddlama 2024-03-14 20:55:08 +01:00
parent 621d725af3
commit a4844807e6
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
27 changed files with 73 additions and 783 deletions

6
flake.lock generated
View file

@ -950,11 +950,11 @@
"pre-commit-hooks": "pre-commit-hooks_3"
},
"locked": {
"lastModified": 1709384560,
"narHash": "sha256-VZpbetW5npjZ1FWcFII81tcDBH03irTboyMVOWzdfF8=",
"lastModified": 1710445839,
"narHash": "sha256-omFKeLtg+OwM8lOGqFak87CGk5mYR/2nfoKmdRs/ELU=",
"owner": "oddlama",
"repo": "nixos-extra-modules",
"rev": "34ba92f0576a3998133310f070381563448e2b1a",
"rev": "f4d989155419461acd8378e0cfbfa8d5db77f21a",
"type": "github"
},
"original": {

View file

@ -63,7 +63,7 @@
#};
## Connect safely via wireguard to skip authentication
#networking.hosts.${nodes.sentinel.config.meta.wireguard.proxy-sentinel.ipv4} = [nodes.sentinel.config.networking.providedDomains.influxdb];
#networking.hosts.${nodes.sentinel.config.wireguard.proxy-sentinel.ipv4} = [nodes.sentinel.config.networking.providedDomains.influxdb];
#meta.telegraf = {
# enable = true;
# influxdb2 = {

View file

@ -15,6 +15,7 @@
boot.mode = "bios";
users.groups.acme.members = ["nginx"];
wireguard.proxy-sentinel.firewallRuleForAll.allowedTCPPorts = [80 443];
services.nginx.enable = true;
services.nginx.recommendedSetup = true;
@ -24,7 +25,7 @@
};
# Connect safely via wireguard to skip authentication
networking.hosts.${config.meta.wireguard.proxy-sentinel.ipv4} = [config.networking.providedDomains.influxdb];
networking.hosts.${config.wireguard.proxy-sentinel.ipv4} = [config.networking.providedDomains.influxdb];
meta.telegraf = {
enable = true;
scrapeSensors = false;

View file

@ -34,23 +34,12 @@
};
};
networking.nftables.firewall = {
zones = {
untrusted.interfaces = ["wan"];
proxy-sentinel.interfaces = ["proxy-sentinel"];
};
# Allow accessing nginx through the proxy
rules.proxy-sentinel-to-local = {
from = ["proxy-sentinel"];
to = ["local"];
allowedTCPPorts = [80 443];
};
};
networking.nftables.firewall.zones.untrusted.interfaces = ["wan"];
meta.wireguard.proxy-sentinel.server = {
wireguard.proxy-sentinel.server = {
host = config.networking.fqdn;
port = 51443;
reservedAddresses = ["10.43.0.0/24" "fd00:43::/120"];
openFirewallRules = ["untrusted-to-local"];
openFirewall = true;
};
}

View file

@ -29,7 +29,7 @@
};
# Connect safely via wireguard to skip authentication
networking.hosts.${nodes.sentinel.config.meta.wireguard.proxy-sentinel.ipv4} = [nodes.sentinel.config.networking.providedDomains.influxdb];
networking.hosts.${nodes.sentinel.config.wireguard.proxy-sentinel.ipv4} = [nodes.sentinel.config.networking.providedDomains.influxdb];
meta.telegraf = {
enable = true;
influxdb2 = {

View file

@ -6,14 +6,13 @@
}: let
sentinelCfg = nodes.sentinel.config;
in {
meta.wireguard-proxy.sentinel = {};
meta.promtail = {
enable = true;
proxy = "sentinel";
};
# Connect safely via wireguard to skip http authentication
networking.hosts.${sentinelCfg.meta.wireguard.proxy-sentinel.ipv4} = [sentinelCfg.networking.providedDomains.influxdb];
networking.hosts.${sentinelCfg.wireguard.proxy-sentinel.ipv4} = [sentinelCfg.networking.providedDomains.influxdb];
meta.telegraf = lib.mkIf (!config.boot.isContainer) {
enable = true;
scrapeSensors = false;

View file

@ -6,7 +6,10 @@
sentinelCfg = nodes.sentinel.config;
grafanaDomain = "grafana.${config.repo.secrets.global.domains.me}";
in {
meta.wireguard-proxy.sentinel.allowedTCPPorts = [config.services.grafana.settings.server.http_port];
wireguard.proxy-sentinel = {
client.via = "sentinel";
firewallRuleForNode.sentinel.allowedTCPPorts = [config.services.grafana.settings.server.http_port];
};
age.secrets.grafana-secret-key = {
rekeyFile = config.node.secretsDir + "/grafana-secret-key.age";
@ -58,7 +61,7 @@ in {
services.nginx = {
upstreams.grafana = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:${toString config.services.grafana.settings.server.http_port}" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:${toString config.services.grafana.settings.server.http_port}" = {};
extraConfig = ''
zone grafana 64k;
keepalive 2;

View file

@ -165,7 +165,10 @@ in {
'';
};
meta.wireguard-proxy.sentinel.allowedTCPPorts = [2283];
wireguard.proxy-sentinel = {
client.via = "sentinel";
firewallRuleForNode.sentinel.allowedTCPPorts = [2283];
};
networking.nftables.chains.forward.into-immich-container = {
after = ["conntrack"];
rules = [
@ -179,7 +182,7 @@ in {
services.nginx = {
upstreams.immich = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:2283" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:2283" = {};
extraConfig = ''
zone immich 64k;
keepalive 2;

View file

@ -9,14 +9,17 @@
influxdbDomain = "influxdb.${config.repo.secrets.global.domains.me}";
influxdbPort = 8086;
in {
meta.wireguard-proxy.sentinel.allowedTCPPorts = [influxdbPort];
wireguard.proxy-sentinel = {
client.via = "sentinel";
firewallRuleForNode.sentinel.allowedTCPPorts = [influxdbPort];
};
nodes.sentinel = {
networking.providedDomains.influxdb = influxdbDomain;
services.nginx = {
upstreams.influxdb = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:${toString influxdbPort}" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:${toString influxdbPort}" = {};
extraConfig = ''
zone influxdb 64k;
keepalive 2;
@ -25,7 +28,7 @@ in {
virtualHosts.${influxdbDomain} = let
accessRules = ''
satisfy any;
${lib.concatMapStrings (ip: "allow ${ip};\n") sentinelCfg.meta.wireguard.proxy-sentinel.server.reservedAddresses}
${lib.concatMapStrings (ip: "allow ${ip};\n") sentinelCfg.wireguard.proxy-sentinel.server.reservedAddresses}
deny all;
'';
in {

View file

@ -6,7 +6,10 @@
sentinelCfg = nodes.sentinel.config;
lokiDomain = "loki.${config.repo.secrets.global.domains.me}";
in {
meta.wireguard-proxy.sentinel.allowedTCPPorts = [config.services.loki.configuration.server.http_listen_port];
wireguard.proxy-sentinel = {
client.via = "sentinel";
firewallRuleForNode.sentinel.allowedTCPPorts = [config.services.loki.configuration.server.http_listen_port];
};
nodes.sentinel = {
networking.providedDomains.loki = lokiDomain;
@ -19,7 +22,7 @@ in {
services.nginx = {
upstreams.loki = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:${toString config.services.loki.configuration.server.http_listen_port}" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:${toString config.services.loki.configuration.server.http_listen_port}" = {};
extraConfig = ''
zone loki 64k;
keepalive 2;

View file

@ -17,7 +17,7 @@ in {
services.nginx = {
upstreams.paperless = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:${toString config.services.paperless.port}" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:${toString config.services.paperless.port}" = {};
extraConfig = ''
zone paperless 64k;
keepalive 2;
@ -38,9 +38,10 @@ in {
};
};
meta.wireguard-proxy.sentinel.allowedTCPPorts = [
config.services.paperless.port
];
wireguard.proxy-sentinel = {
client.via = "sentinel";
firewallRuleForNode.sentinel.allowedTCPPorts = [config.services.paperless.port];
};
age.secrets.paperless-admin-password = {
generator.script = "alnum";
@ -74,7 +75,7 @@ in {
PAPERLESS_URL = "https://${paperlessDomain}";
PAPERLESS_ALLOWED_HOSTS = paperlessDomain;
PAPERLESS_CORS_ALLOWED_HOSTS = "https://${paperlessDomain}";
PAPERLESS_TRUSTED_PROXIES = sentinelCfg.meta.wireguard.proxy-sentinel.ipv4;
PAPERLESS_TRUSTED_PROXIES = sentinelCfg.wireguard.proxy-sentinel.ipv4;
# Authentication via kanidm
PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect";

View file

@ -64,5 +64,5 @@
};
# Allow accessing influx
meta.wireguard.proxy-sentinel.client.via = "sentinel";
wireguard.proxy-sentinel.client.via = "sentinel";
}

View file

@ -30,7 +30,7 @@
};
# Connect safely via wireguard to skip authentication
networking.hosts.${nodes.sentinel.config.meta.wireguard.proxy-sentinel.ipv4} = [nodes.sentinel.config.networking.providedDomains.influxdb];
networking.hosts.${nodes.sentinel.config.wireguard.proxy-sentinel.ipv4} = [nodes.sentinel.config.networking.providedDomains.influxdb];
meta.telegraf = {
enable = true;
influxdb2 = {

View file

@ -7,14 +7,17 @@
}: let
adguardhomeDomain = "adguardhome.${config.repo.secrets.global.domains.me}";
in {
meta.wireguard-proxy.sentinel.allowedTCPPorts = [config.services.adguardhome.settings.bind_port];
wireguard.proxy-sentinel = {
client.via = "sentinel";
firewallRuleForNode.sentinel.allowedTCPPorts = [config.services.adguardhome.settings.bind_port];
};
nodes.sentinel = {
networking.providedDomains.adguard = adguardhomeDomain;
services.nginx = {
upstreams.adguardhome = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:${toString config.services.adguardhome.settings.bind_port}" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:${toString config.services.adguardhome.settings.bind_port}" = {};
extraConfig = ''
zone adguardhome 64k;
keepalive 2;

View file

@ -6,14 +6,13 @@
}: let
sentinelCfg = nodes.sentinel.config;
in {
meta.wireguard-proxy.sentinel = {};
meta.promtail = {
enable = true;
proxy = "sentinel";
};
# Connect safely via wireguard to skip http authentication
networking.hosts.${sentinelCfg.meta.wireguard.proxy-sentinel.ipv4} = [sentinelCfg.networking.providedDomains.influxdb];
networking.hosts.${sentinelCfg.wireguard.proxy-sentinel.ipv4} = [sentinelCfg.networking.providedDomains.influxdb];
meta.telegraf = lib.mkIf (!config.boot.isContainer) {
enable = true;
scrapeSensors = false;

View file

@ -8,9 +8,10 @@
sentinelCfg = nodes.sentinel.config;
forgejoDomain = "git.${config.repo.secrets.global.domains.me}";
in {
meta.wireguard-proxy.sentinel.allowedTCPPorts = [
config.services.forgejo.settings.server.HTTP_PORT
];
wireguard.proxy-sentinel = {
client.via = "sentinel";
firewallRuleForNode.sentinel.allowedTCPPorts = [config.services.forgejo.settings.server.HTTP_PORT];
};
age.secrets.forgejo-mailer-password = {
rekeyFile = config.node.secretsDir + "/forgejo-mailer-password.age";
@ -37,22 +38,22 @@ in {
postrouting.to-forgejo = {
after = ["hook"];
rules = [
"iifname wan ip daddr ${config.meta.wireguard.proxy-sentinel.ipv4} tcp dport 22 masquerade random"
"iifname wan ip6 daddr ${config.meta.wireguard.proxy-sentinel.ipv6} tcp dport 22 masquerade random"
"iifname wan ip daddr ${config.wireguard.proxy-sentinel.ipv4} tcp dport 22 masquerade random"
"iifname wan ip6 daddr ${config.wireguard.proxy-sentinel.ipv6} tcp dport 22 masquerade random"
];
};
prerouting.to-forgejo = {
after = ["hook"];
rules = [
"iifname wan tcp dport 9922 dnat ip to ${config.meta.wireguard.proxy-sentinel.ipv4}:22"
"iifname wan tcp dport 9922 dnat ip6 to ${config.meta.wireguard.proxy-sentinel.ipv6}:22"
"iifname wan tcp dport 9922 dnat ip to ${config.wireguard.proxy-sentinel.ipv4}:22"
"iifname wan tcp dport 9922 dnat ip6 to ${config.wireguard.proxy-sentinel.ipv6}:22"
];
};
};
services.nginx = {
upstreams.forgejo = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:${toString config.services.forgejo.settings.server.HTTP_PORT}" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:${toString config.services.forgejo.settings.server.HTTP_PORT}" = {};
extraConfig = ''
zone forgejo 64k;
keepalive 2;

View file

@ -14,7 +14,10 @@
group = "kanidm";
};
in {
meta.wireguard-proxy.sentinel.allowedTCPPorts = [kanidmPort];
wireguard.proxy-sentinel = {
client.via = "sentinel";
firewallRuleForNode.sentinel.allowedTCPPorts = [kanidmPort];
};
age.secrets."kanidm-self-signed.crt" = {
rekeyFile = config.node.secretsDir + "/kanidm-self-signed.crt.age";
@ -42,7 +45,7 @@ in {
services.nginx = {
upstreams.kanidm = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:${toString kanidmPort}" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:${toString kanidmPort}" = {};
extraConfig = ''
zone kanidm 64k;
keepalive 2;

View file

@ -1,16 +1,17 @@
{config, ...}: let
radicaleDomain = "radicale.${config.repo.secrets.global.domains.personal}";
in {
meta.wireguard-proxy.sentinel.allowedTCPPorts = [
8000
];
wireguard.proxy-sentinel = {
client.via = "sentinel";
firewallRuleForNode.sentinel.allowedTCPPorts = [8000];
};
nodes.sentinel = {
networking.providedDomains.radicale = radicaleDomain;
services.nginx = {
upstreams.radicale = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:8000" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:8000" = {};
extraConfig = ''
zone radicale 64k;
keepalive 2;

View file

@ -5,9 +5,10 @@
}: let
vaultwardenDomain = "pw.${config.repo.secrets.global.domains.personal}";
in {
meta.wireguard-proxy.sentinel.allowedTCPPorts = [
config.services.vaultwarden.config.rocketPort
];
wireguard.proxy-sentinel = {
client.via = "sentinel";
firewallRuleForNode.sentinel.allowedTCPPorts = [config.services.vaultwarden.config.rocketPort];
};
age.secrets.vaultwarden-env = {
rekeyFile = config.node.secretsDir + "/vaultwarden-env.age";
@ -29,7 +30,7 @@ in {
services.nginx = {
upstreams.vaultwarden = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:${toString config.services.vaultwarden.config.rocketPort}" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:${toString config.services.vaultwarden.config.rocketPort}" = {};
extraConfig = ''
zone vaultwarden 64k;
keepalive 2;

View file

@ -114,5 +114,5 @@ in {
};
# Allow accessing influx
meta.wireguard.proxy-sentinel.client.via = "sentinel";
wireguard.proxy-sentinel.client.via = "sentinel";
}

View file

@ -37,14 +37,13 @@ in {
};
};
meta.wireguard-proxy.sentinel = {};
meta.promtail = {
enable = true;
proxy = "sentinel";
};
# Connect safely via wireguard to skip http authentication
networking.hosts.${sentinelCfg.meta.wireguard.proxy-sentinel.ipv4} = [sentinelCfg.networking.providedDomains.influxdb];
networking.hosts.${sentinelCfg.wireguard.proxy-sentinel.ipv4} = [sentinelCfg.networking.providedDomains.influxdb];
meta.telegraf = {
enable = true;
influxdb2 = {

View file

@ -7,8 +7,6 @@
sentinelCfg = nodes.sentinel.config;
homeDomain = "home.${sentinelCfg.repo.secrets.global.domains.personal}";
in {
meta.wireguard-proxy.sentinel.allowedTCPPorts = [80];
environment.persistence."/persist".directories = [
{
directory = config.services.home-assistant.configDir;
@ -145,7 +143,7 @@ in {
nodes.sentinel = {
services.nginx = {
upstreams."zackbiene" = {
servers."${config.meta.wireguard.proxy-sentinel.ipv4}:80" = {};
servers."${config.wireguard.proxy-sentinel.ipv4}:80" = {};
extraConfig = ''
zone zackbiene 64k;
keepalive 2;

View file

@ -1,4 +1,3 @@
inputs: [
(import ./secrets.nix inputs)
(import ./wireguard.nix inputs)
]

View file

@ -1,225 +0,0 @@
inputs: final: prev: let
inherit
(inputs.nixpkgs.lib)
assertMsg
attrNames
attrValues
concatLists
concatMap
concatStringsSep
escapeShellArg
filter
flatten
flip
genAttrs
mapAttrs'
nameValuePair
partition
removeSuffix
;
inherit
(final.lib)
net
concatAttrs
types
;
inherit
(final.lib.secrets)
rageDecryptArgs
;
inherit (inputs.self) nodes;
in {
lib =
prev.lib
// {
wireguard = wgName: let
# Returns the given node's wireguard configuration of this network
wgCfgOf = node: nodes.${node}.config.meta.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: inputs.self.outPath + peerPublicKeyFile peerName;
peerPrivateKeyFile = peerName: "/secrets/wireguard/${wgName}/keys/${peerName}.age";
peerPrivateKeyPath = peerName: inputs.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: inputs.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.meta.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);
externalPeerName = p: "external-${p}";
# Only peers that are defined as externalPeers on the given node.
# Prepends "external-" to their name.
externalPeersForNode = node:
mapAttrs' (p: nameValuePair (externalPeerName p)) (wgCfgOf node).server.externalPeers;
# All peers that are defined as externalPeers on any node.
# Prepends "external-" to their name.
allExternalPeers = concatAttrs (map externalPeersForNode participatingNodes);
# All peers that are part of this network
allPeers = nodePeers // allExternalPeers;
# Concatenation of all external peer names names without any transformations.
externalPeerNamesRaw = concatMap (n: attrNames (wgCfgOf n).server.externalPeers) participatingNodes;
# A list of all occurring addresses.
usedAddresses =
concatMap (n: (wgCfgOf n).addresses) participatingNodes
++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) 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.meta.wireguard.type.functor.wrapped.getSubOptions (wgCfgOf n)).addresses.definitions))
++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) participatingNodes);
# 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 ipv4 address from spannedReservedNetwork.cidrv4
# to each participant that has not explicitly specified an ipv4 address.
assignedIpv6Addresses = assert assertMsg
(spannedReservedNetwork.cidrv6 != null)
"Wireguard network '${wgName}': At least one participating node must reserve a cidrv6 address via `reservedAddresses` so that ipv4 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)}";
# Creates a script that when executed outputs a wg-quick compatible configuration
# file for use with external peers. This is a script so we can access secrets without
# storing them in the nix-store.
wgQuickConfigScript = system: serverNode: extPeer: let
pkgs = inputs.self.pkgs.${system};
snCfg = wgCfgOf serverNode;
peerName = externalPeerName extPeer;
addresses = map toNetworkAddr snCfg.server.externalPeers.${extPeer};
in
pkgs.writeShellScript "create-wg-conf-${wgName}-${serverNode}-${extPeer}" ''
privKey=$(${pkgs.rage}/bin/rage -d ${rageDecryptArgs} ${escapeShellArg (peerPrivateKeyPath peerName)}) \
|| { echo "error: Failed to decrypt!" >&2; exit 1; }
serverPsk=$(${pkgs.rage}/bin/rage -d ${rageDecryptArgs} ${escapeShellArg (peerPresharedKeyPath serverNode peerName)}) \
|| { echo "error: Failed to decrypt!" >&2; exit 1; }
cat <<EOF
[Interface]
Address = ${concatStringsSep ", " addresses}
PrivateKey = $privKey
[Peer]
PublicKey = ${removeSuffix "\n" (builtins.readFile (peerPublicKeyPath serverNode))}
PresharedKey = $serverPsk
AllowedIPs = ${concatStringsSep ", " networkCidrs}
Endpoint = ${snCfg.server.host}:${toString snCfg.server.port}
PersistentKeepalive = 25
EOF
'';
in {
inherit
allExternalPeers
allPeers
assignedIpv4Addresses
assignedIpv6Addresses
explicitlyUsedAddresses
externalPeerName
externalPeerNamesRaw
externalPeersForNode
networkAddresses
networkCidrs
nodePeers
participatingClientNodes
participatingNodes
participatingServerNodes
peerPresharedKeyFile
peerPresharedKeyPath
peerPresharedKeySecret
peerPrivateKeyFile
peerPrivateKeyPath
peerPrivateKeySecret
peerPublicKeyFile
peerPublicKeyPath
sortedPeers
spannedReservedNetwork
toNetworkAddr
usedAddresses
wgCfgOf
wgQuickConfigScript
;
};
};
}

View file

@ -37,8 +37,6 @@
./provided-domains.nix
./secrets.nix
./telegraf.nix
./wireguard-proxy.nix
./wireguard.nix
../topology/module.nix
];

View file

@ -1,78 +0,0 @@
{
config,
lib,
nodes,
...
}: let
inherit
(lib)
attrNames
flip
mkIf
mkMerge
mkOption
types
;
cfg = config.meta.wireguard-proxy;
in {
options.meta.wireguard-proxy = mkOption {
default = {};
description = ''
Each entry here will setup a wireguard network that connects via the
given node and adds appropriate firewall zones. There will be a zone for
the interface and one for the proxy server specifically. A corresponding
rule `''${name}-to-local` will be created to easily expose services to the proxy.
'';
type = types.attrsOf (types.submodule ({name, ...}: {
options = {
nicName = mkOption {
type = types.str;
default = "proxy-${name}";
description = "The name for the created wireguard network and its interface";
};
allowedTCPPorts = mkOption {
type = types.listOf types.int;
default = [];
description = "Convenience option to allow incoming TCP connections from the proxy server (just the server, not the entire network).";
};
allowedUDPPorts = mkOption {
type = types.listOf types.int;
default = [];
description = "Convenience option to allow incoming UDP connections from the proxy server (just the server, not the entire network).";
};
};
}));
};
config = mkIf (cfg != {}) {
meta.wireguard = mkMerge (flip map (attrNames cfg) (proxy: {
${cfg.${proxy}.nicName}.client.via = proxy;
}));
networking.nftables.firewall = mkMerge (flip map (attrNames cfg) (proxy: {
zones = {
# Parent zone for the whole interface
${cfg.${proxy}.nicName}.interfaces = [cfg.${proxy}.nicName];
# Subzone to specifically target the proxy host
${proxy} = {
parent = cfg.${proxy}.nicName;
ipv4Addresses = [nodes.${proxy}.config.meta.wireguard.${cfg.${proxy}.nicName}.ipv4];
ipv6Addresses = [nodes.${proxy}.config.meta.wireguard.${cfg.${proxy}.nicName}.ipv6];
};
};
rules."${proxy}-to-local" = {
from = [proxy];
to = ["local"];
ignoreEmptyRule = true;
inherit
(cfg.${proxy})
allowedTCPPorts
allowedUDPPorts
;
};
}));
};
}

View file

@ -1,411 +0,0 @@
{
config,
lib,
...
}: let
inherit
(lib)
any
attrNames
attrValues
concatAttrs
concatMap
concatMapStrings
concatStringsSep
duplicates
filter
genAttrs
head
mapAttrsToList
mergeToplevelConfigs
mkIf
mkOption
net
optionalAttrs
optionals
stringLength
types
wireguard
;
cfg = config.meta.wireguard;
nodeName = config.node.name;
configForNetwork = wgName: wgCfg: let
inherit
(wireguard wgName)
externalPeerName
externalPeerNamesRaw
networkCidrs
participatingClientNodes
participatingNodes
participatingServerNodes
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 external peers
++ map externalPeerName (attrNames wgCfg.server.externalPeers)
# Our clients
++ ourClientNodes
else [wgCfg.client.via];
# Figure out if there are duplicate peers or addresses so we can
# make an assertion later.
duplicatePeers = duplicates externalPeerNamesRaw;
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
map (net.cidr.make 128) (
# The server accepts traffic to it's own address
snCfg.addresses
# plus traffic for any of its external peers
++ attrValues snCfg.server.externalPeers
# plus traffic for any client that is connected via that server
++ concatMap (n: (wgCfgOf n).addresses) (filter (n: (wgCfgOf n).client.via == serverNode) participatingClientNodes)
);
in {
assertions = [
{
assertion = any (n: (wgCfgOf n).server.host != null) participatingNodes;
message = "${assertionPrefix}: At least one node in a network must be a server.";
}
{
assertion = duplicatePeers == [];
message = "${assertionPrefix}: Multiple definitions for external peer(s):${concatMapStrings (x: " '${x}'") duplicatePeers}";
}
{
assertion = duplicateAddrs == [];
message = "${assertionPrefix}: Addresses used multiple times: ${concatStringsSep ", " duplicateAddrs}";
}
{
assertion = isServer != isClient;
message = "${assertionPrefix}: A node must either be a server (define server.host) or a client (define client.via).";
}
{
assertion = isClient -> ((wgCfgOf wgCfg.client.via).server.host != null);
message = "${assertionPrefix}: The specified via node '${wgCfg.client.via}' must be a wireguard server.";
}
{
assertion = stringLength wgCfg.linkName < 16;
message = "${assertionPrefix}: The specified linkName '${wgCfg.linkName}' is too long (must be max 15 characters).";
}
];
networking.firewall.allowedUDPPorts =
mkIf
(isServer && wgCfg.server.openFirewall)
[wgCfg.server.port];
# Open the port in the given nftables rule if specified
networking.nftables.firewall.rules =
optionalAttrs (isServer && wgCfg.server.openFirewallRules != [])
(genAttrs wgCfg.server.openFirewallRules (_: {allowedUDPPorts = [wgCfg.server.port];}));
age.secrets =
concatAttrs (map
(other: {
${peerPresharedKeySecret nodeName other} = {
rekeyFile = peerPresharedKeyPath nodeName other;
owner = "systemd-network";
generator.script = {pkgs, ...}: "${pkgs.wireguard-tools}/bin/wg genpsk";
};
})
neededPeers)
// {
${peerPrivateKeySecret nodeName} = {
rekeyFile = peerPrivateKeyPath nodeName;
owner = "systemd-network";
generator.script = {
pkgs,
file,
...
}: ''
priv=$(${pkgs.wireguard-tools}/bin/wg genkey)
${pkgs.wireguard-tools}/bin/wg pubkey <<< "$priv" > ${lib.escapeShellArg (lib.removeSuffix ".age" file + ".pub")}
echo "$priv"
'';
};
};
systemd.network.netdevs."${wgCfg.unitConfName}" = {
netdevConfig = {
Kind = "wireguard";
Name = wgCfg.linkName;
Description = "Wireguard network ${wgName}";
};
wireguardConfig =
{
PrivateKeyFile = config.age.secrets.${peerPrivateKeySecret nodeName}.path;
}
// optionalAttrs isServer {
ListenPort = wgCfg.server.port;
};
wireguardPeers =
if isServer
then
# Always include all other server nodes.
map (serverNode: let
snCfg = wgCfgOf serverNode;
in {
wireguardPeerConfig = {
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 our external peers
++ mapAttrsToList (extPeer: ips: let
peerName = externalPeerName extPeer;
in {
wireguardPeerConfig = {
PublicKey = builtins.readFile (peerPublicKeyPath peerName);
PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName peerName}.path;
AllowedIPs = map (net.cidr.make 128) ips;
# Connections to external peers should always be kept alive
PersistentKeepalive = 25;
};
})
wgCfg.server.externalPeers
# All client nodes that have their via set to us.
++ map (clientNode: let
clientCfg = wgCfgOf clientNode;
in {
wireguardPeerConfig = {
PublicKey = builtins.readFile (peerPublicKeyPath clientNode);
PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName clientNode}.path;
AllowedIPs = map (net.cidr.make 128) clientCfg.addresses;
};
})
ourClientNodes
else
# We are a client node, so only include our via server.
[
{
wireguardPeerConfig = let
snCfg = wgCfgOf wgCfg.client.via;
in
{
PublicKey = builtins.readFile (peerPublicKeyPath wgCfg.client.via);
PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName wgCfg.client.via}.path;
Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}";
# Access to the whole network is routed through our entry node.
# TODO this should add any routedAddresses on ANY server in the network, right?
# if A entries via B and only C can route 0.0.0.0/0, does that work?
AllowedIPs = networkCidrs;
}
// optionalAttrs wgCfg.client.keepalive {
PersistentKeepalive = 25;
};
}
];
};
systemd.network.networks."${wgCfg.unitConfName}" = {
matchConfig.Name = wgCfg.linkName;
address = map toNetworkAddr wgCfg.addresses;
};
};
in {
options.meta.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 funnctionality 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`.";
};
openFirewallRules = mkOption {
default = [];
type = types.listOf types.str;
description = "The {option}`port` will be opened for all of the given rules in the nftable-firewall.";
};
externalPeers = mkOption {
type = types.attrsOf (types.listOf (types.net.ip-in config.addresses));
default = {};
example = {my-android-phone = ["10.0.0.97"];};
description = ''
Allows defining an extra set of peers that should be added to this wireguard network,
but will not be managed by this flake. (e.g. phones)
These external peers will only know this node as a peer, which will forward
their traffic to other members of the network if required. This requires
this node to act as a server.
'';
};
reservedAddresses = mkOption {
type = types.listOf types.net.cidr;
default = [];
example = ["10.0.0.1/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. If you know that there might be additional external peers added later,
it may be beneficial to reserve a bigger address space from the start to avoid having
to update existing external peers when the generated address space expands.
'';
};
};
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 (wireguard 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 (wireguard 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.
'';
};
# TODO this is not yet implemented.
# - is 0.0.0.0/0 also for valid for routing global ipv6?
# - is 0.0.0.0/0 routing private spaces such as 192.168.1 ? that'd be baaad
# - force nodes to opt-in or allow nodes to opt-out? sometimes a node wants
# to use the network without routing additional stuff.
# - allow specifying the route metric.
routedAddresses = mkOption {
type = types.listOf types.net.cidr;
default = [];
example = ["0.0.0.0/0"];
description = ''
Additional networks that are accessible through this machine. This will allow
other participants of the network to access these networks through the tunnel.
Make sure to configure a NAT on the created interface (or that the proper routes
are generated) to allow inter-network communication.
'';
};
};
}));
};
config = mkIf (cfg != {}) (mergeToplevelConfigs
["assertions" "age" "networking" "systemd"]
(mapAttrsToList configForNetwork cfg));
}