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}