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