vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / networking / firewall-iptables.nix
blobe4fa7676fd077f8960aba2ee56f3da0cbc4991bb
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
5    uses several chains:
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
17    this chain.
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, ... }:
33 let
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:
44     let
45       dir = pkgs.writeScriptBin name ''
46         #! ${pkgs.runtimeShell} -e
47         ${text}
48       '';
49     in
50     "${dir}/bin/${name}";
52   startScript = writeShScript "firewall-start" ''
53     ${helpers}
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
62     done
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
79     '' else ''
80       ip46tables -A nixos-fw-refuse -j DROP
81     ''}
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: "
90     ''}
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: "
96     ''}
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: "
101     ''}
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: "
127       ''}
128       ip46tables -t mangle -A nixos-fw-rpfilter -j DROP
130       ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter
131     ''}
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
136     '')}
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:
144         ''
145           ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${lib.optionalString (iface != "default") "-i ${iface}"}
146         ''
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
154         ''
155           ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${lib.optionalString (iface != "default") "-i ${iface}"}
156         ''
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:
163         ''
164           ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${lib.optionalString (iface != "default") "-i ${iface}"}
165         ''
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
173         ''
174           ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${lib.optionalString (iface != "default") "-i ${iface}"}
175         ''
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} "
183       }-j nixos-fw-accept
184     ''}
186     ${lib.optionalString config.networking.enableIPv6 ''
187       # Accept all ICMPv6 messages except redirects and node
188       # information queries (type 139).  See RFC 4890, section
189       # 4.4.
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
196     ''}
198     ${cfg.extraCommands}
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
206   '';
208   stopScript = writeShScript "firewall-stop" ''
209     ${helpers}
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
219     ''}
221     ${cfg.extraStopCommands}
222   '';
224   reloadScript = writeShScript "firewall-reload" ''
225     ${helpers}
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
241     else
242       echo "Failed to reload firewall... Stopping"
243       ${stopScript}
244       exit 1
245     fi
246   '';
252   options = {
254     networking.firewall = {
255       extraCommands = lib.mkOption {
256         type = lib.types.lines;
257         default = "";
258         example = "iptables -A INPUT -p icmp -j ACCEPT";
259         description = ''
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.
266         '';
267       };
269       extraStopCommands = lib.mkOption {
270         type = lib.types.lines;
271         default = "";
272         example = "iptables -P INPUT ACCEPT";
273         description = ''
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
277           state.
279           This option only works with the iptables based firewall.
280         '';
281       };
282     };
284   };
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) {
290     assertions = [
291       # This is approximately "checkReversePath -> kernelHasRPFilter",
292       # but the checkReversePath option can include non-boolean
293       # values.
294       {
295         assertion = cfg.checkReversePath == false || kernelHasRPFilter;
296         message = "This kernel does not support rpfilter";
297       }
298     ];
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;
321       serviceConfig = {
322         Type = "oneshot";
323         RemainAfterExit = true;
324         ExecStart = "@${startScript} firewall-start";
325         ExecReload = "@${reloadScript} firewall-reload";
326         ExecStop = "@${stopScript} firewall-stop";
327       };
328     };
330   };