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 = 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 raw -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
113 ip46tables -t raw -F nixos-fw-rpfilter 2> /dev/null || true
114 ip46tables -t raw -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 raw table doesn't have a log-refuse yet
119 ip46tables -t raw -N nixos-fw-rpfilter 2> /dev/null || true
120 ip46tables -t raw -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 raw -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 raw -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 raw -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
130 ''}
131 ip46tables -t raw -A nixos-fw-rpfilter -j DROP
132
133 ip46tables -t raw -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 # Accept IPv4 multicast. Not a big security risk since
183 # probably nobody is listening anyway.
184 #iptables -A nixos-fw -d 224.0.0.0/4 -j nixos-fw-accept
185
186 # Optionally respond to ICMPv4 pings.
187 ${optionalString cfg.allowPing ''
188 iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null)
189 "-m limit ${cfg.pingLimit} "
190 }-j nixos-fw-accept
191 ''}
192
193 ${optionalString config.networking.enableIPv6 ''
194 # Accept all ICMPv6 messages except redirects and node
195 # information queries (type 139). See RFC 4890, section
196 # 4.4.
197 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
198 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
199 ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
200
201 # Allow this host to act as a DHCPv6 client
202 ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept
203 ''}
204
205 ${cfg.extraCommands}
206
207 # Reject/drop everything else.
208 ip46tables -A nixos-fw -j nixos-fw-log-refuse
209
210
211 # Enable the firewall.
212 ip46tables -A INPUT -j nixos-fw
213 '';
214
215 stopScript = writeShScript "firewall-stop" ''
216 ${helpers}
217
218 # Clean up in case reload fails
219 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
220
221 # Clean up after added ruleset
222 ip46tables -D INPUT -j nixos-fw 2>/dev/null || true
223
224 ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
225 ip46tables -t raw -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true
226 ''}
227
228 ${cfg.extraStopCommands}
229 '';
230
231 reloadScript = writeShScript "firewall-reload" ''
232 ${helpers}
233
234 # Create a unique drop rule
235 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
236 ip46tables -F nixos-drop 2>/dev/null || true
237 ip46tables -X nixos-drop 2>/dev/null || true
238 ip46tables -N nixos-drop
239 ip46tables -A nixos-drop -j DROP
240
241 # Don't allow traffic to leak out until the script has completed
242 ip46tables -A INPUT -j nixos-drop
243
244 ${cfg.extraStopCommands}
245
246 if ${startScript}; then
247 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
248 else
249 echo "Failed to reload firewall... Stopping"
250 ${stopScript}
251 exit 1
252 fi
253 '';
254
255 canonicalizePortList =
256 ports: lib.unique (builtins.sort builtins.lessThan ports);
257
258 commonOptions = {
259 allowedTCPPorts = mkOption {
260 type = types.listOf types.port;
261 default = [ ];
262 apply = canonicalizePortList;
263 example = [ 22 80 ];
264 description =
265 ''
266 List of TCP ports on which incoming connections are
267 accepted.
268 '';
269 };
270
271 allowedTCPPortRanges = mkOption {
272 type = types.listOf (types.attrsOf types.port);
273 default = [ ];
274 example = [ { from = 8999; to = 9003; } ];
275 description =
276 ''
277 A range of TCP ports on which incoming connections are
278 accepted.
279 '';
280 };
281
282 allowedUDPPorts = mkOption {
283 type = types.listOf types.port;
284 default = [ ];
285 apply = canonicalizePortList;
286 example = [ 53 ];
287 description =
288 ''
289 List of open UDP ports.
290 '';
291 };
292
293 allowedUDPPortRanges = mkOption {
294 type = types.listOf (types.attrsOf types.port);
295 default = [ ];
296 example = [ { from = 60000; to = 61000; } ];
297 description =
298 ''
299 Range of open UDP ports.
300 '';
301 };
302 };
303
304in
305
306{
307
308 ###### interface
309
310 options = {
311
312 networking.firewall = {
313 enable = mkOption {
314 type = types.bool;
315 default = true;
316 description =
317 ''
318 Whether to enable the firewall. This is a simple stateful
319 firewall that blocks connection attempts to unauthorised TCP
320 or UDP ports on this machine. It does not affect packet
321 forwarding.
322 '';
323 };
324
325 package = mkOption {
326 type = types.package;
327 default = pkgs.iptables;
328 defaultText = "pkgs.iptables";
329 example = literalExample "pkgs.iptables-nftables-compat";
330 description =
331 ''
332 The iptables package to use for running the firewall service."
333 '';
334 };
335
336 logRefusedConnections = mkOption {
337 type = types.bool;
338 default = true;
339 description =
340 ''
341 Whether to log rejected or dropped incoming connections.
342 '';
343 };
344
345 logRefusedPackets = mkOption {
346 type = types.bool;
347 default = false;
348 description =
349 ''
350 Whether to log all rejected or dropped incoming packets.
351 This tends to give a lot of log messages, so it's mostly
352 useful for debugging.
353 '';
354 };
355
356 logRefusedUnicastsOnly = mkOption {
357 type = types.bool;
358 default = true;
359 description =
360 ''
361 If <option>networking.firewall.logRefusedPackets</option>
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 ''
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 ''
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 ''
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 ''
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 example = "loose";
421 description =
422 ''
423 Performs a reverse path filter test on a packet. If a reply
424 to the packet would not be sent via the same interface that
425 the packet arrived on, it is refused.
426
427 If using asymmetric routing or other complicated routing, set
428 this option to loose mode or disable it and setup your own
429 counter-measures.
430
431 This option can be either true (or "strict"), "loose" (only
432 drop the packet if the source address is not reachable via any
433 interface) or false. Defaults to the value of
434 kernelHasRPFilter.
435
436 (needs kernel 3.3+)
437 '';
438 };
439
440 logReversePathDrops = mkOption {
441 type = types.bool;
442 default = false;
443 description =
444 ''
445 Logs dropped packets failing the reverse path filter test if
446 the option networking.firewall.checkReversePath is enabled.
447 '';
448 };
449
450 connectionTrackingModules = mkOption {
451 type = types.listOf types.str;
452 default = [ ];
453 example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ];
454 description =
455 ''
456 List of connection-tracking helpers that are auto-loaded.
457 The complete list of possible values is given in the example.
458
459 As helpers can pose as a security risk, it is advised to
460 set this to an empty list and disable the setting
461 networking.firewall.autoLoadConntrackHelpers unless you
462 know what you are doing. Connection tracking is disabled
463 by default.
464
465 Loading of helpers is recommended to be done through the
466 CT target. More info:
467 https://home.regit.org/netfilter-en/secure-use-of-helpers/
468 '';
469 };
470
471 autoLoadConntrackHelpers = mkOption {
472 type = types.bool;
473 default = false;
474 description =
475 ''
476 Whether to auto-load connection-tracking helpers.
477 See the description at networking.firewall.connectionTrackingModules
478
479 (needs kernel 3.5+)
480 '';
481 };
482
483 extraCommands = mkOption {
484 type = types.lines;
485 default = "";
486 example = "iptables -A INPUT -p icmp -j ACCEPT";
487 description =
488 ''
489 Additional shell commands executed as part of the firewall
490 initialisation script. These are executed just before the
491 final "reject" firewall rule is added, so they can be used
492 to allow packets that would otherwise be refused.
493 '';
494 };
495
496 extraPackages = mkOption {
497 type = types.listOf types.package;
498 default = [ ];
499 example = literalExample "[ pkgs.ipset ]";
500 description =
501 ''
502 Additional packages to be included in the environment of the system
503 as well as the path of networking.firewall.extraCommands.
504 '';
505 };
506
507 extraStopCommands = mkOption {
508 type = types.lines;
509 default = "";
510 example = "iptables -P INPUT ACCEPT";
511 description =
512 ''
513 Additional shell commands executed as part of the firewall
514 shutdown script. These are executed just after the removal
515 of the NixOS input rule, or if the service enters a failed
516 state.
517 '';
518 };
519
520 interfaces = mkOption {
521 default = { };
522 type = with types; attrsOf (submodule [ { options = commonOptions; } ]);
523 description =
524 ''
525 Interface-specific open ports.
526 '';
527 };
528 } // commonOptions;
529
530 };
531
532
533 ###### implementation
534
535 # FIXME: Maybe if `enable' is false, the firewall should still be
536 # built but not started by default?
537 config = mkIf cfg.enable {
538
539 networking.firewall.trustedInterfaces = [ "lo" ];
540
541 environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
542
543 boot.kernelModules = (optional cfg.autoLoadConntrackHelpers "nf_conntrack")
544 ++ map (x: "nf_conntrack_${x}") cfg.connectionTrackingModules;
545 boot.extraModprobeConfig = optionalString cfg.autoLoadConntrackHelpers ''
546 options nf_conntrack nf_conntrack_helper=1
547 '';
548
549 assertions = [
550 # This is approximately "checkReversePath -> kernelHasRPFilter",
551 # but the checkReversePath option can include non-boolean
552 # values.
553 { assertion = cfg.checkReversePath == false || kernelHasRPFilter;
554 message = "This kernel does not support rpfilter"; }
555 ];
556
557 systemd.services.firewall = {
558 description = "Firewall";
559 wantedBy = [ "sysinit.target" ];
560 wants = [ "network-pre.target" ];
561 before = [ "network-pre.target" ];
562 after = [ "systemd-modules-load.service" ];
563
564 path = [ cfg.package ] ++ cfg.extraPackages;
565
566 # FIXME: this module may also try to load kernel modules, but
567 # containers don't have CAP_SYS_MODULE. So the host system had
568 # better have all necessary modules already loaded.
569 unitConfig.ConditionCapability = "CAP_NET_ADMIN";
570 unitConfig.DefaultDependencies = false;
571
572 reloadIfChanged = true;
573
574 serviceConfig = {
575 Type = "oneshot";
576 RemainAfterExit = true;
577 ExecStart = "@${startScript} firewall-start";
578 ExecReload = "@${reloadScript} firewall-reload";
579 ExecStop = "@${stopScript} firewall-stop";
580 };
581 };
582
583 };
584
585}