about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/networking/nat-nftables.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/modules/services/networking/nat-nftables.nix')
-rw-r--r--nixpkgs/nixos/modules/services/networking/nat-nftables.nix184
1 files changed, 184 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/services/networking/nat-nftables.nix b/nixpkgs/nixos/modules/services/networking/nat-nftables.nix
new file mode 100644
index 000000000000..483910a16658
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/networking/nat-nftables.nix
@@ -0,0 +1,184 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.networking.nat;
+
+  mkDest = externalIP:
+    if externalIP == null
+    then "masquerade"
+    else "snat ${externalIP}";
+  dest = mkDest cfg.externalIP;
+  destIPv6 = mkDest cfg.externalIPv6;
+
+  toNftSet = list: concatStringsSep ", " list;
+  toNftRange = ports: replaceStrings [ ":" ] [ "-" ] (toString ports);
+
+  ifaceSet = toNftSet (map (x: ''"${x}"'') cfg.internalInterfaces);
+  ipSet = toNftSet cfg.internalIPs;
+  ipv6Set = toNftSet cfg.internalIPv6s;
+  oifExpr = optionalString (cfg.externalInterface != null) ''oifname "${cfg.externalInterface}"'';
+
+  # Whether given IP (plus optional port) is an IPv6.
+  isIPv6 = ip: length (lib.splitString ":" ip) > 2;
+
+  splitIPPorts = IPPorts:
+    let
+      matchIP = if isIPv6 IPPorts then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
+      m = builtins.match "${matchIP}:([0-9-]+)" IPPorts;
+    in
+    {
+      IP = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 0;
+      ports = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 1;
+    };
+
+  mkTable = { ipVer, dest, ipSet, forwardPorts, dmzHost }:
+    let
+      # nftables does not support both port and port range as values in a dnat map.
+      # e.g. "dnat th dport map { 80 : 10.0.0.1 . 80, 443 : 10.0.0.2 . 900-1000 }"
+      # So we split them.
+      fwdPorts = filter (x: length (splitString "-" x.destination) == 1) forwardPorts;
+      fwdPortsRange = filter (x: length (splitString "-" x.destination) > 1) forwardPorts;
+
+      # nftables maps for port forward
+      # l4proto . dport : addr . port
+      toFwdMap = forwardPorts: toNftSet (map
+        (fwd:
+          with (splitIPPorts fwd.destination);
+          "${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}"
+        )
+        forwardPorts);
+      fwdMap = toFwdMap fwdPorts;
+      fwdRangeMap = toFwdMap fwdPortsRange;
+
+      # nftables maps for port forward loopback dnat
+      # daddr . l4proto . dport : addr . port
+      toFwdLoopDnatMap = forwardPorts: toNftSet (concatMap
+        (fwd: map
+          (loopbackip:
+            with (splitIPPorts fwd.destination);
+            "${loopbackip} . ${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}"
+          )
+          fwd.loopbackIPs)
+        forwardPorts);
+      fwdLoopDnatMap = toFwdLoopDnatMap fwdPorts;
+      fwdLoopDnatRangeMap = toFwdLoopDnatMap fwdPortsRange;
+
+      # nftables set for port forward loopback snat
+      # daddr . l4proto . dport
+      fwdLoopSnatSet = toNftSet (map
+        (fwd:
+          with (splitIPPorts fwd.destination);
+          "${IP} . ${fwd.proto} . ${ports}"
+        )
+        forwardPorts);
+    in
+    ''
+      chain pre {
+        type nat hook prerouting priority dstnat;
+
+        ${optionalString (fwdMap != "") ''
+          iifname "${cfg.externalInterface}" dnat meta l4proto . th dport map { ${fwdMap} } comment "port forward"
+        ''}
+        ${optionalString (fwdRangeMap != "") ''
+          iifname "${cfg.externalInterface}" dnat meta l4proto . th dport map { ${fwdRangeMap} } comment "port forward"
+        ''}
+
+        ${optionalString (fwdLoopDnatMap != "") ''
+          dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from other hosts behind NAT"
+        ''}
+        ${optionalString (fwdLoopDnatRangeMap != "") ''
+          dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatRangeMap} } comment "port forward loopback from other hosts behind NAT"
+        ''}
+
+        ${optionalString (dmzHost != null) ''
+          iifname "${cfg.externalInterface}" dnat ${dmzHost} comment "dmz"
+        ''}
+      }
+
+      chain post {
+        type nat hook postrouting priority srcnat;
+
+        ${optionalString (ifaceSet != "") ''
+          iifname { ${ifaceSet} } ${oifExpr} ${dest} comment "from internal interfaces"
+        ''}
+        ${optionalString (ipSet != "") ''
+          ${ipVer} saddr { ${ipSet} } ${oifExpr} ${dest} comment "from internal IPs"
+        ''}
+
+        ${optionalString (fwdLoopSnatSet != "") ''
+          iifname != "${cfg.externalInterface}" ${ipVer} daddr . meta l4proto . th dport { ${fwdLoopSnatSet} } masquerade comment "port forward loopback snat"
+        ''}
+      }
+
+      chain out {
+        type nat hook output priority mangle;
+
+        ${optionalString (fwdLoopDnatMap != "") ''
+          dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from the host itself"
+        ''}
+        ${optionalString (fwdLoopDnatRangeMap != "") ''
+          dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatRangeMap} } comment "port forward loopback from the host itself"
+        ''}
+      }
+    '';
+
+in
+
+{
+
+  config = mkIf (config.networking.nftables.enable && cfg.enable) {
+
+    assertions = [
+      {
+        assertion = cfg.extraCommands == "";
+        message = "extraCommands is incompatible with the nftables based nat module: ${cfg.extraCommands}";
+      }
+      {
+        assertion = cfg.extraStopCommands == "";
+        message = "extraStopCommands is incompatible with the nftables based nat module: ${cfg.extraStopCommands}";
+      }
+      {
+        assertion = config.networking.nftables.rulesetFile == null;
+        message = "networking.nftables.rulesetFile conflicts with the nat module";
+      }
+    ];
+
+    networking.nftables.ruleset = ''
+      table ip nixos-nat {
+        ${mkTable {
+          ipVer = "ip";
+          inherit dest ipSet;
+          forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
+          inherit (cfg) dmzHost;
+        }}
+      }
+
+      ${optionalString cfg.enableIPv6 ''
+        table ip6 nixos-nat {
+          ${mkTable {
+            ipVer = "ip6";
+            dest = destIPv6;
+            ipSet = ipv6Set;
+            forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
+            dmzHost = null;
+          }}
+        }
+      ''}
+    '';
+
+    networking.firewall.extraForwardRules = optionalString config.networking.firewall.filterForward ''
+      ${optionalString (ifaceSet != "") ''
+        iifname { ${ifaceSet} } ${oifExpr} accept comment "from internal interfaces"
+      ''}
+      ${optionalString (ipSet != "") ''
+        ip saddr { ${ipSet} } ${oifExpr} accept comment "from internal IPs"
+      ''}
+      ${optionalString (ipv6Set != "") ''
+        ip6 saddr { ${ipv6Set} } ${oifExpr} accept comment "from internal IPv6s"
+      ''}
+    '';
+
+  };
+}