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