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
11 cfg = config.networking.nat;
12
13 dest = if cfg.externalIP == null then "-j MASQUERADE" else "-j SNAT --to-source ${cfg.externalIP}";
14
15 flushNat = ''
16 iptables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
17 iptables -w -t nat -F nixos-nat-pre 2>/dev/null || true
18 iptables -w -t nat -X nixos-nat-pre 2>/dev/null || true
19 iptables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
20 iptables -w -t nat -F nixos-nat-post 2>/dev/null || true
21 iptables -w -t nat -X nixos-nat-post 2>/dev/null || true
22
23 ${cfg.extraStopCommands}
24 '';
25
26 setupNat = ''
27 # Create subchain where we store rules
28 iptables -w -t nat -N nixos-nat-pre
29 iptables -w -t nat -N nixos-nat-post
30
31 # We can't match on incoming interface in POSTROUTING, so
32 # mark packets coming from the external interfaces.
33 ${concatMapStrings (iface: ''
34 iptables -w -t nat -A nixos-nat-pre \
35 -i '${iface}' -j MARK --set-mark 1
36 '') cfg.internalInterfaces}
37
38 # NAT the marked packets.
39 ${optionalString (cfg.internalInterfaces != []) ''
40 iptables -w -t nat -A nixos-nat-post -m mark --mark 1 \
41 -o ${cfg.externalInterface} ${dest}
42 ''}
43
44 # NAT packets coming from the internal IPs.
45 ${concatMapStrings (range: ''
46 iptables -w -t nat -A nixos-nat-post \
47 -s '${range}' -o ${cfg.externalInterface} ${dest}
48 '') cfg.internalIPs}
49
50 # NAT from external ports to internal ports.
51 ${concatMapStrings (fwd: ''
52 iptables -w -t nat -A nixos-nat-pre \
53 -i ${cfg.externalInterface} -p ${fwd.proto} \
54 --dport ${builtins.toString fwd.sourcePort} \
55 -j DNAT --to-destination ${fwd.destination}
56
57 ${concatMapStrings (loopbackip:
58 let
59 m = builtins.match "([0-9.]+):([0-9-]+)" fwd.destination;
60 destinationIP = if (m == null) then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
61 destinationPorts = if (m == null) then throw "bad ip:ports `${fwd.destination}'" else elemAt m 1;
62 in ''
63 # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
64 iptables -w -t nat -A OUTPUT \
65 -d ${loopbackip} -p ${fwd.proto} \
66 --dport ${builtins.toString fwd.sourcePort} \
67 -j DNAT --to-destination ${fwd.destination}
68
69 # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT
70 iptables -w -t nat -A nixos-nat-pre \
71 -d ${loopbackip} -p ${fwd.proto} \
72 --dport ${builtins.toString fwd.sourcePort} \
73 -j DNAT --to-destination ${fwd.destination}
74
75 iptables -w -t nat -A nixos-nat-post \
76 -d ${destinationIP} -p ${fwd.proto} \
77 --dport ${destinationPorts} \
78 -j SNAT --to-source ${loopbackip}
79 '') fwd.loopbackIPs}
80 '') cfg.forwardPorts}
81
82 ${optionalString (cfg.dmzHost != null) ''
83 iptables -w -t nat -A nixos-nat-pre \
84 -i ${cfg.externalInterface} -j DNAT \
85 --to-destination ${cfg.dmzHost}
86 ''}
87
88 ${cfg.extraCommands}
89
90 # Append our chains to the nat tables
91 iptables -w -t nat -A PREROUTING -j nixos-nat-pre
92 iptables -w -t nat -A POSTROUTING -j nixos-nat-post
93 '';
94
95in
96
97{
98
99 ###### interface
100
101 options = {
102
103 networking.nat.enable = mkOption {
104 type = types.bool;
105 default = false;
106 description =
107 ''
108 Whether to enable Network Address Translation (NAT).
109 '';
110 };
111
112 networking.nat.internalInterfaces = mkOption {
113 type = types.listOf types.str;
114 default = [];
115 example = [ "eth0" ];
116 description =
117 ''
118 The interfaces for which to perform NAT. Packets coming from
119 these interface and destined for the external interface will
120 be rewritten.
121 '';
122 };
123
124 networking.nat.internalIPs = mkOption {
125 type = types.listOf types.str;
126 default = [];
127 example = [ "192.168.1.0/24" ];
128 description =
129 ''
130 The IP address ranges for which to perform NAT. Packets
131 coming from these addresses (on any interface) and destined
132 for the external interface will be rewritten.
133 '';
134 };
135
136 networking.nat.externalInterface = mkOption {
137 type = types.str;
138 example = "eth1";
139 description =
140 ''
141 The name of the external network interface.
142 '';
143 };
144
145 networking.nat.externalIP = mkOption {
146 type = types.nullOr types.str;
147 default = null;
148 example = "203.0.113.123";
149 description =
150 ''
151 The public IP address to which packets from the local
152 network are to be rewritten. If this is left empty, the
153 IP address associated with the external interface will be
154 used.
155 '';
156 };
157
158 networking.nat.forwardPorts = mkOption {
159 type = with types; listOf (submodule {
160 options = {
161 sourcePort = mkOption {
162 type = types.either types.int (types.strMatching "[[:digit:]]+:[[:digit:]]+");
163 example = 8080;
164 description = "Source port of the external interface; to specify a port range, use a string with a colon (e.g. \"60000:61000\")";
165 };
166
167 destination = mkOption {
168 type = types.str;
169 example = "10.0.0.1:80";
170 description = "Forward connection to destination ip:port; to specify a port range, use ip:start-end";
171 };
172
173 proto = mkOption {
174 type = types.str;
175 default = "tcp";
176 example = "udp";
177 description = "Protocol of forwarded connection";
178 };
179
180 loopbackIPs = mkOption {
181 type = types.listOf types.str;
182 default = [];
183 example = literalExample ''[ "55.1.2.3" ]'';
184 description = "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT";
185 };
186 };
187 });
188 default = [];
189 example = [ { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; } ];
190 description =
191 ''
192 List of forwarded ports from the external interface to
193 internal destinations by using DNAT.
194 '';
195 };
196
197 networking.nat.dmzHost = mkOption {
198 type = types.nullOr types.str;
199 default = null;
200 example = "10.0.0.1";
201 description =
202 ''
203 The local IP address to which all traffic that does not match any
204 forwarding rule is forwarded.
205 '';
206 };
207
208 networking.nat.extraCommands = mkOption {
209 type = types.lines;
210 default = "";
211 example = "iptables -A INPUT -p icmp -j ACCEPT";
212 description =
213 ''
214 Additional shell commands executed as part of the nat
215 initialisation script.
216 '';
217 };
218
219 networking.nat.extraStopCommands = mkOption {
220 type = types.lines;
221 default = "";
222 example = "iptables -D INPUT -p icmp -j ACCEPT || true";
223 description =
224 ''
225 Additional shell commands executed as part of the nat
226 teardown script.
227 '';
228 };
229
230 };
231
232
233 ###### implementation
234
235 config = mkMerge [
236 { networking.firewall.extraCommands = mkBefore flushNat; }
237 (mkIf config.networking.nat.enable {
238
239 environment.systemPackages = [ pkgs.iptables ];
240
241 boot = {
242 kernelModules = [ "nf_nat_ftp" ];
243 kernel.sysctl = {
244 "net.ipv4.conf.all.forwarding" = mkOverride 99 true;
245 "net.ipv4.conf.default.forwarding" = mkOverride 99 true;
246 };
247 };
248
249 networking.firewall = mkIf config.networking.firewall.enable {
250 extraCommands = setupNat;
251 extraStopCommands = flushNat;
252 };
253
254 systemd.services = mkIf (!config.networking.firewall.enable) { nat = {
255 description = "Network Address Translation";
256 wantedBy = [ "network.target" ];
257 after = [ "network-pre.target" "systemd-modules-load.service" ];
258 path = [ pkgs.iptables ];
259 unitConfig.ConditionCapability = "CAP_NET_ADMIN";
260
261 serviceConfig = {
262 Type = "oneshot";
263 RemainAfterExit = true;
264 };
265
266 script = flushNat + setupNat;
267
268 postStop = flushNat;
269 }; };
270 })
271 ];
272}