feat[server]: support multiple wireguard tunnels

This commit is contained in:
Leon Schwarzäugl 2025-12-22 14:14:45 +01:00
parent b6eb29fad9
commit f1fa6f8e99
Signed by: swarsel
GPG key ID: 26A54C31F2A4FD84
16 changed files with 546 additions and 357 deletions

View file

@ -1,176 +1,217 @@
{ self, lib, pkgs, config, confLib, nodes, globals, ... }:
let
wgInterface = "wg0";
inherit (confLib.gen { name = "wireguard"; port = 52829; user = "systemd-network"; group = "systemd-network"; }) servicePort serviceName serviceUser serviceGroup;
inherit (confLib.gen {
name = "wireguard";
port = 52829;
user = "systemd-network";
group = "systemd-network";
}) servicePort serviceName serviceUser serviceGroup;
inherit (config.swarselsystems) sopsFile;
wgSopsFile = self + "/secrets/repo/wg.yaml";
inherit (config.swarselsystems.server.wireguard) peers isClient isServer serverName serverNetConfigPrefix ifName;
cfg = config.swarselsystems.server.wireguard;
inherit (cfg) interfaces;
ifaceList = builtins.attrValues interfaces;
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} settings";
swarselmodules.server.${serviceName} =
lib.mkEnableOption "enable ${serviceName} settings";
swarselsystems.server.wireguard = {
isServer = lib.mkEnableOption "set this as a wireguard server";
isClient = lib.mkEnableOption "set this as a wireguard client";
serverName = lib.mkOption {
type = lib.types.str;
default = "";
};
serverNetConfigPrefix = lib.mkOption {
type = lib.types.str;
default = "${if nodes.${serverName}.config.swarselsystems.isCloud then nodes.${serverName}.config.node.name else "home"}";
readOnly = true;
};
ifName = lib.mkOption {
type = lib.types.str;
default = wgInterface;
};
peers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Wireguard peer config names";
interfaces = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({ name, config, ... }: {
options = {
isServer = lib.mkEnableOption "set this interface as a wireguard server";
isClient = lib.mkEnableOption "set this interface as a wireguard client";
serverName = lib.mkOption {
type = lib.types.str;
default = "";
description = "Hostname of the WireGuard server this interface connects to (when isClient = true).";
};
serverNetConfigPrefix = lib.mkOption {
type = lib.types.str;
default =
let
serverCfg = nodes.${config.serverName}.config;
in
if serverCfg.swarselsystems.isCloud
then serverCfg.node.name
else "home";
readOnly = true;
description = "Prefix used to look up the server network in globals.networks.\"<prefix>-wg\".";
};
ifName = lib.mkOption {
type = lib.types.str;
default = name;
description = "Name of the WireGuard interface.";
};
peers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "WireGuard peer config names (clients when this host is server, or additional peers).";
};
};
}));
default = { };
description = "WireGuard interfaces defined on this host.";
};
};
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
environment.systemPackages = with pkgs; [
wireguard-tools
];
sops = {
secrets = {
wireguard-private-key = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0600"; };
# create this secret only if this is a simple client with only one peer (the server)
"wireguard-${serverName}-${config.node.name}-presharedKey" = lib.mkIf (isClient && peers == [ ]) { sopsFile = wgSopsFile; owner = serviceUser; group = serviceGroup; mode = "0600"; };
}
# create these secrets only if this host has multiple peers
// lib.optionalAttrs (peers != [ ]) (builtins.listToAttrs (map
(clientName: {
name = "wireguard-${config.node.name}-${clientName}-presharedKey";
value = { sopsFile = wgSopsFile; owner = serviceUser; group = serviceGroup; mode = "0600"; };
})
peers));
};
sops.secrets =
lib.mkMerge (
[
{
# shared host private key
wireguard-private-key = {
inherit sopsFile;
owner = serviceUser;
group = serviceGroup;
mode = "0600";
};
}
] ++ (map
(i:
let
simpleClientSecrets =
lib.optionalAttrs (i.isClient && i.peers == [ ]) {
"wireguard-${i.serverName}-${config.node.name}-${i.ifName}-presharedKey" = {
sopsFile = wgSopsFile;
owner = serviceUser;
group = serviceGroup;
mode = "0600";
};
};
multiPeerSecrets =
lib.optionalAttrs (i.peers != [ ]) (builtins.listToAttrs (map
(clientName: {
name = "wireguard-${config.node.name}-${clientName}-${i.ifName}-presharedKey";
value = {
sopsFile = wgSopsFile;
owner = serviceUser;
group = serviceGroup;
mode = "0600";
};
})
i.peers));
in
simpleClientSecrets // multiPeerSecrets
)
ifaceList)
);
networking = {
firewall.checkReversePath = lib.mkIf isClient "loose";
firewall.allowedUDPPorts = [ servicePort ];
# nat = lib.mkIf (config.swarselsystems.isCloud && isServer) {
# enable = true;
# enableIPv6 = true;
# externalInterface = "enp0s6";
# internalInterfaces = [ ifName ];
# };
# interfaces.${ifName}.mtu = 1280; # the default (1420) is not enough!
firewall.checkReversePath =
lib.mkIf (lib.any (i: i.isClient) ifaceList) "loose";
firewall.allowedUDPPorts =
lib.mkIf (lib.any (i: i.isServer) ifaceList) [ servicePort ];
};
systemd.network = {
enable = true;
networks."50-${ifName}" = {
matchConfig.Name = ifName;
linkConfig = {
MTUBytes = 1408; # TODO: figure out where we lose those 12 bits (8 from pppoe maybe + ???)
};
# networkConfig = lib.mkIf (config.swarselsystems.isCloud && isServer) {
# IPv4Forwarding = true;
# IPv6Forwarding = true;
# };
address =
if isServer then [
globals.networks."${config.swarselsystems.server.netConfigPrefix}-wg".hosts.${config.node.name}.cidrv4
globals.networks."${config.swarselsystems.server.netConfigPrefix}-wg".hosts.${config.node.name}.cidrv6
] else [
globals.networks."${serverNetConfigPrefix}-wg".hosts.${config.node.name}.cidrv4
globals.networks."${serverNetConfigPrefix}-wg".hosts.${config.node.name}.cidrv6
];
};
netdevs."50-${ifName}" = {
netdevConfig = {
Kind = "wireguard";
Name = ifName;
};
wireguardConfig = {
ListenPort = lib.mkIf isServer servicePort;
# ensure file is readable by `systemd-network` user
PrivateKeyFile = config.sops.secrets.wireguard-private-key.path;
# To automatically create routes for everything in AllowedIPs,
# add RouteTable=main
RouteTable = lib.mkIf isClient "main";
# FirewallMark marks all packets send and received by wg0
# with the number 42, which can be used to define policy rules on these packets.
# FirewallMark = 42;
};
wireguardPeers = lib.optionals isClient [
networks = lib.mkMerge (map
(i:
let
inherit (i) ifName;
in
{
PublicKey = builtins.readFile "${self}/secrets/public/wg/${serverName}.pub";
PresharedKeyFile = config.sops.secrets."wireguard-${serverName}-${config.node.name}-presharedKey".path;
Endpoint = "server.${serverName}.${globals.domains.main}:${toString servicePort}";
# Access to the whole network is routed through our entry node.
PersistentKeepalive = 25;
AllowedIPs =
let
wgNetwork = globals.networks."${serverNetConfigPrefix}-wg";
in
(lib.optional (wgNetwork.cidrv4 != null) wgNetwork.cidrv4)
++ (lib.optional (wgNetwork.cidrv6 != null) wgNetwork.cidrv6);
}
] ++ lib.optionals isServer (map
(clientName: {
PublicKey = builtins.readFile "${self}/secrets/public/wg/${clientName}.pub";
PresharedKeyFile = config.sops.secrets."wireguard-${config.node.name}-${clientName}-presharedKey".path;
# PersistentKeepalive = 25;
AllowedIPs =
let
clientInWgNetwork = globals.networks."${config.swarselsystems.server.netConfigPrefix}-wg".hosts.${clientName};
in
(lib.optional (clientInWgNetwork.ipv4 != null) (lib.net.cidr.make 32 clientInWgNetwork.ipv4))
++ (lib.optional (clientInWgNetwork.ipv6 != null) (lib.net.cidr.make 128 clientInWgNetwork.ipv6));
"50-${ifName}" = {
matchConfig.Name = ifName;
linkConfig = {
MTUBytes = 1408; # TODO: figure out where we lose those 12 bits (8 from pppoe maybe + ???)
};
address =
if i.isServer then [
globals.networks."${config.swarselsystems.server.netConfigPrefix}-${ifName}".hosts.${config.node.name}.cidrv4
globals.networks."${config.swarselsystems.server.netConfigPrefix}-${ifName}".hosts.${config.node.name}.cidrv6
] else [
globals.networks."${i.serverNetConfigPrefix}-${ifName}".hosts.${config.node.name}.cidrv4
globals.networks."${i.serverNetConfigPrefix}-${ifName}".hosts.${config.node.name}.cidrv6
];
};
})
peers);
ifaceList);
};
netdevs = lib.mkMerge (map
(i:
let
inherit (i) ifName;
in
{
"50-${ifName}" = {
netdevConfig = {
Kind = "wireguard";
Name = ifName;
};
wireguardConfig = {
ListenPort = lib.mkIf i.isServer servicePort;
PrivateKeyFile = config.sops.secrets.wireguard-private-key.path;
RouteTable = lib.mkIf i.isClient "main";
};
wireguardPeers =
lib.optionals i.isClient [
{
PublicKey =
builtins.readFile "${self}/secrets/public/wg/${i.serverName}.pub";
PresharedKeyFile =
config.sops.secrets."wireguard-${i.serverName}-${config.node.name}-${i.ifName}-presharedKey".path;
Endpoint =
"server.${i.serverName}.${globals.domains.main}:${toString servicePort}";
PersistentKeepalive = 25;
AllowedIPs =
let
wgNetwork = globals.networks."${i.serverNetConfigPrefix}-${i.ifName}";
in
(lib.optional (wgNetwork.cidrv4 != null) wgNetwork.cidrv4)
++ (lib.optional (wgNetwork.cidrv6 != null) wgNetwork.cidrv6);
}
]
++ lib.optionals i.isServer (map
(clientName: {
PublicKey =
builtins.readFile "${self}/secrets/public/wg/${clientName}.pub";
PresharedKeyFile =
config.sops.secrets."wireguard-${config.node.name}-${clientName}-${i.ifName}-presharedKey".path;
AllowedIPs =
let
clientInWgNetwork =
globals.networks."${config.swarselsystems.server.netConfigPrefix}-${i.ifName}".hosts.${clientName};
in
(lib.optional (clientInWgNetwork.ipv4 != null)
(lib.net.cidr.make 32 clientInWgNetwork.ipv4))
++ (lib.optional (clientInWgNetwork.ipv6 != null)
(lib.net.cidr.make 128 clientInWgNetwork.ipv6));
})
i.peers);
};
})
ifaceList);
};
# networking = {
# wireguard = {
# enable = true;
# interfaces = {
# wg1 = {
# privateKeyFile = config.sops.secrets.wireguard-private-key.path;
# ips = [ "192.168.178.201/24" ];
# peers = [
# {
# publicKey = "PmeFInoEJcKx+7Kva4dNnjOEnJ8lbudSf1cbdo/tzgw=";
# presharedKeyFile = config.sops.secrets.wireguard-home-preshared-key.path;
# name = "moonside";
# persistentKeepalive = 25;
# # endpoint = "${config.repo.secrets.common.ipv4}:51820";
# endpoint = "${config.repo.secrets.common.wireguardEndpoint}";
# # allowedIPs = [
# # "192.168.3.0/24"
# # "192.168.1.0/24"
# # ];
# allowedIPs = [
# "192.168.178.0/24"
# ];
# }
# ];
# };
# };
# };
# };
};
}