1/* This module enables a simple firewall.
2
3 The firewall can be customised in arbitrary ways by setting
4 ‘networking.firewall.extraCommands’. For modularity, the firewall
5 uses several chains:
6
7 - ‘nixos-fw-input’ is the main chain for input packet processing.
8
9 - ‘nixos-fw-log-refuse’ and ‘nixos-fw-refuse’ are called for
10 refused packets. (The former jumps to the latter after logging
11 the packet.) If you want additional logging, or want to accept
12 certain packets anyway, you can insert rules at the start of
13 these chain.
14
15 - ‘nixos-fw-accept’ is called for accepted packets. If you want
16 additional logging, or want to reject certain packets anyway, you
17 can insert rules at the start of this chain.
18
19*/
20
21{ config, lib, pkgs, ... }:
22
23with lib;
24
25let
26
27 cfg = config.networking.firewall;
28
29 helpers =
30 ''
31 # Helper command to manipulate both the IPv4 and IPv6 tables.
32 ip46tables() {
33 iptables -w "$@"
34 ${optionalString config.networking.enableIPv6 ''
35 ip6tables -w "$@"
36 ''}
37 }
38 '';
39
40 writeShScript = name: text: let dir = pkgs.writeScriptBin name ''
41 #! ${pkgs.stdenv.shell} -e
42 ${text}
43 ''; in "${dir}/bin/${name}";
44
45 startScript = writeShScript "firewall-start" ''
46 ${helpers}
47
48 # Flush the old firewall rules. !!! Ideally, updating the
49 # firewall would be atomic. Apparently that's possible
50 # with iptables-restore.
51 ip46tables -D INPUT -j nixos-fw 2> /dev/null || true
52 for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse FW_REFUSE; do
53 ip46tables -F "$chain" 2> /dev/null || true
54 ip46tables -X "$chain" 2> /dev/null || true
55 done
56
57
58 # The "nixos-fw-accept" chain just accepts packets.
59 ip46tables -N nixos-fw-accept
60 ip46tables -A nixos-fw-accept -j ACCEPT
61
62
63 # The "nixos-fw-refuse" chain rejects or drops packets.
64 ip46tables -N nixos-fw-refuse
65
66 ${if cfg.rejectPackets then ''
67 # Send a reset for existing TCP connections that we've
68 # somehow forgotten about. Send ICMP "port unreachable"
69 # for everything else.
70 ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset
71 ip46tables -A nixos-fw-refuse -j REJECT
72 '' else ''
73 ip46tables -A nixos-fw-refuse -j DROP
74 ''}
75
76
77 # The "nixos-fw-log-refuse" chain performs logging, then
78 # jumps to the "nixos-fw-refuse" chain.
79 ip46tables -N nixos-fw-log-refuse
80
81 ${optionalString cfg.logRefusedConnections ''
82 ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "rejected connection: "
83 ''}
84 ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
85 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \
86 -j LOG --log-level info --log-prefix "rejected broadcast: "
87 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \
88 -j LOG --log-level info --log-prefix "rejected multicast: "
89 ''}
90 ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
91 ${optionalString cfg.logRefusedPackets ''
92 ip46tables -A nixos-fw-log-refuse \
93 -j LOG --log-level info --log-prefix "rejected packet: "
94 ''}
95 ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse
96
97
98 # The "nixos-fw" chain does the actual work.
99 ip46tables -N nixos-fw
100
101 # Perform a reverse-path test to refuse spoofers
102 # For now, we just drop, as the raw table doesn't have a log-refuse yet
103 ${optionalString (kernelHasRPFilter && cfg.checkReversePath) ''
104 # Clean up rpfilter rules
105 ip46tables -t raw -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
106 ip46tables -t raw -F nixos-fw-rpfilter 2> /dev/null || true
107 ip46tables -t raw -N nixos-fw-rpfilter 2> /dev/null || true
108
109 ip46tables -t raw -A nixos-fw-rpfilter -m rpfilter -j RETURN
110
111 # Allows this host to act as a DHCPv4 server
112 iptables -t raw -A nixos-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN
113
114 ${optionalString cfg.logReversePathDrops ''
115 ip46tables -t raw -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
116 ''}
117 ip46tables -t raw -A nixos-fw-rpfilter -j DROP
118
119 ip46tables -t raw -A PREROUTING -j nixos-fw-rpfilter
120 ''}
121
122 # Accept all traffic on the trusted interfaces.
123 ${flip concatMapStrings cfg.trustedInterfaces (iface: ''
124 ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept
125 '')}
126
127 # Accept packets from established or related connections.
128 ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept
129
130 # Accept connections to the allowed TCP ports.
131 ${concatMapStrings (port:
132 ''
133 ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept
134 ''
135 ) cfg.allowedTCPPorts
136 }
137
138 # Accept connections to the allowed TCP port ranges.
139 ${concatMapStrings (rangeAttr:
140 let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
141 ''
142 ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept
143 ''
144 ) cfg.allowedTCPPortRanges
145 }
146
147 # Accept packets on the allowed UDP ports.
148 ${concatMapStrings (port:
149 ''
150 ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept
151 ''
152 ) cfg.allowedUDPPorts
153 }
154
155 # Accept packets on the allowed UDP port ranges.
156 ${concatMapStrings (rangeAttr:
157 let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
158 ''
159 ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept
160 ''
161 ) cfg.allowedUDPPortRanges
162 }
163
164 # Accept IPv4 multicast. Not a big security risk since
165 # probably nobody is listening anyway.
166 #iptables -A nixos-fw -d 224.0.0.0/4 -j nixos-fw-accept
167
168 # Optionally respond to ICMPv4 pings.
169 ${optionalString cfg.allowPing ''
170 iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null)
171 "-m limit ${cfg.pingLimit} "
172 }-j nixos-fw-accept
173 ''}
174
175 # Accept all ICMPv6 messages except redirects and node
176 # information queries (type 139). See RFC 4890, section
177 # 4.4.
178 ${optionalString config.networking.enableIPv6 ''
179 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
180 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
181 ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
182 ''}
183
184 ${cfg.extraCommands}
185
186 # Reject/drop everything else.
187 ip46tables -A nixos-fw -j nixos-fw-log-refuse
188
189
190 # Enable the firewall.
191 ip46tables -A INPUT -j nixos-fw
192 '';
193
194 stopScript = writeShScript "firewall-stop" ''
195 ${helpers}
196
197 # Clean up in case reload fails
198 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
199
200 # Clean up after added ruleset
201 ip46tables -D INPUT -j nixos-fw 2>/dev/null || true
202
203 ${optionalString (kernelHasRPFilter && cfg.checkReversePath) ''
204 ip46tables -t raw -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true
205 ''}
206
207 ${cfg.extraStopCommands}
208 '';
209
210 reloadScript = writeShScript "firewall-reload" ''
211 ${helpers}
212
213 # Create a unique drop rule
214 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
215 ip46tables -F nixos-drop 2>/dev/null || true
216 ip46tables -X nixos-drop 2>/dev/null || true
217 ip46tables -N nixos-drop
218 ip46tables -A nixos-drop -j DROP
219
220 # Don't allow traffic to leak out until the script has completed
221 ip46tables -A INPUT -j nixos-drop
222 if ${startScript}; then
223 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
224 else
225 echo "Failed to reload firewall... Stopping"
226 ${stopScript}
227 exit 1
228 fi
229 '';
230
231 kernelPackages = config.boot.kernelPackages;
232
233 kernelHasRPFilter = kernelPackages.kernel.features.netfilterRPFilter or false;
234 kernelCanDisableHelpers = kernelPackages.kernel.features.canDisableNetfilterConntrackHelpers or false;
235
236in
237
238{
239
240 ###### interface
241
242 options = {
243
244 networking.firewall.enable = mkOption {
245 type = types.bool;
246 default = true;
247 description =
248 ''
249 Whether to enable the firewall. This is a simple stateful
250 firewall that blocks connection attempts to unauthorised TCP
251 or UDP ports on this machine. It does not affect packet
252 forwarding.
253 '';
254 };
255
256 networking.firewall.logRefusedConnections = mkOption {
257 type = types.bool;
258 default = true;
259 description =
260 ''
261 Whether to log rejected or dropped incoming connections.
262 '';
263 };
264
265 networking.firewall.logRefusedPackets = mkOption {
266 type = types.bool;
267 default = false;
268 description =
269 ''
270 Whether to log all rejected or dropped incoming packets.
271 This tends to give a lot of log messages, so it's mostly
272 useful for debugging.
273 '';
274 };
275
276 networking.firewall.logRefusedUnicastsOnly = mkOption {
277 type = types.bool;
278 default = true;
279 description =
280 ''
281 If <option>networking.firewall.logRefusedPackets</option>
282 and this option are enabled, then only log packets
283 specifically directed at this machine, i.e., not broadcasts
284 or multicasts.
285 '';
286 };
287
288 networking.firewall.rejectPackets = mkOption {
289 type = types.bool;
290 default = false;
291 description =
292 ''
293 If set, forbidden packets are rejected rather than dropped
294 (ignored). This means that an ICMP "port unreachable" error
295 message is sent back to the client. Rejecting packets makes
296 port scanning somewhat easier.
297 '';
298 };
299
300 networking.firewall.trustedInterfaces = mkOption {
301 type = types.listOf types.str;
302 description =
303 ''
304 Traffic coming in from these interfaces will be accepted
305 unconditionally.
306 '';
307 };
308
309 networking.firewall.allowedTCPPorts = mkOption {
310 default = [];
311 example = [ 22 80 ];
312 type = types.listOf types.int;
313 description =
314 ''
315 List of TCP ports on which incoming connections are
316 accepted.
317 '';
318 };
319
320 networking.firewall.allowedTCPPortRanges = mkOption {
321 default = [];
322 example = [ { from = 8999; to = 9003; } ];
323 type = types.listOf (types.attrsOf types.int);
324 description =
325 ''
326 A range of TCP ports on which incoming connections are
327 accepted.
328 '';
329 };
330
331 networking.firewall.allowedUDPPorts = mkOption {
332 default = [];
333 example = [ 53 ];
334 type = types.listOf types.int;
335 description =
336 ''
337 List of open UDP ports.
338 '';
339 };
340
341 networking.firewall.allowedUDPPortRanges = mkOption {
342 default = [];
343 example = [ { from = 60000; to = 61000; } ];
344 type = types.listOf (types.attrsOf types.int);
345 description =
346 ''
347 Range of open UDP ports.
348 '';
349 };
350
351 networking.firewall.allowPing = mkOption {
352 default = true;
353 type = types.bool;
354 description =
355 ''
356 Whether to respond to incoming ICMPv4 echo requests
357 ("pings"). ICMPv6 pings are always allowed because the
358 larger address space of IPv6 makes network scanning much
359 less effective.
360 '';
361 };
362
363 networking.firewall.pingLimit = mkOption {
364 default = null;
365 type = types.nullOr (types.separatedString " ");
366 description =
367 ''
368 If pings are allowed, this allows setting rate limits
369 on them. If non-null, this option should be in the form
370 of flags like "--limit 1/minute --limit-burst 5"
371 '';
372 };
373
374 networking.firewall.checkReversePath = mkOption {
375 default = kernelHasRPFilter;
376 type = types.bool;
377 description =
378 ''
379 Performs a reverse path filter test on a packet.
380 If a reply to the packet would not be sent via the same interface
381 that the packet arrived on, it is refused.
382
383 If using asymmetric routing or other complicated routing,
384 disable this setting and setup your own counter-measures.
385
386 (needs kernel 3.3+)
387 '';
388 };
389
390 networking.firewall.logReversePathDrops = mkOption {
391 default = false;
392 type = types.bool;
393 description =
394 ''
395 Logs dropped packets failing the reverse path filter test if
396 the option networking.firewall.checkReversePath is enabled.
397 '';
398 };
399
400 networking.firewall.connectionTrackingModules = mkOption {
401 default = [ "ftp" ];
402 example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ];
403 type = types.listOf types.str;
404 description =
405 ''
406 List of connection-tracking helpers that are auto-loaded.
407 The complete list of possible values is given in the example.
408
409 As helpers can pose as a security risk, it is advised to
410 set this to an empty list and disable the setting
411 networking.firewall.autoLoadConntrackHelpers
412
413 Loading of helpers is recommended to be done through the new
414 CT target. More info:
415 https://home.regit.org/netfilter-en/secure-use-of-helpers/
416 '';
417 };
418
419 networking.firewall.autoLoadConntrackHelpers = mkOption {
420 default = true;
421 type = types.bool;
422 description =
423 ''
424 Whether to auto-load connection-tracking helpers.
425 See the description at networking.firewall.connectionTrackingModules
426
427 (needs kernel 3.5+)
428 '';
429 };
430
431 networking.firewall.extraCommands = mkOption {
432 type = types.lines;
433 default = "";
434 example = "iptables -A INPUT -p icmp -j ACCEPT";
435 description =
436 ''
437 Additional shell commands executed as part of the firewall
438 initialisation script. These are executed just before the
439 final "reject" firewall rule is added, so they can be used
440 to allow packets that would otherwise be refused.
441 '';
442 };
443
444 networking.firewall.extraPackages = mkOption {
445 type = types.listOf types.package;
446 default = [ ];
447 example = literalExample "[ pkgs.ipset ]";
448 description =
449 ''
450 Additional packages to be included in the environment of the system
451 as well as the path of networking.firewall.extraCommands.
452 '';
453 };
454
455 networking.firewall.extraStopCommands = mkOption {
456 type = types.lines;
457 default = "";
458 example = "iptables -P INPUT ACCEPT";
459 description =
460 ''
461 Additional shell commands executed as part of the firewall
462 shutdown script. These are executed just after the removal
463 of the nixos input rule, or if the service enters a failed state.
464 '';
465 };
466
467 };
468
469
470 ###### implementation
471
472 # FIXME: Maybe if `enable' is false, the firewall should still be
473 # built but not started by default?
474 config = mkIf cfg.enable {
475
476 networking.firewall.trustedInterfaces = [ "lo" ];
477
478 environment.systemPackages = [ pkgs.iptables ] ++ cfg.extraPackages;
479
480 boot.kernelModules = map (x: "nf_conntrack_${x}") cfg.connectionTrackingModules;
481 boot.extraModprobeConfig = optionalString (!cfg.autoLoadConntrackHelpers) ''
482 options nf_conntrack nf_conntrack_helper=0
483 '';
484
485 assertions = [ { assertion = ! cfg.checkReversePath || kernelHasRPFilter;
486 message = "This kernel does not support rpfilter"; }
487 { assertion = cfg.autoLoadConntrackHelpers || kernelCanDisableHelpers;
488 message = "This kernel does not support disabling conntrack helpers"; }
489 ];
490
491 systemd.services.firewall = {
492 description = "Firewall";
493 wantedBy = [ "network-pre.target" ];
494 before = [ "network-pre.target" ];
495 after = [ "systemd-modules-load.service" ];
496
497 path = [ pkgs.iptables ] ++ cfg.extraPackages;
498
499 # FIXME: this module may also try to load kernel modules, but
500 # containers don't have CAP_SYS_MODULE. So the host system had
501 # better have all necessary modules already loaded.
502 unitConfig.ConditionCapability = "CAP_NET_ADMIN";
503
504 reloadIfChanged = true;
505
506 serviceConfig = {
507 Type = "oneshot";
508 RemainAfterExit = true;
509 ExecStart = "@${startScript} firewall-start";
510 ExecReload = "@${reloadScript} firewall-reload";
511 ExecStop = "@${stopScript} firewall-stop";
512 };
513 };
514
515 };
516
517}