From ec9c4143a7142e28851b9b158a32274675114f2b Mon Sep 17 00:00:00 2001 From: "William A. Kennington III" Date: Tue, 16 Sep 2014 01:15:10 -0700 Subject: [PATCH] nixos/firewall: Cleanup in case reload fails --- .../modules/services/networking/firewall.nix | 359 ++++++++++-------- 1 file changed, 192 insertions(+), 167 deletions(-) diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix index eaf48b9c6e5b..68aac3d30de1 100644 --- a/nixos/modules/services/networking/firewall.nix +++ b/nixos/modules/services/networking/firewall.nix @@ -37,6 +37,180 @@ let } ''; + writeShScript = name: text: let dir = pkgs.writeScriptBin name '' + #! ${pkgs.stdenv.shell} -e + ${text} + ''; in "${dir}/bin/${name}"; + + startScript = writeShScript "firewall-start" '' + ${helpers} + + # Flush the old firewall rules. !!! Ideally, updating the + # firewall would be atomic. Apparently that's possible + # with iptables-restore. + ip46tables -D INPUT -j nixos-fw 2> /dev/null || true + for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse FW_REFUSE; do + ip46tables -F "$chain" 2> /dev/null || true + ip46tables -X "$chain" 2> /dev/null || true + done + + + # The "nixos-fw-accept" chain just accepts packets. + ip46tables -N nixos-fw-accept + ip46tables -A nixos-fw-accept -j ACCEPT + + + # The "nixos-fw-refuse" chain rejects or drops packets. + ip46tables -N nixos-fw-refuse + + ${if cfg.rejectPackets then '' + # Send a reset for existing TCP connections that we've + # somehow forgotten about. Send ICMP "port unreachable" + # for everything else. + ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset + ip46tables -A nixos-fw-refuse -j REJECT + '' else '' + ip46tables -A nixos-fw-refuse -j DROP + ''} + + + # The "nixos-fw-log-refuse" chain performs logging, then + # jumps to the "nixos-fw-refuse" chain. + ip46tables -N nixos-fw-log-refuse + + ${optionalString cfg.logRefusedConnections '' + ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "rejected connection: " + ''} + ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) '' + ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \ + -j LOG --log-level info --log-prefix "rejected broadcast: " + ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \ + -j LOG --log-level info --log-prefix "rejected multicast: " + ''} + ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse + ${optionalString cfg.logRefusedPackets '' + ip46tables -A nixos-fw-log-refuse \ + -j LOG --log-level info --log-prefix "rejected packet: " + ''} + ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse + + + # The "nixos-fw" chain does the actual work. + ip46tables -N nixos-fw + + # Perform a reverse-path test to refuse spoofers + # For now, we just drop, as the raw table doesn't have a log-refuse yet + ${optionalString (kernelHasRPFilter && cfg.checkReversePath) '' + if ! ip46tables -A PREROUTING -t raw -m rpfilter --invert -j DROP; then + echo "<2>failed to initialise rpfilter support" >&2 + fi + ''} + + # Accept all traffic on the trusted interfaces. + ${flip concatMapStrings cfg.trustedInterfaces (iface: '' + ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept + '')} + + # Accept packets from established or related connections. + ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept + + # Accept connections to the allowed TCP ports. + ${concatMapStrings (port: + '' + ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept + '' + ) cfg.allowedTCPPorts + } + + # Accept connections to the allowed TCP port ranges. + ${concatMapStrings (rangeAttr: + let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in + '' + ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept + '' + ) cfg.allowedTCPPortRanges + } + + # Accept packets on the allowed UDP ports. + ${concatMapStrings (port: + '' + ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept + '' + ) cfg.allowedUDPPorts + } + + # Accept packets on the allowed UDP port ranges. + ${concatMapStrings (rangeAttr: + let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in + '' + ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept + '' + ) cfg.allowedUDPPortRanges + } + + # Accept IPv4 multicast. Not a big security risk since + # probably nobody is listening anyway. + #iptables -A nixos-fw -d 224.0.0.0/4 -j nixos-fw-accept + + # Optionally respond to ICMPv4 pings. + ${optionalString cfg.allowPing '' + iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null) + "-m limit ${cfg.pingLimit} " + }-j nixos-fw-accept + ''} + + # Accept all ICMPv6 messages except redirects and node + # information queries (type 139). See RFC 4890, section + # 4.4. + ${optionalString config.networking.enableIPv6 '' + ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP + ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP + ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept + ''} + + ${cfg.extraCommands} + + # Reject/drop everything else. + ip46tables -A nixos-fw -j nixos-fw-log-refuse + + + # Enable the firewall. + ip46tables -A INPUT -j nixos-fw + ''; + + stopScript = writeShScript "firewall-stop" '' + ${helpers} + + # Clean up in case reload fails + ip46tables -D INPUT -j nixos-drop 2>/dev/null || true + + # Clean up after added ruleset + ip46tables -D INPUT -j nixos-fw 2>/dev/null || true + + ${cfg.extraStopCommands} + ''; + + reloadScript = writeShScript "firewall-reload" '' + ${helpers} + + # Create a unique drop rule + ip46tables -D INPUT -j nixos-drop 2>/dev/null || true + ip46tables -F nixos-drop 2>/dev/null || true + ip46tables -X nixos-drop 2>/dev/null || true + ip46tables -N nixos-drop + ip46tables -A nixos-drop -j DROP + + # Don't allow traffic to leak out until the script has completed + ip46tables -A INPUT -j nixos-drop + if ${startScript}; then + ip46tables -D INPUT -j nixos-drop 2>/dev/null || true + else + echo "Failed to reload firewall... Stopping" + ${stopScript} + exit 1 + fi + ''; + kernelPackages = config.boot.kernelPackages; kernelHasRPFilter = kernelPackages.kernel.features.netfilterRPFilter or false; @@ -248,7 +422,7 @@ in '' Additional shell commands executed as part of the firewall shutdown script. These are executed just after the removal - of the nixos input rule. + of the nixos input rule, or if the service enters a failed state. ''; }; @@ -276,177 +450,28 @@ in message = "This kernel does not support disabling conntrack helpers"; } ]; - systemd.services.firewall = rec - { description = "Firewall"; + systemd.services.firewall = { + description = "Firewall"; + wantedBy = [ "network.target" ]; + after = [ "network-interfaces.target" "systemd-modules-load.service" ]; - wantedBy = [ "network.target" ]; - after = [ "network-interfaces.target" "systemd-modules-load.service" ]; + path = [ pkgs.iptables ]; - path = [ pkgs.iptables ]; + # FIXME: this module may also try to load kernel modules, but + # containers don't have CAP_SYS_MODULE. So the host system had + # better have all necessary modules already loaded. + unitConfig.ConditionCapability = "CAP_NET_ADMIN"; - # FIXME: this module may also try to load kernel modules, but - # containers don't have CAP_SYS_MODULE. So the host system had - # better have all necessary modules already loaded. - unitConfig.ConditionCapability = "CAP_NET_ADMIN"; + reloadIfChanged = true; - reloadIfChanged = true; - - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - - script = - '' - ${helpers} - - # Flush the old firewall rules. !!! Ideally, updating the - # firewall would be atomic. Apparently that's possible - # with iptables-restore. - ip46tables -D INPUT -j nixos-fw 2> /dev/null || true - for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse FW_REFUSE; do - ip46tables -F "$chain" 2> /dev/null || true - ip46tables -X "$chain" 2> /dev/null || true - done - - - # The "nixos-fw-accept" chain just accepts packets. - ip46tables -N nixos-fw-accept - ip46tables -A nixos-fw-accept -j ACCEPT - - - # The "nixos-fw-refuse" chain rejects or drops packets. - ip46tables -N nixos-fw-refuse - - ${if cfg.rejectPackets then '' - # Send a reset for existing TCP connections that we've - # somehow forgotten about. Send ICMP "port unreachable" - # for everything else. - ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset - ip46tables -A nixos-fw-refuse -j REJECT - '' else '' - ip46tables -A nixos-fw-refuse -j DROP - ''} - - - # The "nixos-fw-log-refuse" chain performs logging, then - # jumps to the "nixos-fw-refuse" chain. - ip46tables -N nixos-fw-log-refuse - - ${optionalString cfg.logRefusedConnections '' - ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "rejected connection: " - ''} - ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) '' - ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \ - -j LOG --log-level info --log-prefix "rejected broadcast: " - ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \ - -j LOG --log-level info --log-prefix "rejected multicast: " - ''} - ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse - ${optionalString cfg.logRefusedPackets '' - ip46tables -A nixos-fw-log-refuse \ - -j LOG --log-level info --log-prefix "rejected packet: " - ''} - ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse - - - # The "nixos-fw" chain does the actual work. - ip46tables -N nixos-fw - - # Perform a reverse-path test to refuse spoofers - # For now, we just drop, as the raw table doesn't have a log-refuse yet - ${optionalString (kernelHasRPFilter && cfg.checkReversePath) '' - if ! ip46tables -A PREROUTING -t raw -m rpfilter --invert -j DROP; then - echo "<2>failed to initialise rpfilter support" >&2 - fi - ''} - - # Accept all traffic on the trusted interfaces. - ${flip concatMapStrings cfg.trustedInterfaces (iface: '' - ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept - '')} - - # Accept packets from established or related connections. - ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept - - # Accept connections to the allowed TCP ports. - ${concatMapStrings (port: - '' - ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept - '' - ) cfg.allowedTCPPorts - } - - # Accept connections to the allowed TCP port ranges. - ${concatMapStrings (rangeAttr: - let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in - '' - ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept - '' - ) cfg.allowedTCPPortRanges - } - - # Accept packets on the allowed UDP ports. - ${concatMapStrings (port: - '' - ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept - '' - ) cfg.allowedUDPPorts - } - - # Accept packets on the allowed UDP port ranges. - ${concatMapStrings (rangeAttr: - let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in - '' - ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept - '' - ) cfg.allowedUDPPortRanges - } - - # Accept IPv4 multicast. Not a big security risk since - # probably nobody is listening anyway. - #iptables -A nixos-fw -d 224.0.0.0/4 -j nixos-fw-accept - - # Optionally respond to ICMPv4 pings. - ${optionalString cfg.allowPing '' - iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null) - "-m limit ${cfg.pingLimit} " - }-j nixos-fw-accept - ''} - - # Accept all ICMPv6 messages except redirects and node - # information queries (type 139). See RFC 4890, section - # 4.4. - ${optionalString config.networking.enableIPv6 '' - ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP - ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP - ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept - ''} - - ${cfg.extraCommands} - - # Reject/drop everything else. - ip46tables -A nixos-fw -j nixos-fw-log-refuse - - - # Enable the firewall. - ip46tables -A INPUT -j nixos-fw - ''; - - reload = '' - ${helpers} - ip46tables -A INPUT -j DROP - ${script} - ip46tables -D INPUT -j DROP || true # extraCommands might delete the above rule and cause this to fail - ''; - - postStop = - '' - ${helpers} - ip46tables -D INPUT -j nixos-fw || true - ${cfg.extraStopCommands} - ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "@${startScript} firewall-start"; + ExecReload = "@${reloadScript} firewall-reload"; + ExecStop = "@${stopScript} firewall-stop"; }; + }; };