diff options
Diffstat (limited to 'nixpkgs/nixos/modules/services/networking/nat-nftables.nix')
-rw-r--r-- | nixpkgs/nixos/modules/services/networking/nat-nftables.nix | 184 |
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" + ''} + ''; + + }; +} |