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}