1 # This module enables Network Address Translation (NAT).
2 # XXX: todo: support multiple upstream links
3 # see http://yesican.chsoft.biz/lartc/MultihomedLinuxNetworking.html
5 { config, lib, pkgs, ... }:
10 cfg = config.networking.nat;
12 mkDest = externalIP: if externalIP == null
14 else "-j SNAT --to-source ${externalIP}";
15 dest = mkDest cfg.externalIP;
16 destIPv6 = mkDest cfg.externalIPv6;
18 # Whether given IP (plus optional port) is an IPv6.
19 isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
21 helpers = import ./helpers.nix { inherit config lib; };
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
35 ${cfg.extraStopCommands}
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}
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}
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}
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}
65 ${concatMapStrings (loopbackip:
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);
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}
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}
84 ${iptables} -w -t nat -A nixos-nat-post \
85 -d ${destinationIP} -p ${fwd.proto} \
86 --dport ${destinationPorts} \
87 -j SNAT --to-source ${loopbackip}
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
100 iptables = "iptables";
102 inherit (cfg) internalIPs;
103 forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
106 ${optionalString cfg.enableIPv6 (mkSetupNat {
107 iptables = "ip6tables";
109 internalIPs = cfg.internalIPv6s;
110 forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
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}
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
135 networking.nat.enable = mkOption {
140 Whether to enable Network Address Translation (NAT).
144 networking.nat.enableIPv6 = mkOption {
149 Whether to enable IPv6 NAT.
153 networking.nat.internalInterfaces = mkOption {
154 type = types.listOf types.str;
156 example = [ "eth0" ];
159 The interfaces for which to perform NAT. Packets coming from
160 these interface and destined for the external interface will
165 networking.nat.internalIPs = mkOption {
166 type = types.listOf types.str;
168 example = [ "192.168.1.0/24" ];
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.
177 networking.nat.internalIPv6s = mkOption {
178 type = types.listOf types.str;
180 example = [ "fc00::/64" ];
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.
189 networking.nat.externalInterface = mkOption {
190 type = types.nullOr types.str;
195 The name of the external network interface.
199 networking.nat.externalIP = mkOption {
200 type = types.nullOr types.str;
202 example = "203.0.113.123";
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
212 networking.nat.externalIPv6 = mkOption {
213 type = types.nullOr types.str;
215 example = "2001:dc0:2001:11::175";
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
225 networking.nat.forwardPorts = mkOption {
226 type = with types; listOf (submodule {
228 sourcePort = mkOption {
229 type = types.either types.int (types.strMatching "[[:digit:]]+:[[:digit:]]+");
231 description = lib.mdDoc "Source port of the external interface; to specify a port range, use a string with a colon (e.g. \"60000:61000\")";
234 destination = mkOption {
236 example = "10.0.0.1:80";
237 description = lib.mdDoc "Forward connection to destination ip:port (or [ipv6]:port); to specify a port range, use ip:start-end";
244 description = lib.mdDoc "Protocol of forwarded connection";
247 loopbackIPs = mkOption {
248 type = types.listOf types.str;
250 example = literalExpression ''[ "55.1.2.3" ]'';
251 description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT";
257 { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; }
258 { sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; }
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.
268 networking.nat.dmzHost = mkOption {
269 type = types.nullOr types.str;
271 example = "10.0.0.1";
274 The local IP address to which all traffic that does not match any
275 forwarding rule is forwarded.
279 networking.nat.extraCommands = mkOption {
282 example = "iptables -A INPUT -p icmp -j ACCEPT";
285 Additional shell commands executed as part of the nat
286 initialisation script.
290 networking.nat.extraStopCommands = mkOption {
293 example = "iptables -D INPUT -p icmp -j ACCEPT || true";
296 Additional shell commands executed as part of the nat
304 ###### implementation
307 { networking.firewall.extraCommands = mkBefore flushNat; }
308 (mkIf config.networking.nat.enable {
311 { assertion = cfg.enableIPv6 -> config.networking.enableIPv6;
312 message = "networking.nat.enableIPv6 requires networking.enableIPv6";
314 { assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null);
315 message = "networking.nat.dmzHost requires networking.nat.externalInterface";
317 { assertion = (cfg.forwardPorts != []) -> (cfg.externalInterface != null);
318 message = "networking.nat.forwardPorts requires networking.nat.externalInterface";
322 # Use the same iptables package as in config.networking.firewall.
323 # When the firewall is enabled, this should be deduplicated without any
325 environment.systemPackages = [ config.networking.firewall.package ];
328 kernelModules = [ "nf_nat_ftp" ];
330 "net.ipv4.conf.all.forwarding" = mkOverride 99 true;
331 "net.ipv4.conf.default.forwarding" = mkOverride 99 true;
332 } // optionalAttrs cfg.enableIPv6 {
333 # Do not prevent IPv6 autoconfiguration.
334 # See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>.
335 "net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
336 "net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
338 # Forward IPv6 packets.
339 "net.ipv6.conf.all.forwarding" = mkOverride 99 true;
340 "net.ipv6.conf.default.forwarding" = mkOverride 99 true;
344 networking.firewall = mkIf config.networking.firewall.enable {
345 extraCommands = setupNat;
346 extraStopCommands = flushNat;
349 systemd.services = mkIf (!config.networking.firewall.enable) { nat = {
350 description = "Network Address Translation";
351 wantedBy = [ "network.target" ];
352 after = [ "network-pre.target" "systemd-modules-load.service" ];
353 path = [ config.networking.firewall.package ];
354 unitConfig.ConditionCapability = "CAP_NET_ADMIN";
358 RemainAfterExit = true;
361 script = flushNat + setupNat;