{ 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 with CIDR 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 with CIDR (e.g., 2001:db8::2/128)."; }; clientSubnet = mkOption { type = types.str; description = "Peer subnet IPv6 CIDR (e.g., 2001:db8::/64)."; }; serverPrivateKeyFile = mkOption { type = types.str; description = "Name of the sops secret containing server private key."; }; clientPublicKeyFile = mkOption { type = types.str; description = "Name of the sops secret containing client public key."; }; }; }); 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}"; }; secretAssertions = lib.flatten (map (cfg: [ { assertion = builtins.hasAttr cfg.serverPrivateKeyFile config.sops.secrets; message = "Missing sops secret: ${cfg.serverPrivateKeyFile}"; } { assertion = builtins.hasAttr cfg.clientPublicKeyFile config.sops.secrets; message = "Missing sops secret: ${cfg.clientPublicKeyFile}"; } ]) 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; description = "List of WireGuard interface configurations."; }; config = { sops.secrets = let def = { owner = "systemd-network"; group = "systemd-network"; }; in lib.mkMerge (map (cfg: { "${cfg.serverPrivateKeyFile}" = def; "${cfg.clientPublicKeyFile}" = def; }) interfaces); assertions = lib.mkAfter (secretAssertions ++ uniquenessAssertions); systemd.network.netdevs = lib.mkMerge (map (cfg: { "${cfg.interface}" = { netdevConfig = { Kind = "wireguard"; Name = cfg.interface; }; wireguardConfig = { PrivateKeyFile = config.sops.secrets.${cfg.serverPrivateKeyFile}.path; ListenPort = cfg.serverPort; }; wireguardPeers = [{ PublicKeyFile = config.sops.secrets.${cfg.clientPublicKeyFile}.path; AllowedIPs = [ cfg.clientAddress cfg.clientSubnet ]; }]; }; }) interfaces); systemd.network.networks = lib.mkMerge (map (cfg: { "${cfg.interface}" = { matchConfig = { Name = cfg.interface; }; address = [ cfg.serverAddress ]; routes = [ { Destination = cfg.clientAddress; Scope = "link"; } { Destination = cfg.clientSubnet; Scope = "link"; } ]; }; }) interfaces); }; }