at 23.11-pre 12 kB view raw
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 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. 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: 48 let 49 dir = pkgs.writeScriptBin name '' 50 #! ${pkgs.runtimeShell} -e 51 ${text} 52 ''; 53 in 54 "${dir}/bin/${name}"; 55 56 startScript = writeShScript "firewall-start" '' 57 ${helpers} 58 59 # Flush the old firewall rules. !!! Ideally, updating the 60 # firewall would be atomic. Apparently that's possible 61 # with iptables-restore. 62 ip46tables -D INPUT -j nixos-fw 2> /dev/null || true 63 for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do 64 ip46tables -F "$chain" 2> /dev/null || true 65 ip46tables -X "$chain" 2> /dev/null || true 66 done 67 68 69 # The "nixos-fw-accept" chain just accepts packets. 70 ip46tables -N nixos-fw-accept 71 ip46tables -A nixos-fw-accept -j ACCEPT 72 73 74 # The "nixos-fw-refuse" chain rejects or drops packets. 75 ip46tables -N nixos-fw-refuse 76 77 ${if cfg.rejectPackets then '' 78 # Send a reset for existing TCP connections that we've 79 # somehow forgotten about. Send ICMP "port unreachable" 80 # for everything else. 81 ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset 82 ip46tables -A nixos-fw-refuse -j REJECT 83 '' else '' 84 ip46tables -A nixos-fw-refuse -j DROP 85 ''} 86 87 88 # The "nixos-fw-log-refuse" chain performs logging, then 89 # jumps to the "nixos-fw-refuse" chain. 90 ip46tables -N nixos-fw-log-refuse 91 92 ${optionalString cfg.logRefusedConnections '' 93 ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: " 94 ''} 95 ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) '' 96 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \ 97 -j LOG --log-level info --log-prefix "refused broadcast: " 98 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \ 99 -j LOG --log-level info --log-prefix "refused multicast: " 100 ''} 101 ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse 102 ${optionalString cfg.logRefusedPackets '' 103 ip46tables -A nixos-fw-log-refuse \ 104 -j LOG --log-level info --log-prefix "refused packet: " 105 ''} 106 ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse 107 108 109 # The "nixos-fw" chain does the actual work. 110 ip46tables -N nixos-fw 111 112 # Clean up rpfilter rules 113 ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true 114 ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true 115 ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true 116 117 ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 118 # Perform a reverse-path test to refuse spoofers 119 # For now, we just drop, as the mangle table doesn't have a log-refuse yet 120 ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true 121 ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN 122 123 # Allows this host to act as a DHCP4 client without first having to use APIPA 124 iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN 125 126 # Allows this host to act as a DHCPv4 server 127 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 128 129 ${optionalString cfg.logReversePathDrops '' 130 ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: " 131 ''} 132 ip46tables -t mangle -A nixos-fw-rpfilter -j DROP 133 134 ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter 135 ''} 136 137 # Accept all traffic on the trusted interfaces. 138 ${flip concatMapStrings cfg.trustedInterfaces (iface: '' 139 ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept 140 '')} 141 142 # Accept packets from established or related connections. 143 ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept 144 145 # Accept connections to the allowed TCP ports. 146 ${concatStrings (mapAttrsToList (iface: cfg: 147 concatMapStrings (port: 148 '' 149 ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 150 '' 151 ) cfg.allowedTCPPorts 152 ) cfg.allInterfaces)} 153 154 # Accept connections to the allowed TCP port ranges. 155 ${concatStrings (mapAttrsToList (iface: cfg: 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 ${optionalString (iface != "default") "-i ${iface}"} 160 '' 161 ) cfg.allowedTCPPortRanges 162 ) cfg.allInterfaces)} 163 164 # Accept packets on the allowed UDP ports. 165 ${concatStrings (mapAttrsToList (iface: cfg: 166 concatMapStrings (port: 167 '' 168 ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 169 '' 170 ) cfg.allowedUDPPorts 171 ) cfg.allInterfaces)} 172 173 # Accept packets on the allowed UDP port ranges. 174 ${concatStrings (mapAttrsToList (iface: cfg: 175 concatMapStrings (rangeAttr: 176 let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in 177 '' 178 ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 179 '' 180 ) cfg.allowedUDPPortRanges 181 ) cfg.allInterfaces)} 182 183 # Optionally respond to ICMPv4 pings. 184 ${optionalString cfg.allowPing '' 185 iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null) 186 "-m limit ${cfg.pingLimit} " 187 }-j nixos-fw-accept 188 ''} 189 190 ${optionalString config.networking.enableIPv6 '' 191 # Accept all ICMPv6 messages except redirects and node 192 # information queries (type 139). See RFC 4890, section 193 # 4.4. 194 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP 195 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP 196 ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept 197 198 # Allow this host to act as a DHCPv6 client 199 ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept 200 ''} 201 202 ${cfg.extraCommands} 203 204 # Reject/drop everything else. 205 ip46tables -A nixos-fw -j nixos-fw-log-refuse 206 207 208 # Enable the firewall. 209 ip46tables -A INPUT -j nixos-fw 210 ''; 211 212 stopScript = writeShScript "firewall-stop" '' 213 ${helpers} 214 215 # Clean up in case reload fails 216 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 217 218 # Clean up after added ruleset 219 ip46tables -D INPUT -j nixos-fw 2>/dev/null || true 220 221 ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 222 ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true 223 ''} 224 225 ${cfg.extraStopCommands} 226 ''; 227 228 reloadScript = writeShScript "firewall-reload" '' 229 ${helpers} 230 231 # Create a unique drop rule 232 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 233 ip46tables -F nixos-drop 2>/dev/null || true 234 ip46tables -X nixos-drop 2>/dev/null || true 235 ip46tables -N nixos-drop 236 ip46tables -A nixos-drop -j DROP 237 238 # Don't allow traffic to leak out until the script has completed 239 ip46tables -A INPUT -j nixos-drop 240 241 ${cfg.extraStopCommands} 242 243 if ${startScript}; then 244 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 245 else 246 echo "Failed to reload firewall... Stopping" 247 ${stopScript} 248 exit 1 249 fi 250 ''; 251 252in 253 254{ 255 256 options = { 257 258 networking.firewall = { 259 extraCommands = mkOption { 260 type = types.lines; 261 default = ""; 262 example = "iptables -A INPUT -p icmp -j ACCEPT"; 263 description = lib.mdDoc '' 264 Additional shell commands executed as part of the firewall 265 initialisation script. These are executed just before the 266 final "reject" firewall rule is added, so they can be used 267 to allow packets that would otherwise be refused. 268 269 This option only works with the iptables based firewall. 270 ''; 271 }; 272 273 extraStopCommands = mkOption { 274 type = types.lines; 275 default = ""; 276 example = "iptables -P INPUT ACCEPT"; 277 description = lib.mdDoc '' 278 Additional shell commands executed as part of the firewall 279 shutdown script. These are executed just after the removal 280 of the NixOS input rule, or if the service enters a failed 281 state. 282 283 This option only works with the iptables based firewall. 284 ''; 285 }; 286 }; 287 288 }; 289 290 # FIXME: Maybe if `enable' is false, the firewall should still be 291 # built but not started by default? 292 config = mkIf (cfg.enable && config.networking.nftables.enable == false) { 293 294 assertions = [ 295 # This is approximately "checkReversePath -> kernelHasRPFilter", 296 # but the checkReversePath option can include non-boolean 297 # values. 298 { 299 assertion = cfg.checkReversePath == false || kernelHasRPFilter; 300 message = "This kernel does not support rpfilter"; 301 } 302 ]; 303 304 networking.firewall.checkReversePath = mkIf (!kernelHasRPFilter) (mkDefault false); 305 306 systemd.services.firewall = { 307 description = "Firewall"; 308 wantedBy = [ "sysinit.target" ]; 309 wants = [ "network-pre.target" ]; 310 before = [ "network-pre.target" ]; 311 after = [ "systemd-modules-load.service" ]; 312 313 path = [ cfg.package ] ++ cfg.extraPackages; 314 315 # FIXME: this module may also try to load kernel modules, but 316 # containers don't have CAP_SYS_MODULE. So the host system had 317 # better have all necessary modules already loaded. 318 unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 319 unitConfig.DefaultDependencies = false; 320 321 reloadIfChanged = true; 322 323 serviceConfig = { 324 Type = "oneshot"; 325 RemainAfterExit = true; 326 ExecStart = "@${startScript} firewall-start"; 327 ExecReload = "@${reloadScript} firewall-reload"; 328 ExecStop = "@${stopScript} firewall-stop"; 329 }; 330 }; 331 332 }; 333 334}