main
  1{
  2  config,
  3  lib,
  4  pkgs,
  5  ...
  6}:
  7let
  8  inherit (lib)
  9    mkEnableOption
 10    mkIf
 11    mkOption
 12    types
 13    ;
 14  cfg = config.services.wireguard.server;
 15
 16  # Detect if nftables is enabled
 17  usingNftables = config.networking.nftables.enable;
 18in
 19{
 20  options = {
 21    services.wireguard.server = {
 22      enable = mkEnableOption "Enable a wireguard server";
 23      ips = mkOption {
 24        type = with types; listOf str;
 25        description = ''
 26          The peer IPs
 27        '';
 28      };
 29      peers = mkOption {
 30        default = [ ];
 31        description = "Peers linked to the interface.";
 32        type = with types; listOf anything;
 33      };
 34      interface = mkOption {
 35        type = types.str;
 36        default = "wg0";
 37        description = ''
 38          Name of the wireguard interface. Override when a host already uses
 39          wg0 as a client (e.g. lutece, which is a mesh client on wg0 and a
 40          travel exit server on wg1).
 41        '';
 42      };
 43      listenPort = mkOption {
 44        type = types.int;
 45        default = 51820;
 46        description = ''
 47          UDP port the server listens on. Opened in the firewall.
 48        '';
 49      };
 50      privateKeyFile = mkOption {
 51        type = types.str;
 52        default = "/etc/wireguard/private.key";
 53        description = ''
 54          Path to the interface private key. Must differ from the client key
 55          when a host runs both roles.
 56        '';
 57      };
 58      natSource = mkOption {
 59        type = types.str;
 60        default = "10.100.0.0/24";
 61        description = ''
 62          Source subnet to masquerade. Applied on POSTROUTING without an output
 63          interface, so it covers both internet egress and traffic forwarded
 64          into another wireguard interface.
 65        '';
 66      };
 67      mtu = mkOption {
 68        type = with types; nullOr int;
 69        default = 1420;
 70        description = ''
 71          MTU size for the WireGuard interface.
 72          Common values: 1420 (conservative), 1380 (for PPPoE).
 73          If null, uses system default.
 74        '';
 75      };
 76    };
 77  };
 78  config = mkIf cfg.enable {
 79    environment.systemPackages = [ pkgs.wireguard-tools ];
 80    boot.kernel.sysctl."net.ipv4.ip_forward" = lib.mkForce 1; # FIXME should probably be mkDefault
 81
 82    # Firewall configuration - supports both iptables and nftables
 83    networking.firewall = {
 84      allowedUDPPorts = [ cfg.listenPort ];
 85      trustedInterfaces = [ cfg.interface ];
 86
 87      # iptables rules (used when nftables is disabled)
 88      extraCommands = mkIf (!usingNftables) ''
 89        iptables -t nat -A POSTROUTING -s ${cfg.natSource} -j MASQUERADE
 90        iptables -A FORWARD -i wg+ -j ACCEPT
 91      '';
 92    };
 93
 94    # nftables rules (used when nftables is enabled)
 95    networking.nftables.tables = mkIf usingNftables {
 96      wireguard-nat = {
 97        family = "ip";
 98        content = ''
 99          chain postrouting {
100            type nat hook postrouting priority srcnat; policy accept;
101            ip saddr ${cfg.natSource} masquerade
102          }
103
104          chain forward {
105            type filter hook forward priority filter; policy accept;
106            iifname "wg*" accept
107          }
108        '';
109      };
110    };
111
112    networking.wireguard.enable = true;
113    networking.wireguard.interfaces = {
114      "${cfg.interface}" = {
115        inherit (cfg)
116          ips
117          peers
118          listenPort
119          privateKeyFile
120          ;
121      }
122      // lib.optionalAttrs (cfg.mtu != null) { inherit (cfg) mtu; };
123    };
124  };
125}