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{
6 config,
7 lib,
8 pkgs,
9 ...
10}:
11
12with lib;
13
14let
15 cfg = config.networking.nat;
16
17 mkDest =
18 externalIP: if externalIP == null then "-j MASQUERADE" else "-j SNAT --to-source ${externalIP}";
19 dest = mkDest cfg.externalIP;
20 destIPv6 = mkDest cfg.externalIPv6;
21
22 # Whether given IP (plus optional port) is an IPv6.
23 isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
24
25 helpers = import ./helpers.nix { inherit config lib; };
26
27 flushNat = ''
28 ${helpers}
29 ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
30 ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true
31 ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true
32 ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
33 ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true
34 ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true
35 ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true
36 ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true
37 ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true
38 ip46tables -w -t filter -D FORWARD -j nixos-filter-forward 2>/dev/null || true
39 ip46tables -w -t filter -F nixos-filter-forward 2>/dev/null || true
40 ip46tables -w -t filter -X nixos-filter-forward 2>/dev/null || true
41
42 ${cfg.extraStopCommands}
43 '';
44
45 mkSetupNat =
46 {
47 iptables,
48 dest,
49 internalIPs,
50 forwardPorts,
51 externalIp,
52 }:
53 ''
54 # We can't match on incoming interface in POSTROUTING, so
55 # mark packets coming from the internal interfaces.
56 ${concatMapStrings (iface: ''
57 ${iptables} -w -t nat -A nixos-nat-pre \
58 -i '${iface}' -j MARK --set-mark 1
59 ${iptables} -w -t filter -A nixos-filter-forward \
60 -i '${iface}' ${
61 optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"
62 } -j ACCEPT
63 '') cfg.internalInterfaces}
64
65 # NAT the marked packets.
66 ${optionalString (cfg.internalInterfaces != [ ]) ''
67 ${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \
68 ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
69 ''}
70
71 # NAT packets coming from the internal IPs.
72 ${concatMapStrings (range: ''
73 ${iptables} -w -t nat -A nixos-nat-post \
74 -s '${range}' ${
75 optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"
76 } ${dest}
77 ${iptables} -w -t filter -A nixos-filter-forward \
78 -s '${range}' ${
79 optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"
80 } -j ACCEPT
81 '') internalIPs}
82
83 # Related connections are allowed
84 ${iptables} -w -t filter -A nixos-filter-forward \
85 -m state --state ESTABLISHED,RELATED -j ACCEPT
86
87 # NAT from external ports to internal ports.
88 ${concatMapStrings (fwd: ''
89 ${iptables} -w -t nat -A nixos-nat-pre \
90 -i ${toString cfg.externalInterface} -p ${fwd.proto} \
91 ${
92 optionalString (externalIp != null) "-d ${externalIp}"
93 } --dport ${builtins.toString fwd.sourcePort} \
94 -j DNAT --to-destination ${fwd.destination}
95 ${iptables} -w -t filter -A nixos-filter-forward \
96 -i ${toString cfg.externalInterface} -p ${fwd.proto} \
97 --dport ${builtins.toString fwd.sourcePort} -j ACCEPT
98
99 ${concatMapStrings (
100 loopbackip:
101 let
102 matchIP = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
103 m = builtins.match "${matchIP}:([0-9-]+)" fwd.destination;
104 destinationIP = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
105 destinationPorts =
106 if m == null then
107 throw "bad ip:ports `${fwd.destination}'"
108 else
109 builtins.replaceStrings [ "-" ] [ ":" ] (elemAt m 1);
110 in
111 ''
112 # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
113 ${iptables} -w -t nat -A nixos-nat-out \
114 -d ${loopbackip} -p ${fwd.proto} \
115 --dport ${builtins.toString fwd.sourcePort} \
116 -j DNAT --to-destination ${fwd.destination}
117
118 # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT
119 ${concatMapStrings (range: ''
120 ${iptables} -w -t nat -A nixos-nat-pre \
121 -d ${loopbackip} -p ${fwd.proto} -s '${range}' \
122 --dport ${builtins.toString fwd.sourcePort} \
123 -j DNAT --to-destination ${fwd.destination}
124 ${iptables} -w -t nat -A nixos-nat-post \
125 -d ${destinationIP} -p ${fwd.proto} \
126 -s '${range}' --dport ${destinationPorts} \
127 -j SNAT --to-source ${loopbackip}
128 ${iptables} -w -t filter -A nixos-filter-forward \
129 -d ${destinationIP} -p ${fwd.proto} \
130 -s '${range}' --dport ${destinationPorts} -j ACCEPT
131 '') internalIPs}
132 ${concatMapStrings (iface: ''
133 ${iptables} -w -t nat -A nixos-nat-pre \
134 -d ${loopbackip} -p ${fwd.proto} -i '${iface}' \
135 --dport ${builtins.toString fwd.sourcePort} \
136 -j DNAT --to-destination ${fwd.destination}
137 ${iptables} -w -t nat -A nixos-nat-post \
138 -d ${destinationIP} -p ${fwd.proto} \
139 -i '${iface}' --dport ${destinationPorts} \
140 -j SNAT --to-source ${loopbackip}
141 ${iptables} -w -t filter -A nixos-filter-forward \
142 -d ${destinationIP} -p ${fwd.proto} \
143 -i '${iface}' --dport ${destinationPorts} -j ACCEPT
144 '') cfg.internalInterfaces}
145 ''
146 ) fwd.loopbackIPs}
147 '') forwardPorts}
148 '';
149
150 setupNat = ''
151 ${helpers}
152 # Create subchains where we store rules
153 ip46tables -w -t nat -N nixos-nat-pre
154 ip46tables -w -t nat -N nixos-nat-post
155 ip46tables -w -t nat -N nixos-nat-out
156 ip46tables -w -t filter -N nixos-filter-forward
157
158 ${mkSetupNat {
159 iptables = "iptables";
160 inherit dest;
161 inherit (cfg) internalIPs;
162 forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
163 externalIp = cfg.externalIP;
164 }}
165
166 ${optionalString cfg.enableIPv6 (mkSetupNat {
167 iptables = "ip6tables";
168 dest = destIPv6;
169 internalIPs = cfg.internalIPv6s;
170 forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
171 externalIp = cfg.externalIPv6;
172 })}
173
174 ${optionalString (cfg.dmzHost != null) ''
175 iptables -w -t nat -A nixos-nat-pre \
176 -i ${toString cfg.externalInterface} -j DNAT \
177 --to-destination ${cfg.dmzHost}
178 ''}
179
180 ${cfg.extraCommands}
181
182 # Append our chains to the nat tables
183 ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre
184 ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post
185 ip46tables -w -t nat -A OUTPUT -j nixos-nat-out
186 ip46tables -w -t filter -A FORWARD -j nixos-filter-forward
187 '';
188
189in
190
191{
192
193 options = {
194
195 networking.nat.extraCommands = mkOption {
196 type = types.lines;
197 default = "";
198 example = "iptables -A INPUT -p icmp -j ACCEPT";
199 description = ''
200 Additional shell commands executed as part of the nat
201 initialisation script.
202
203 This option is incompatible with the nftables based nat module.
204 '';
205 };
206
207 networking.nat.extraStopCommands = mkOption {
208 type = types.lines;
209 default = "";
210 example = "iptables -D INPUT -p icmp -j ACCEPT || true";
211 description = ''
212 Additional shell commands executed as part of the nat
213 teardown script.
214
215 This option is incompatible with the nftables based nat module.
216 '';
217 };
218
219 };
220
221 config = mkIf (!config.networking.nftables.enable) (mkMerge [
222 ({ networking.firewall.extraCommands = mkBefore flushNat; })
223 (mkIf config.networking.nat.enable {
224
225 networking.firewall = mkIf config.networking.firewall.enable {
226 extraCommands = setupNat;
227 extraStopCommands = flushNat;
228 };
229
230 systemd.services = mkIf (!config.networking.firewall.enable) {
231 nat = {
232 description = "Network Address Translation";
233 wantedBy = [ "network.target" ];
234 after = [
235 "network-pre.target"
236 "systemd-modules-load.service"
237 ];
238 path = [ config.networking.firewall.package ];
239 unitConfig.ConditionCapability = "CAP_NET_ADMIN";
240
241 serviceConfig = {
242 Type = "oneshot";
243 RemainAfterExit = true;
244 };
245
246 script = flushNat + setupNat;
247
248 postStop = flushNat;
249 };
250 };
251 })
252 ]);
253}