1 /* This module enables a simple firewall.
3 The firewall can be customised in arbitrary ways by setting
4 ‘networking.firewall.extraCommands’. For modularity, the firewall
7 - ‘nixos-fw’ is the main chain for input packet processing.
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.
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
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.
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).
32 { config, lib, pkgs, ... }:
35 cfg = config.networking.firewall;
37 inherit (config.boot.kernelPackages) kernel;
39 kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false);
41 helpers = import ./helpers.nix { inherit config lib; };
43 writeShScript = name: text:
45 dir = pkgs.writeScriptBin name ''
46 #! ${pkgs.runtimeShell} -e
52 startScript = writeShScript "firewall-start" ''
55 # Flush the old firewall rules. !!! Ideally, updating the
56 # firewall would be atomic. Apparently that's possible
57 # with iptables-restore.
58 ip46tables -D INPUT -j nixos-fw 2> /dev/null || true
59 for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do
60 ip46tables -F "$chain" 2> /dev/null || true
61 ip46tables -X "$chain" 2> /dev/null || true
65 # The "nixos-fw-accept" chain just accepts packets.
66 ip46tables -N nixos-fw-accept
67 ip46tables -A nixos-fw-accept -j ACCEPT
70 # The "nixos-fw-refuse" chain rejects or drops packets.
71 ip46tables -N nixos-fw-refuse
73 ${if cfg.rejectPackets then ''
74 # Send a reset for existing TCP connections that we've
75 # somehow forgotten about. Send ICMP "port unreachable"
76 # for everything else.
77 ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset
78 ip46tables -A nixos-fw-refuse -j REJECT
80 ip46tables -A nixos-fw-refuse -j DROP
84 # The "nixos-fw-log-refuse" chain performs logging, then
85 # jumps to the "nixos-fw-refuse" chain.
86 ip46tables -N nixos-fw-log-refuse
88 ${lib.optionalString cfg.logRefusedConnections ''
89 ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: "
91 ${lib.optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
92 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \
93 -j LOG --log-level info --log-prefix "refused broadcast: "
94 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \
95 -j LOG --log-level info --log-prefix "refused multicast: "
97 ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
98 ${lib.optionalString cfg.logRefusedPackets ''
99 ip46tables -A nixos-fw-log-refuse \
100 -j LOG --log-level info --log-prefix "refused packet: "
102 ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse
105 # The "nixos-fw" chain does the actual work.
106 ip46tables -N nixos-fw
108 # Clean up rpfilter rules
109 ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
110 ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true
111 ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true
113 ${lib.optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
114 # Perform a reverse-path test to refuse spoofers
115 # For now, we just drop, as the mangle table doesn't have a log-refuse yet
116 ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true
117 ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${lib.optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN
119 # Allows this host to act as a DHCP4 client without first having to use APIPA
120 iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN
122 # Allows this host to act as a DHCPv4 server
123 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
125 ${lib.optionalString cfg.logReversePathDrops ''
126 ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
128 ip46tables -t mangle -A nixos-fw-rpfilter -j DROP
130 ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter
133 # Accept all traffic on the trusted interfaces.
134 ${lib.flip lib.concatMapStrings cfg.trustedInterfaces (iface: ''
135 ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept
138 # Accept packets from established or related connections.
139 ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept
141 # Accept connections to the allowed TCP ports.
142 ${lib.concatStrings (lib.mapAttrsToList (iface: cfg:
143 lib.concatMapStrings (port:
145 ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${lib.optionalString (iface != "default") "-i ${iface}"}
147 ) cfg.allowedTCPPorts
148 ) cfg.allInterfaces)}
150 # Accept connections to the allowed TCP port ranges.
151 ${lib.concatStrings (lib.mapAttrsToList (iface: cfg:
152 lib.concatMapStrings (rangeAttr:
153 let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
155 ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${lib.optionalString (iface != "default") "-i ${iface}"}
157 ) cfg.allowedTCPPortRanges
158 ) cfg.allInterfaces)}
160 # Accept packets on the allowed UDP ports.
161 ${lib.concatStrings (lib.mapAttrsToList (iface: cfg:
162 lib.concatMapStrings (port:
164 ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${lib.optionalString (iface != "default") "-i ${iface}"}
166 ) cfg.allowedUDPPorts
167 ) cfg.allInterfaces)}
169 # Accept packets on the allowed UDP port ranges.
170 ${lib.concatStrings (lib.mapAttrsToList (iface: cfg:
171 lib.concatMapStrings (rangeAttr:
172 let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
174 ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${lib.optionalString (iface != "default") "-i ${iface}"}
176 ) cfg.allowedUDPPortRanges
177 ) cfg.allInterfaces)}
179 # Optionally respond to ICMPv4 pings.
180 ${lib.optionalString cfg.allowPing ''
181 iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${lib.optionalString (cfg.pingLimit != null)
182 "-m limit ${cfg.pingLimit} "
186 ${lib.optionalString config.networking.enableIPv6 ''
187 # Accept all ICMPv6 messages except redirects and node
188 # information queries (type 139). See RFC 4890, section
190 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
191 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
192 ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
194 # Allow this host to act as a DHCPv6 client
195 ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept
200 # Reject/drop everything else.
201 ip46tables -A nixos-fw -j nixos-fw-log-refuse
204 # Enable the firewall.
205 ip46tables -A INPUT -j nixos-fw
208 stopScript = writeShScript "firewall-stop" ''
211 # Clean up in case reload fails
212 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
214 # Clean up after added ruleset
215 ip46tables -D INPUT -j nixos-fw 2>/dev/null || true
217 ${lib.optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
218 ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true
221 ${cfg.extraStopCommands}
224 reloadScript = writeShScript "firewall-reload" ''
227 # Create a unique drop rule
228 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
229 ip46tables -F nixos-drop 2>/dev/null || true
230 ip46tables -X nixos-drop 2>/dev/null || true
231 ip46tables -N nixos-drop
232 ip46tables -A nixos-drop -j DROP
234 # Don't allow traffic to leak out until the script has completed
235 ip46tables -A INPUT -j nixos-drop
237 ${cfg.extraStopCommands}
239 if ${startScript}; then
240 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
242 echo "Failed to reload firewall... Stopping"
254 networking.firewall = {
255 extraCommands = lib.mkOption {
256 type = lib.types.lines;
258 example = "iptables -A INPUT -p icmp -j ACCEPT";
260 Additional shell commands executed as part of the firewall
261 initialisation script. These are executed just before the
262 final "reject" firewall rule is added, so they can be used
263 to allow packets that would otherwise be refused.
265 This option only works with the iptables based firewall.
269 extraStopCommands = lib.mkOption {
270 type = lib.types.lines;
272 example = "iptables -P INPUT ACCEPT";
274 Additional shell commands executed as part of the firewall
275 shutdown script. These are executed just after the removal
276 of the NixOS input rule, or if the service enters a failed
279 This option only works with the iptables based firewall.
286 # FIXME: Maybe if `enable' is false, the firewall should still be
287 # built but not started by default?
288 config = lib.mkIf (cfg.enable && config.networking.nftables.enable == false) {
291 # This is approximately "checkReversePath -> kernelHasRPFilter",
292 # but the checkReversePath option can include non-boolean
295 assertion = cfg.checkReversePath == false || kernelHasRPFilter;
296 message = "This kernel does not support rpfilter";
300 environment.systemPackages = [ pkgs.nixos-firewall-tool ];
301 networking.firewall.checkReversePath = lib.mkIf (!kernelHasRPFilter) (lib.mkDefault false);
303 systemd.services.firewall = {
304 description = "Firewall";
305 wantedBy = [ "sysinit.target" ];
306 wants = [ "network-pre.target" ];
307 after = [ "systemd-modules-load.service" ];
308 before = [ "network-pre.target" "shutdown.target" ];
309 conflicts = [ "shutdown.target" ];
311 path = [ cfg.package ] ++ cfg.extraPackages;
313 # FIXME: this module may also try to load kernel modules, but
314 # containers don't have CAP_SYS_MODULE. So the host system had
315 # better have all necessary modules already loaded.
316 unitConfig.ConditionCapability = "CAP_NET_ADMIN";
317 unitConfig.DefaultDependencies = false;
319 reloadIfChanged = true;
323 RemainAfterExit = true;
324 ExecStart = "@${startScript} firewall-start";
325 ExecReload = "@${reloadScript} firewall-reload";
326 ExecStop = "@${stopScript} firewall-stop";