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