summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Keller <tjk@tjkeller.xyz>2026-06-15 11:29:01 -0500
committerTim Keller <tjk@tjkeller.xyz>2026-06-15 11:29:01 -0500
commit31c2bcdd5f0a40da1882acf9ae108ed80a2e4740 (patch)
tree16e95bd629c4c4396d72deb3ead0988a3d5e8b0b
parent816add7332d1e180bce16bb467cd77cb3c354a8f (diff)
downloadnixos-31c2bcdd5f0a40da1882acf9ae108ed80a2e4740.tar.xz
nixos-31c2bcdd5f0a40da1882acf9ae108ed80a2e4740.zip
add hairpinning module to networking.nat for router
-rw-r--r--nixos/services/router/default.nix1
-rw-r--r--nixos/services/router/hairpinning.nix47
-rw-r--r--nixos/services/router/routing.nix6
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 != []);
};
};
};