commit 80639a7256551ee1e4e38e0febf9068694db9f94
parent 994cca671ba616db9d06db2b5aafdbf5ca31d0e3
Author: Daniel Barlow <dan@telent.net>
Date: Sun, 18 Jun 2023 17:40:16 +0100
add firewallgen package, which creates an nft script
Diffstat:
4 files changed, 184 insertions(+), 0 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
+let
+ pkgs = import <nixpkgs> { overlays = [( import ../../overlay.nix)]; };
+ ruleset = import ./test-rules-min.nix;
+in pkgs.firewallgen "firewall.nft" ruleset