You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

254 lines
8.5 KiB

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.wg-easy;
boolString = value: if value then "true" else "false";
listString = values: lib.concatStringsSep "," values;
validListItems =
values: lib.all (value: value != "" && builtins.match ".*,.*" value == null) values;
interfaceNamePattern = "[A-Za-z0-9_.-]{1,15}";
in
{
options.services.wg-easy = {
enable = lib.mkEnableOption "wg-easy";
package = lib.mkOption {
type = lib.types.package;
default = pkgs.callPackage ./package.nix { };
defaultText = lib.literalExpression "pkgs.callPackage ./package.nix { }";
description = "wg-easy package to run.";
};
interfaceName = lib.mkOption {
type = lib.types.str;
default = "wg0";
description = "WireGuard interface name managed by wg-easy.";
};
wireguardPort = lib.mkOption {
type = lib.types.port;
default = 51820;
description = "UDP port used by the WireGuard interface.";
};
uiPort = lib.mkOption {
type = lib.types.port;
default = 51821;
description = "TCP port used by the wg-easy web UI.";
};
stateDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/wg-easy";
description = "Writable directory for the SQLite database and generated WireGuard config.";
};
externalInterface = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Optional egress network interface used in wg-easy NAT hooks.";
};
enableIPv6 = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable IPv6 addresses and IPv6 firewall hooks in wg-easy.";
};
defaultDns = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = "DNS servers to stamp onto new clients. Leave null to use wg-easy's global default.";
};
defaultAllowedIps = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = "Allowed IPs to stamp onto new client configs. Leave null to use wg-easy's global default.";
};
defaultServerAllowedIps = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = "Additional server-side peer AllowedIPs to stamp onto new clients.";
};
defaultFirewallAllowedIps = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = "Firewall-enforced allowed destinations to stamp onto new clients.";
};
defaultPersistentKeepalive = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 0 65535);
default = null;
description = "Persistent keepalive to stamp onto new clients. Leave null to preserve wg-easy's configured default.";
};
firewallEnabled = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
description = "Enable or disable wg-easy per-client firewall filtering. Leave null to preserve wg-easy's configured value.";
};
forceUpdateClients = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Overwrite existing clients with configured Nix defaults on service startup.";
};
insecure = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Serve the UI as insecure HTTP. Keep false when using HTTPS through a reverse proxy.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Open the WireGuard UDP port in the NixOS firewall.";
};
openWebUIFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open the wg-easy web UI TCP port in the NixOS firewall.";
};
extraEnvironment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Additional environment variables for the wg-easy service.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = builtins.match interfaceNamePattern cfg.interfaceName != null;
message = "services.wg-easy.interfaceName must be 1-15 characters using only letters, numbers, '.', '_', or '-'.";
}
{
assertion =
cfg.externalInterface == null || builtins.match interfaceNamePattern cfg.externalInterface != null;
message = "services.wg-easy.externalInterface must be 1-15 characters using only letters, numbers, '.', '_', or '-'.";
}
{
assertion = lib.hasPrefix "/" cfg.stateDir;
message = "services.wg-easy.stateDir must be an absolute path.";
}
{
assertion = cfg.defaultDns == null || validListItems cfg.defaultDns;
message = "services.wg-easy.defaultDns entries must be non-empty and must not contain commas.";
}
{
assertion =
cfg.defaultAllowedIps == null
|| (cfg.defaultAllowedIps != [ ] && validListItems cfg.defaultAllowedIps);
message = "services.wg-easy.defaultAllowedIps must be null or a non-empty list without commas.";
}
{
assertion = cfg.defaultServerAllowedIps == null || validListItems cfg.defaultServerAllowedIps;
message = "services.wg-easy.defaultServerAllowedIps entries must be non-empty and must not contain commas.";
}
{
assertion =
cfg.defaultFirewallAllowedIps == null
|| (cfg.defaultFirewallAllowedIps != [ ] && validListItems cfg.defaultFirewallAllowedIps);
message = "services.wg-easy.defaultFirewallAllowedIps must be null or a non-empty list without commas.";
}
];
boot.kernel.sysctl = {
"net.ipv4.ip_forward" = lib.mkDefault 1;
"net.ipv4.conf.all.src_valid_mark" = lib.mkDefault 1;
}
// lib.optionalAttrs cfg.enableIPv6 {
"net.ipv6.conf.all.disable_ipv6" = lib.mkDefault 0;
"net.ipv6.conf.all.forwarding" = lib.mkDefault 1;
"net.ipv6.conf.default.forwarding" = lib.mkDefault 1;
};
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [ cfg.wireguardPort ];
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openWebUIFirewall [ cfg.uiPort ];
systemd.tmpfiles.rules = [
"d ${cfg.stateDir} 0700 root root -"
];
systemd.services.wg-easy = {
description = "WireGuard Easy";
documentation = [ "https://wg-easy.github.io/wg-easy/latest/" ];
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment = {
DISABLE_IPV6 = boolString (!cfg.enableIPv6);
INSECURE = boolString cfg.insecure;
PORT = toString cfg.uiPort;
WG_FORCE_UPDATE_CLIENTS = boolString cfg.forceUpdateClients;
WG_INTERFACE_NAME = cfg.interfaceName;
WG_PORT = toString cfg.wireguardPort;
WG_STATE_DIR = cfg.stateDir;
}
// lib.optionalAttrs (cfg.defaultDns != null) {
WG_DEFAULT_DNS = listString cfg.defaultDns;
}
// lib.optionalAttrs (cfg.defaultAllowedIps != null) {
WG_DEFAULT_ALLOWED_IPS = listString cfg.defaultAllowedIps;
}
// lib.optionalAttrs (cfg.defaultServerAllowedIps != null) {
WG_DEFAULT_SERVER_ALLOWED_IPS = listString cfg.defaultServerAllowedIps;
}
// lib.optionalAttrs (cfg.defaultFirewallAllowedIps != null) {
WG_DEFAULT_FIREWALL_ALLOWED_IPS = listString cfg.defaultFirewallAllowedIps;
}
// lib.optionalAttrs (cfg.defaultPersistentKeepalive != null) {
WG_DEFAULT_PERSISTENT_KEEPALIVE = toString cfg.defaultPersistentKeepalive;
}
// lib.optionalAttrs (cfg.firewallEnabled != null) {
WG_FIREWALL_ENABLED = boolString cfg.firewallEnabled;
}
// lib.optionalAttrs (cfg.externalInterface != null) {
WG_DEVICE = cfg.externalInterface;
}
// cfg.extraEnvironment;
path = with pkgs; [
bash
coreutils
gawk
gnugrep
gnused
iproute2
iptables
kmod
nftables
procps
wireguard-go
wireguard-tools
];
serviceConfig = {
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
RestartSec = "5s";
User = "root";
AmbientCapabilities = [
"CAP_NET_ADMIN"
"CAP_SYS_MODULE"
];
CapabilityBoundingSet = [
"CAP_NET_ADMIN"
"CAP_SYS_MODULE"
];
};
};
};
}