{ config, lib, pkgs, ... }: let inherit (lib) mkOption types; wgConfigType = types.listOf (types.submodule { options = { interface = mkOption { type = types.str; description = "WireGuard interface name."; }; serverAddress = mkOption { type = types.str; description = "IPv6 address/prefix to bind WireGuard (e.g., 2001:db8::1/64)."; }; serverPort = mkOption { type = types.port; description = "Port for WireGuard."; }; clientAddress = mkOption { type = types.str; description = "Peer IPv6 address/prefix (e.g., 2001:db8::2/128)."; }; clientSubnet = mkOption { type = types.str; description = "Peer IPv6 subnet (e.g., 2001:db8::/64)."; }; serverPrivateKeyFile = mkOption { type = types.path; description = "Path to server private key file."; }; clientPublicKeyFile = mkOption { type = types.path; description = "Path to client public key file."; }; }; }); interfaces = config.wireguard.interfaces; extract = attr: map (x: x.${attr}) interfaces; assertUnique = name: values: { assertion = values == lib.unique values; message = "Duplicate values found for '${name}': ${builtins.toString values}"; }; fileAssertions = lib.flatten (map (cfg: [ { assertion = cfg.serverPrivateKeyFile != null; message = "serverPrivateKeyFile must be provided for interface ${cfg.interface}"; } { assertion = cfg.clientPublicKeyFile != null; message = "clientPublicKeyFile must be provided for interface ${cfg.interface}"; } ]) interfaces); uniquenessAssertions = [ (assertUnique "interface" (extract "interface")) (assertUnique "serverAddress" (extract "serverAddress")) (assertUnique "serverPort" (extract "serverPort")) (assertUnique "clientAddress" (extract "clientAddress")) (assertUnique "clientSubnet" (extract "clientSubnet")) ]; in { options.wireguard.interfaces = mkOption { type = wgConfigType; default = []; description = "List of WireGuard interface configurations."; }; config = { assertions = lib.mkAfter (fileAssertions ++ uniquenessAssertions); systemd.network.enable = true; systemd.network.netdevs = lib.mkMerge (map (cfg: { "${cfg.interface}" = { netdevConfig = { Kind = "wireguard"; MTUBytes = "1412"; Name = cfg.interface; }; wireguardConfig = { PrivateKeyFile = cfg.serverPrivateKeyFile; ListenPort = cfg.serverPort; }; wireguardPeers = [{ PublicKeyFile = cfg.clientPublicKeyFile; AllowedIPs = [ cfg.clientAddress cfg.clientSubnet ]; }]; }; }) interfaces); systemd.network.networks = lib.mkMerge (map (cfg: { "${cfg.interface}" = { matchConfig = { Name = cfg.interface; }; networkConfig = { IPv6Forwarding = "yes"; }; address = [ cfg.serverAddress ]; routes = [ { Destination = cfg.clientSubnet; Scope = "link"; } ]; }; }) interfaces); # Automatically open firewall ports for WireGuard networking.firewall.allowedUDPPorts = map (cfg: cfg.serverPort) interfaces; }; }