diff options
| author | Tim Keller <tjk@tjkeller.xyz> | 2026-06-15 11:29:01 -0500 |
|---|---|---|
| committer | Tim Keller <tjk@tjkeller.xyz> | 2026-06-15 11:29:01 -0500 |
| commit | 31c2bcdd5f0a40da1882acf9ae108ed80a2e4740 (patch) | |
| tree | 16e95bd629c4c4396d72deb3ead0988a3d5e8b0b | |
| parent | 816add7332d1e180bce16bb467cd77cb3c354a8f (diff) | |
| download | nixos-31c2bcdd5f0a40da1882acf9ae108ed80a2e4740.tar.xz nixos-31c2bcdd5f0a40da1882acf9ae108ed80a2e4740.zip | |
add hairpinning module to networking.nat for router
| -rw-r--r-- | nixos/services/router/default.nix | 1 | ||||
| -rw-r--r-- | nixos/services/router/hairpinning.nix | 47 | ||||
| -rw-r--r-- | nixos/services/router/routing.nix | 6 |
3 files changed, 49 insertions, 5 deletions
diff --git a/nixos/services/router/default.nix b/nixos/services/router/default.nix index d9df5a1..4e59752 100644 --- a/nixos/services/router/default.nix +++ b/nixos/services/router/default.nix @@ -1,6 +1,7 @@ { imports = [ ./dns-dhcp.nix + ./hairpinning.nix ./routing.nix ./unbound-blocklist.nix ]; diff --git a/nixos/services/router/hairpinning.nix b/nixos/services/router/hairpinning.nix new file mode 100644 index 0000000..e449525 --- /dev/null +++ b/nixos/services/router/hairpinning.nix @@ -0,0 +1,47 @@ +{ config, lib, ... }: let + cfg = config.networking.nat._hairpinning; + + forwardPorts = config.networking.nat.forwardPorts; + internalInterfaces = config.networking.nat.internalInterfaces; + + # DNAT + mkIfacesMap = ifs: lib.concatMapStringsSep ", " (i: ''"${i}"'') ifs; + mkDnatRule = ifs: proto: sourcePort: destination: '' + iifname { ${mkIfacesMap ifs} } ${proto} dport ${toString sourcePort} dnat to ${destination} + ''; + dnatRules = lib.concatMapStrings (p: mkDnatRule internalInterfaces p.proto p.sourcePort p.destination) forwardPorts; + + # SNAT + mkSnatTargets = proto: destIp: destPort: '' + ip daddr ${destIp} ${proto} dport ${destPort} masquerade + ''; + snatTargets = lib.concatMapStrings(p: let + d = lib.splitString ":" p.destination; + destIp = builtins.elemAt d 0; + destPort = builtins.elemAt d 1; + in mkSnatTargets p.proto destIp destPort) forwardPorts; +in { + options.networking.nat._hairpinning = { + enable = lib.mkEnableOption "enable nat hairpinning (loopback) to allow internal interfaces to reach forwarded services via the external ip"; + }; + config = lib.mkIf (config.networking.nat.enable && cfg.enable) { + warnings = [ + (lib.mkIf (forwardPorts == []) + "nat hairpinning enabled but no forwardPorts have been configured. skipping configuration.") + ]; + + networking.nftables.tables."hairpin" = lib.mkIf (forwardPorts != []) { + family = "ip"; + content = '' + chain pre { + type nat hook prerouting priority dstnat; policy accept; + ${dnatRules} + } + chain post { + type nat hook postrouting priority srcnat; policy accept; + ${snatTargets} + } + ''; + }; + }; +} diff --git a/nixos/services/router/routing.nix b/nixos/services/router/routing.nix index 9534081..c86301f 100644 --- a/nixos/services/router/routing.nix +++ b/nixos/services/router/routing.nix @@ -27,16 +27,12 @@ in { # Allow lan interfaces to access the router trustedInterfaces = cfg.interfaces.lan; - - # Allow lan interfaces to access the internet - extraForwardRules = lib.concatMapStrings (lanIf: '' - iifname "${lanIf}" oifname "${cfg.interfaces.wan}" accept - '') cfg.interfaces.lan; }; nat = { enable = lib.mkDefault true; externalInterface = lib.mkDefault cfg.interfaces.wan; internalInterfaces = lib.mkDefault cfg.interfaces.lan; + _hairpinning.enable = lib.mkDefault (config.networking.nat.forwardPorts != []); }; }; }; |
