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