diff --git a/pkgs/default.nix b/pkgs/default.nix index de8ce663..d60ec95e 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -53,4 +53,5 @@ min-collect-garbage = callPackage ./min-collect-garbage {}; min-copy-closure = callPackage ./min-copy-closure {}; hi = callPackage ./hi {}; + firewallgen = callPackage ./firewallgen {}; } diff --git a/pkgs/firewallgen/default.nix b/pkgs/firewallgen/default.nix new file mode 100644 index 00000000..92991235 --- /dev/null +++ b/pkgs/firewallgen/default.nix @@ -0,0 +1,58 @@ +{ + nftables +, writeScript +, lib +} : +name : ruleset : +let + inherit (lib.strings) concatStringsSep splitString hasInfix substring; + inherit (lib.lists) groupBy; + inherit (lib.attrsets) mapAttrsToList nameValuePair; + inherit (builtins) map listToAttrs replaceStrings head tail; + + indentLines = offset : lines : + if lines == [] + then "" + else + let + line = head lines; + isOpen = hasInfix "{" line; + isClose = hasInfix "}" line; + offset' = offset + + (if isOpen then 4 else 0) + + (if isClose then -4 else 0); + padding = offset: substring 0 offset " "; + in + if (isClose && !isOpen) + then + (padding offset') + line + "\n" + indentLines offset' (tail lines) + else + (padding offset) + line + "\n" + indentLines offset' (tail lines); + + indent = text : indentLines 0 (splitString "\n" text); + + dochain = { name, type, family, rules, policy ? null, hook ? null } : '' + chain ${name} { + ${if hook != null + then "type ${type} hook ${hook}; policy ${policy};" + else "" + } + ${concatStringsSep "\n" rules} + } + ''; + dotable = family : chains : '' + table ${family} ${family} { + ${concatStringsSep "\n" (map dochain chains)} + } + ''; + categorise = chains : + groupBy + ({ family, ... } : family) + (mapAttrsToList (n : v : v // { name = n; }) chains); +in writeScript name '' +#!${nftables}/sbin/nft -cf + +flush ruleset + +${indent (concatStringsSep "\n" (mapAttrsToList dotable (categorise ruleset)))} +'' diff --git a/pkgs/firewallgen/test-rules-min.nix b/pkgs/firewallgen/test-rules-min.nix new file mode 100644 index 00000000..7560a54d --- /dev/null +++ b/pkgs/firewallgen/test-rules-min.nix @@ -0,0 +1,121 @@ +let + drop = expr : "${expr} drop"; + accept = expr : "${expr} accept"; + mcast-scope = 8; + allow-incoming = true; + bogons-ip6 = { + type = "filter"; + family = "ip6"; + rules = [ + (drop "saddr ff00::/8") # multicast saddr is illegal + + (drop "saddr ::/128") # unspecified address + (drop "daddr ::/128") + (drop "saddr 2001:db8::/32") # documentation addresses + (drop "daddr 2001:db8::/32") + + # I think this means "check FIB for (saddr, iif) to see if we + # could route a packet to that address using that interface", + # and if we can't then it was an inapproppriate source address + # for packets received _from_ said interface + (drop "fib saddr . iif oif eq 0") + + (drop "icmpv6 type router-renumbering") + (drop "icmpv6 type 139") # Node Information Query + (drop "icmpv6 type 140") # Node Information Response + (drop "icmpv6 type 100") + (drop "icmpv6 type 101") + (drop "icmpv6 type 200") + (drop "icmpv6 type 201") + (drop "icmpv6 type 127") + (drop "icmpv6 type 255") + (drop "icmpv6 type destination-unreachable ct state invalid,untracked") + ]; + }; + forward-ip6 = { + type = "filter"; + family = "ip6"; + policy = "drop"; + hook = "forward"; + rules = [ + "jump bogons-ip6" + (drop "saddr ::1/128") # loopback address [RFC4291] + (drop "daddr ::1/128") + (drop "saddr ::FFFF:0:0/96")# IPv4-mapped addresses + (drop "daddr ::FFFF:0:0/96") + (drop "saddr fe80::/10") # link-local unicast + (drop "daddr fe80::/10") + (drop "saddr fc00::/7") # unique-local addresses + (drop "daddr fc00::/7") + (drop "saddr 2001:10::/28") # ORCHID [RFC4843]. + (drop "daddr 2001:10::/28") + + (drop "saddr fc00::/7") # unique local source + (drop "daddr fc00::/7") # and/or dst addresses [RFC4193] + + # multicast with wrong scopes + (drop + # dest addr first byte 0xff, low nibble of second byte <= scope + # https://www.mankier.com/8/nft#Payload_Expressions-Raw_Payload_Expression + "@nh,192,8 eq 0xff @nh,204,4 le ${toString mcast-scope})") + + (accept "oifname \"int\" iifname \"ppp0\" meta l4proto udp ct state established,related") + (accept "iifname \"int\" oifname \"ppp0\" meta l4proto udp") + + (accept "icmpv6") + (accept "ah") + (accept "esp") + (accept "udp port 500") # IKE Protocol [RFC5996]. haha zyxel + (accept "ip6 nexthdr hip") + + ## FIXME no support yet for recs 27-30 Mobility Header + + (accept "oifname \"int\" iifname \"ppp0\" meta l4proto tcp ct state established,related") + (accept "iifname \"int\" oifname \"ppp0\" meta l4proto tcp") + + (accept "oifname \"int\" iifname \"ppp0\" meta l4proto sctp ct state established,related") + (accept "iifname \"int\" oifname \"ppp0\" meta l4proto sctp") + + (accept "oifname \"int\" iifname \"ppp0\" meta l4proto dccp ct state established,related") + (accept "iifname \"int\" oifname \"ppp0\" meta l4proto dccp") + + # we can allow all reasonable inbound, or we can use an explicit + # allowlist to enumerate the endpoints that are allowed to + # accept inbound from the WAN + (if allow-incoming + then accept "oifname \"int\" iifname \"ppp0\"" + else { rule = "oifname \"int\" iifname \"ppp0\" jump incoming-allowed-ip6"; } + ) + # allow all outbound and any inbound that's part of a + # recognised (outbound-initiated) flow + (accept "oifname \"int\" iifname \"ppp0\" ct state established,related") + (accept "iifname \"int\" oifname \"ppp0\" ") + ]; + }; + input-ip6 = { + type = "filter"; + family = "ip6"; + policy = "drop"; + hook = "input"; + rules = [ + "jump bogons-ip6" + (accept "icmpv6") + (if allow-incoming + then accept "oifname \"int\" iifname \"ppp0\"" + else { rule = "oifname \"int\" iifname \"ppp0\" jump incoming-allowed-ip6"; } + ) + (accept "oifname \"int\" iifname \"ppp0\" ct state established,related") + (accept "iifname \"int\" oifname \"ppp0\" ") + ]; + }; + + incoming-allowed-ip6 = { + type = "filter"; + family = "ip6"; + rules = [ + "oifname \"int\" tcp port 22 daddr loaclhost.lan" + ]; + }; +in { + inherit input-ip6 forward-ip6 bogons-ip6 incoming-allowed-ip6; +} diff --git a/pkgs/firewallgen/test.nix b/pkgs/firewallgen/test.nix new file mode 100644 index 00000000..5b11cb7b --- /dev/null +++ b/pkgs/firewallgen/test.nix @@ -0,0 +1,4 @@ +let + pkgs = import { overlays = [( import ../../overlay.nix)]; }; + ruleset = import ./test-rules-min.nix; +in pkgs.firewallgen "firewall.nft" ruleset