1# This module enables Network Address Translation (NAT). 2# XXX: todo: support multiple upstream links 3# see http://yesican.chsoft.biz/lartc/MultihomedLinuxNetworking.html 4 5{ config, lib, pkgs, ... }: 6 7with lib; 8 9let 10 cfg = config.networking.nat; 11 12 mkDest = externalIP: if externalIP == null 13 then "-j MASQUERADE" 14 else "-j SNAT --to-source ${externalIP}"; 15 dest = mkDest cfg.externalIP; 16 destIPv6 = mkDest cfg.externalIPv6; 17 18 # Whether given IP (plus optional port) is an IPv6. 19 isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2; 20 21 helpers = import ./helpers.nix { inherit config lib; }; 22 23 flushNat = '' 24 ${helpers} 25 ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true 26 ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true 27 ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true 28 ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true 29 ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true 30 ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true 31 ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true 32 ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true 33 ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true 34 35 ${cfg.extraStopCommands} 36 ''; 37 38 mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: '' 39 # We can't match on incoming interface in POSTROUTING, so 40 # mark packets coming from the internal interfaces. 41 ${concatMapStrings (iface: '' 42 ${iptables} -w -t nat -A nixos-nat-pre \ 43 -i '${iface}' -j MARK --set-mark 1 44 '') cfg.internalInterfaces} 45 46 # NAT the marked packets. 47 ${optionalString (cfg.internalInterfaces != []) '' 48 ${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \ 49 ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest} 50 ''} 51 52 # NAT packets coming from the internal IPs. 53 ${concatMapStrings (range: '' 54 ${iptables} -w -t nat -A nixos-nat-post \ 55 -s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest} 56 '') internalIPs} 57 58 # NAT from external ports to internal ports. 59 ${concatMapStrings (fwd: '' 60 ${iptables} -w -t nat -A nixos-nat-pre \ 61 -i ${toString cfg.externalInterface} -p ${fwd.proto} \ 62 --dport ${builtins.toString fwd.sourcePort} \ 63 -j DNAT --to-destination ${fwd.destination} 64 65 ${concatMapStrings (loopbackip: 66 let 67 matchIP = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)"; 68 m = builtins.match "${matchIP}:([0-9-]+)" fwd.destination; 69 destinationIP = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0; 70 destinationPorts = if m == null then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1); 71 in '' 72 # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself 73 ${iptables} -w -t nat -A nixos-nat-out \ 74 -d ${loopbackip} -p ${fwd.proto} \ 75 --dport ${builtins.toString fwd.sourcePort} \ 76 -j DNAT --to-destination ${fwd.destination} 77 78 # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT 79 ${iptables} -w -t nat -A nixos-nat-pre \ 80 -d ${loopbackip} -p ${fwd.proto} \ 81 --dport ${builtins.toString fwd.sourcePort} \ 82 -j DNAT --to-destination ${fwd.destination} 83 84 ${iptables} -w -t nat -A nixos-nat-post \ 85 -d ${destinationIP} -p ${fwd.proto} \ 86 --dport ${destinationPorts} \ 87 -j SNAT --to-source ${loopbackip} 88 '') fwd.loopbackIPs} 89 '') forwardPorts} 90 ''; 91 92 setupNat = '' 93 ${helpers} 94 # Create subchains where we store rules 95 ip46tables -w -t nat -N nixos-nat-pre 96 ip46tables -w -t nat -N nixos-nat-post 97 ip46tables -w -t nat -N nixos-nat-out 98 99 ${mkSetupNat { 100 iptables = "iptables"; 101 inherit dest; 102 inherit (cfg) internalIPs; 103 forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts; 104 }} 105 106 ${optionalString cfg.enableIPv6 (mkSetupNat { 107 iptables = "ip6tables"; 108 dest = destIPv6; 109 internalIPs = cfg.internalIPv6s; 110 forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts; 111 })} 112 113 ${optionalString (cfg.dmzHost != null) '' 114 iptables -w -t nat -A nixos-nat-pre \ 115 -i ${toString cfg.externalInterface} -j DNAT \ 116 --to-destination ${cfg.dmzHost} 117 ''} 118 119 ${cfg.extraCommands} 120 121 # Append our chains to the nat tables 122 ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre 123 ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post 124 ip46tables -w -t nat -A OUTPUT -j nixos-nat-out 125 ''; 126 127in 128 129{ 130 131 ###### interface 132 133 options = { 134 135 networking.nat.enable = mkOption { 136 type = types.bool; 137 default = false; 138 description = 139 '' 140 Whether to enable Network Address Translation (NAT). 141 ''; 142 }; 143 144 networking.nat.enableIPv6 = mkOption { 145 type = types.bool; 146 default = false; 147 description = 148 '' 149 Whether to enable IPv6 NAT. 150 ''; 151 }; 152 153 networking.nat.internalInterfaces = mkOption { 154 type = types.listOf types.str; 155 default = []; 156 example = [ "eth0" ]; 157 description = 158 '' 159 The interfaces for which to perform NAT. Packets coming from 160 these interface and destined for the external interface will 161 be rewritten. 162 ''; 163 }; 164 165 networking.nat.internalIPs = mkOption { 166 type = types.listOf types.str; 167 default = []; 168 example = [ "192.168.1.0/24" ]; 169 description = 170 '' 171 The IP address ranges for which to perform NAT. Packets 172 coming from these addresses (on any interface) and destined 173 for the external interface will be rewritten. 174 ''; 175 }; 176 177 networking.nat.internalIPv6s = mkOption { 178 type = types.listOf types.str; 179 default = []; 180 example = [ "fc00::/64" ]; 181 description = 182 '' 183 The IPv6 address ranges for which to perform NAT. Packets 184 coming from these addresses (on any interface) and destined 185 for the external interface will be rewritten. 186 ''; 187 }; 188 189 networking.nat.externalInterface = mkOption { 190 type = types.nullOr types.str; 191 default = null; 192 example = "eth1"; 193 description = 194 '' 195 The name of the external network interface. 196 ''; 197 }; 198 199 networking.nat.externalIP = mkOption { 200 type = types.nullOr types.str; 201 default = null; 202 example = "203.0.113.123"; 203 description = 204 '' 205 The public IP address to which packets from the local 206 network are to be rewritten. If this is left empty, the 207 IP address associated with the external interface will be 208 used. 209 ''; 210 }; 211 212 networking.nat.externalIPv6 = mkOption { 213 type = types.nullOr types.str; 214 default = null; 215 example = "2001:dc0:2001:11::175"; 216 description = 217 '' 218 The public IPv6 address to which packets from the local 219 network are to be rewritten. If this is left empty, the 220 IP address associated with the external interface will be 221 used. 222 ''; 223 }; 224 225 networking.nat.forwardPorts = mkOption { 226 type = with types; listOf (submodule { 227 options = { 228 sourcePort = mkOption { 229 type = types.either types.int (types.strMatching "[[:digit:]]+:[[:digit:]]+"); 230 example = 8080; 231 description = "Source port of the external interface; to specify a port range, use a string with a colon (e.g. \"60000:61000\")"; 232 }; 233 234 destination = mkOption { 235 type = types.str; 236 example = "10.0.0.1:80"; 237 description = "Forward connection to destination ip:port (or [ipv6]:port); to specify a port range, use ip:start-end"; 238 }; 239 240 proto = mkOption { 241 type = types.str; 242 default = "tcp"; 243 example = "udp"; 244 description = "Protocol of forwarded connection"; 245 }; 246 247 loopbackIPs = mkOption { 248 type = types.listOf types.str; 249 default = []; 250 example = literalExample ''[ "55.1.2.3" ]''; 251 description = "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT"; 252 }; 253 }; 254 }); 255 default = []; 256 example = [ 257 { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; } 258 { sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; } 259 ]; 260 description = 261 '' 262 List of forwarded ports from the external interface to 263 internal destinations by using DNAT. Destination can be 264 IPv6 if IPv6 NAT is enabled. 265 ''; 266 }; 267 268 networking.nat.dmzHost = mkOption { 269 type = types.nullOr types.str; 270 default = null; 271 example = "10.0.0.1"; 272 description = 273 '' 274 The local IP address to which all traffic that does not match any 275 forwarding rule is forwarded. 276 ''; 277 }; 278 279 networking.nat.extraCommands = mkOption { 280 type = types.lines; 281 default = ""; 282 example = "iptables -A INPUT -p icmp -j ACCEPT"; 283 description = 284 '' 285 Additional shell commands executed as part of the nat 286 initialisation script. 287 ''; 288 }; 289 290 networking.nat.extraStopCommands = mkOption { 291 type = types.lines; 292 default = ""; 293 example = "iptables -D INPUT -p icmp -j ACCEPT || true"; 294 description = 295 '' 296 Additional shell commands executed as part of the nat 297 teardown script. 298 ''; 299 }; 300 301 }; 302 303 304 ###### implementation 305 306 config = mkMerge [ 307 { networking.firewall.extraCommands = mkBefore flushNat; } 308 (mkIf config.networking.nat.enable { 309 310 assertions = [ 311 { assertion = cfg.enableIPv6 -> config.networking.enableIPv6; 312 message = "networking.nat.enableIPv6 requires networking.enableIPv6"; 313 } 314 { assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null); 315 message = "networking.nat.dmzHost requires networking.nat.externalInterface"; 316 } 317 { assertion = (cfg.forwardPorts != []) -> (cfg.externalInterface != null); 318 message = "networking.nat.forwardPorts requires networking.nat.externalInterface"; 319 } 320 ]; 321 322 environment.systemPackages = [ pkgs.iptables ]; 323 324 boot = { 325 kernelModules = [ "nf_nat_ftp" ]; 326 kernel.sysctl = { 327 "net.ipv4.conf.all.forwarding" = mkOverride 99 true; 328 "net.ipv4.conf.default.forwarding" = mkOverride 99 true; 329 } // optionalAttrs cfg.enableIPv6 { 330 # Do not prevent IPv6 autoconfiguration. 331 # See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>. 332 "net.ipv6.conf.all.accept_ra" = mkOverride 99 2; 333 "net.ipv6.conf.default.accept_ra" = mkOverride 99 2; 334 335 # Forward IPv6 packets. 336 "net.ipv6.conf.all.forwarding" = mkOverride 99 true; 337 "net.ipv6.conf.default.forwarding" = mkOverride 99 true; 338 }; 339 }; 340 341 networking.firewall = mkIf config.networking.firewall.enable { 342 extraCommands = setupNat; 343 extraStopCommands = flushNat; 344 }; 345 346 systemd.services = mkIf (!config.networking.firewall.enable) { nat = { 347 description = "Network Address Translation"; 348 wantedBy = [ "network.target" ]; 349 after = [ "network-pre.target" "systemd-modules-load.service" ]; 350 path = [ pkgs.iptables ]; 351 unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 352 353 serviceConfig = { 354 Type = "oneshot"; 355 RemainAfterExit = true; 356 }; 357 358 script = flushNat + setupNat; 359 360 postStop = flushNat; 361 }; }; 362 }) 363 ]; 364}