at 25.11-pre 5.6 kB view raw
1{ config, lib, ... }: 2 3with lib; 4 5let 6 cfg = config.networking.nat; 7 8 mkDest = externalIP: if externalIP == null then "masquerade" else "snat ${externalIP}"; 9 dest = mkDest cfg.externalIP; 10 destIPv6 = mkDest cfg.externalIPv6; 11 12 toNftSet = list: concatStringsSep ", " list; 13 toNftRange = ports: replaceStrings [ ":" ] [ "-" ] (toString ports); 14 15 ifaceSet = toNftSet (map (x: ''"${x}"'') cfg.internalInterfaces); 16 ipSet = toNftSet cfg.internalIPs; 17 ipv6Set = toNftSet cfg.internalIPv6s; 18 oifExpr = optionalString (cfg.externalInterface != null) ''oifname "${cfg.externalInterface}"''; 19 20 # Whether given IP (plus optional port) is an IPv6. 21 isIPv6 = ip: length (lib.splitString ":" ip) > 2; 22 23 splitIPPorts = 24 IPPorts: 25 let 26 matchIP = if isIPv6 IPPorts then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)"; 27 m = builtins.match "${matchIP}:([0-9-]+)" IPPorts; 28 in 29 { 30 IP = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 0; 31 ports = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 1; 32 }; 33 34 mkTable = 35 { 36 ipVer, 37 dest, 38 ipSet, 39 forwardPorts, 40 dmzHost, 41 externalIP, 42 }: 43 let 44 # nftables maps for port forward 45 # [daddr .] l4proto . dport : addr . port 46 fwdMap = toNftSet ( 47 map ( 48 fwd: 49 with (splitIPPorts fwd.destination); 50 "${ 51 optionalString (externalIP != null) "${externalIP} . " 52 }${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}" 53 ) forwardPorts 54 ); 55 56 # nftables maps for port forward loopback dnat 57 # daddr . l4proto . dport : addr . port 58 fwdLoopDnatMap = toNftSet ( 59 concatMap ( 60 fwd: 61 map ( 62 loopbackip: 63 with (splitIPPorts fwd.destination); 64 "${loopbackip} . ${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}" 65 ) fwd.loopbackIPs 66 ) forwardPorts 67 ); 68 69 # nftables set for port forward loopback snat 70 # daddr . l4proto . dport 71 fwdLoopSnatSet = toNftSet ( 72 map (fwd: with (splitIPPorts fwd.destination); "${IP} . ${fwd.proto} . ${ports}") forwardPorts 73 ); 74 in 75 '' 76 chain pre { 77 type nat hook prerouting priority dstnat; 78 79 ${optionalString (fwdMap != "") '' 80 iifname "${cfg.externalInterface}" meta l4proto { tcp, udp } dnat ${ 81 optionalString (externalIP != null) "${ipVer} daddr . " 82 }meta l4proto . th dport map { ${fwdMap} } comment "port forward" 83 ''} 84 85 ${optionalString (fwdLoopDnatMap != "") '' 86 meta l4proto { tcp, udp } dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from other hosts behind NAT" 87 ''} 88 89 ${optionalString (dmzHost != null) '' 90 iifname "${cfg.externalInterface}" dnat ${dmzHost} comment "dmz" 91 ''} 92 } 93 94 chain post { 95 type nat hook postrouting priority srcnat; 96 97 ${optionalString (ifaceSet != "") '' 98 iifname { ${ifaceSet} } ${oifExpr} ${dest} comment "from internal interfaces" 99 ''} 100 ${optionalString (ipSet != "") '' 101 ${ipVer} saddr { ${ipSet} } ${oifExpr} ${dest} comment "from internal IPs" 102 ''} 103 104 ${optionalString (fwdLoopSnatSet != "") '' 105 iifname != "${cfg.externalInterface}" ${ipVer} daddr . meta l4proto . th dport { ${fwdLoopSnatSet} } masquerade comment "port forward loopback snat" 106 ''} 107 } 108 109 chain out { 110 type nat hook output priority mangle; 111 112 ${optionalString (fwdLoopDnatMap != "") '' 113 meta l4proto { tcp, udp } dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from the host itself" 114 ''} 115 } 116 ''; 117 118in 119 120{ 121 122 config = mkIf (config.networking.nftables.enable && cfg.enable) { 123 124 assertions = [ 125 { 126 assertion = cfg.extraCommands == ""; 127 message = "extraCommands is incompatible with the nftables based nat module: ${cfg.extraCommands}"; 128 } 129 { 130 assertion = cfg.extraStopCommands == ""; 131 message = "extraStopCommands is incompatible with the nftables based nat module: ${cfg.extraStopCommands}"; 132 } 133 { 134 assertion = config.networking.nftables.rulesetFile == null; 135 message = "networking.nftables.rulesetFile conflicts with the nat module"; 136 } 137 ]; 138 139 networking.nftables.tables = { 140 "nixos-nat" = { 141 family = "ip"; 142 content = mkTable { 143 ipVer = "ip"; 144 inherit dest ipSet; 145 forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts; 146 inherit (cfg) dmzHost externalIP; 147 }; 148 }; 149 "nixos-nat6" = mkIf cfg.enableIPv6 { 150 family = "ip6"; 151 name = "nixos-nat"; 152 content = mkTable { 153 ipVer = "ip6"; 154 dest = destIPv6; 155 ipSet = ipv6Set; 156 forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts; 157 dmzHost = null; 158 externalIP = cfg.externalIPv6; 159 }; 160 }; 161 }; 162 163 networking.firewall.extraForwardRules = optionalString config.networking.firewall.filterForward '' 164 ${optionalString (ifaceSet != "") '' 165 iifname { ${ifaceSet} } ${oifExpr} accept comment "from internal interfaces" 166 ''} 167 ${optionalString (ipSet != "") '' 168 ip saddr { ${ipSet} } ${oifExpr} accept comment "from internal IPs" 169 ''} 170 ${optionalString (ipv6Set != "") '' 171 ip6 saddr { ${ipv6Set} } ${oifExpr} accept comment "from internal IPv6s" 172 ''} 173 ''; 174 175 }; 176}