Parameter-driven Firewall Generator Specification
Last revised 23 July 2021, 6 am PDT


This is the specification for the new firewall for the satchell.net
gateway router between LAN and Internet.

GOALS:
(1) Use interface specifications only, not IP addresses where practical.
Reserved networks are listed as black-hole routes in the FIB (Forward
Information Base, aka routing table) via another means.

(2) Provide for IPv6 forwarding, and block a subnet of IPv6 to protect
LAN devices within that particular subnet.  Particularly, protect
the subnet from which DHCPv6 servers allocated IPv6 addresses; the
administrator needs to assign an address outside of the private
subnet to be accessible to the world at large.

(3) Design the generator such that a GUI front end can be written
to manage the pinholes and other parameters without code changes
in the base generator.

(4) Use a macro language to simplify both execution of the original
design, and to facilitate the addition of extensions in the future.


DISCUSSION:  The original firewall was written for iptables(8).  Due
 to limitations in the implementation of iptables(8), though, this
specification will now target nftables(8).

The addition of IPv6-aware functionality needs to be straightforward
enough to be easily understood even by non-technical people, yet
robust enough to isolate LAN computers from outside fiddling.  In
other words, using different hiding techniques for the new realm
of IPv6 than was used in IPv4, NAT.

Because much of this specification was pulled from old existing
iptables(8) implementation, the description of the function for each
table and chain are couched in terms of the original rules.  The
generated rules, and the fixed template, will be written in the
syntax of nftables(8).

The generated code will consist of a pre-built nftable structure,
with macro substitution and macro operators

NOTE: This document uses the phrases "on us" and "transit" in the
same manner as banks use these phrases for check processing.
"On us" means that the input is intended for the firewall,
while "transit" means that the input is intended for another
endpoint.  This is a clarification of the iptables(8) and
nftables(8) descriptions as to which packets are given to
the INPUT chains versus the FORWARD chains.

Specifically, if the destination address, when run through
the routing table, has the fib type LOCAL, it's on-us,
otherwise it's transit (or unroutable).

------------------------------------------------------------------------
* Need to understand if I have to worry about -m state --state SNAT
  or DNAT.

------------------------------------------------------------------------
nftables set negation test:  tcp dport != {1,2,3}  If it's needed.

#======================================================================
#======================================================================
#======================================================================

%% SAMPLE PARAMETERS FILE:
%% =======================

%% This sample file is the result of 20 years' collection of ports
%% and protocols that needed to be supported.  In real life, the
%% parameter file will become MUCH smaller, as the barnacles are
%% removed.

%% Parameters for this particular server.  These assignments are
%% specific to my network.  The parameters will be pared down as
%% development continues.  The --lint option will list all unused
%% and undefined symbols to stderr

%PUB=162
%ID=31

%% Used to protect LAN-specific IPv6 addressing.  These assignments
%% are specific to my network.  The prefix length is used in
%% determining whether the IPv6 address is private.  This is in view
%% of my network; no other meaning should be attached.

%IPV6_PREFIX="2600:1700:79b0:ddc0"
%IPV6_PRIVATE="%IPV6_PREFIX:0::"
%IPV6_PUBLIC="%IPV6_PREFIX:ff00::"
%IPV6_SUBNET_LENGTH=72

%NET="enp1s0"
%NET_IP="76.209.1.%PUB"
%NET_PREFIX=29

%LAN="enp2s0"
%LAN_IP="10.1.1.%ID"
%LAN_PREFIX=24

%% %FORWARD_*  = LAN-initiated to the Internet
%% %INTERNET_* = Internet-initiated traffic to the firewall (service)
%% %LAN_*      = LAN-initiated traffic to the firewall (service)
%% %EXTERNAL_* = firewall-initiated traffic to the Internet
%% %INTERNAL_* = firewall-initiated traffic to the LAN
%% %*_PROTOCOLS= protocols (other than tcp, udp, and icmp) to accept
%%
%% Use the names contained in /etc/services or the port number.
%% For %*_PROCOCOLs, use the number, or names, in /etc/protocols.  Not
%% required for tcp, udp, or icmp.  Usually: gre, esp, ah
%%
%% NOTE:  FORWARD only allows LAN systems to initiate connections.  FORWARD
%%        never forwards connection attempts from the Internet to the LAN.

%FORWARD_TCP=""
%FORWARD_UDP=""
%FORWARD_PROTOCOLS=""

%NET_TCP=""
%NET_UDP=""
%NET_PROTOCOLS=""

%LAN_TCP=""
%LAN_UDP=""
%LAN_UDP_BCST=""   (needed?)
%LAN_PROTOCOLS=""

%EXTERNAL_TCP=""
%EXTERNAL_UDP=""
%EXTERNAL_PROTOCOLS=""

%INTERNAL_TCP=""
%INTERNAL_UDP=""
%INTERNAL_UDP_BCST=""
%INTERNAL_PROTOCOLS=""


%% += concatinates the original string with a space and the new text.

%% --------originated by LAN host to Internet-----------------

%FORWARD_TCP+="ssh telnet"                    %% terminal
%FORWARD_TCP+="ftp ftp-data ftps ftps-data rsync"      %% file transfer
%FORWARD_TCP+="smtp smtps submission submissions"      %% email server
%FORWARD_TCP+="pop3 imap pop3s imaps"                  %% e-mail client
%FORWARD_TCP+="http https 8008 webcache"               %% Web
%FORWARD_TCP+="8008 8443 8888"                         %% Web alternate
%FORWARD_TCP+="47000:47014 47101:47120 51001:51012"    %% [pyramid.net radios]
%FORWARD_TCP+="11371"             %% [HP signature server]
%FORWARD_TCP+="1701"              %% [Cary L2TP VPN]
%FORWARD_TCP+="1723"              %% [Microsoft PPTP]
%FORWARD_TCP+="1863"              %% [msnp, Microsoft Messenger]
%FORWARD_TCP+="2705"              %% [memoq]
%FORWARD_TCP+="3390"              %% [this is randy and bob's fault]
%FORWARD_TCP+="407 1417:1420"     %% [Timbuktu client, Service Ports 1-4]
%FORWARD_TCP+="500"               %% IPSec VPN
%FORWARD_TCP+="5222 5223 8002"    %% [xmpp-client]
%FORWARD_TCP+="7080"              %% [surge webmail]
%FORWARD_TCP+="8123"              %% [Cary Carl's bootcamp portal]
%FORWARD_TCP+="888 8880"          %% [CD database, Plesk (8880)]
%FORWARD_TCP+="9418"              %% [GIT port]
%FORWARD_TCP+="domain"            %% DNS/BIND
%FORWARD_TCP+="<redacted>"  %% [INE SSH to *.ine.com]
%FORWARD_TCP+="ms-wbt-server"     %% Microsoft WBT (Remote desktop)
%FORWARD_TCP+="radius"            %% Remote Auth Dial_In User Service
%FORWARD_TCP+="snmp"              %% Simple Net Mgmt
%FORWARD_TCP+="snpp"              %% Simple Network Paging Protocol
%FORWARD_TCP+="whois"             %% whois

%FORWARD_UDP+="10000"             %% [I forgot what this is for!]
%FORWARD_UDP+="1723"              %% [Microsoft PPTP]
%FORWARD_UDP+="3389"              %% [MS terminal services]
%FORWARD_UDP+="407"               %% [Timbuktu]
%FORWARD_UDP+="55555"             %% [amhosting.com monitor]
%FORWARD_UDP+="domain"            %% DNS/Bind
%FORWARD_UDP+="ipsec-nat-t isakmp" %% IPSec VPN
%FORWARD_UDP+="l2tp"              %% Level 2 VPN
%FORWARD_UDP+="ntp"               %% Network Time Protocol
%FORWARD_UDP+="radius"            %% Remote Auth Dial_In User Service
%FORWARD_UDP+="snmp"              %% Simple Net Mgmt

%FORWARD_PROTOCOLS+="gre esp ah"  %% [IPSEC, Windows VPN]

%% --------inbound to firewall--------------------------------
%LAN_TCP+="ssh domain smtp imap imaps submission pop3 http"
%LAN_TCP+="submissions 5222 25900 1935 5900 873 3333 2705"
%LAN_UDP+="ntp domain snmp traceroute"
%LAN_UDP_BCST+=""
%INTERNET_TCP+="ssh smtp smtps"
%INTERNET_UDP+="domain ntp"

%%  ---------originated by firewall---------------------------
%INTERNAL_TCP+="ftp telnet pop3 imap ssh 137 138 139"
%INTERNAL_UDP+="tftp domain ntp traceroute 137 138 139"
%INTERNAL_UDP_BCST+="137 138 139"

%EXTERNAL_TCP+="ssh smtp smtps submission domain"
%EXTERNAL_UDP+="domain ntp isakmp traceroute"

%% Other parameters may be added during development.  In the macro
%% system, all these parameters appear as macro symbols.

#=======================================================================
#=======================================================================
#=======================================================================

An alternative to the repeated use of += is to use line splicing
in the parameter file.  One such use is shown here:

%FORWARD_UDP+="\
  10000             %% [I forgot what this is for!] \
  1723              %% [Microsoft PPTP] \
  3389              %% [MS terminal services] \
  407               %% [Timbuktu] \
  55555             %% [amhosting.com monitor] \
  domain            %% DNS/Bind \
  ipsec-nat-t isakmp %% IPSec VPN \
  l2tp              %% Level 2 VPN \
  ntp               %% Network Time Protocol \
  radius            %% Remote Auth Dial_In User Service \
  snmp              %% Simple Net Mgmt \
  "

#-----------------------------------------------------------------------

Incoming rp_filter check:

Incoming packets from non-lo interfaces:
-t raw PREROUTING
  -i enp1s0 -m rpfilter --loose --invert -j DROP
     (connection tracking)
---------------------------------------------------------------------
Incoming packets (on-us) for host IPv4:               (iptables only)
-t mangle INPUT
  empty
-t filter INPUT
  -m state --state ESTABLISHED,RELATED -j ACCEPT
  -m state --state INVALID,UNTRACKED -j DROP

  -i lo     -j ACCEPT
  #-i enp2s0 -j ACCEPT

  -p tcp    -j tcp-flags-check
  -p tcp    -j filter-input-on-us-tcp
  -p udp    -j filter-input-on-us-udp
  -p icmp   -j filter-input-on-us-icmp
            -j filter-input-on-us-other
  -j DROP
---------------------------------------------------------------------
Incoming packets (on-us) for host IPv6:              (ip6tables only)
-t mangle INPUT
  empty
-t filter INPUT
  -m state --state ESTABLISHED,RELATED -j ACCEPT
  -m state --state INVALID,UNTRACKED -j DROP

  -i lo     -j ACCEPT
  #-i enp2s0 -j ACCEPT

  -i enp1s0 -d 2600:1700:79b0:ddc0::1:0/112 -j DROP -m comment --comment "block internal LAN IPv6 addresses"

  -p tcp    -j tcp-flags-check
  -p tcp    -j filter-input-on-us-tcp
  -p udp    -j filter-input-on-us-udp
  -p icmp   -j filter-input-on-us-icmp
            -j filter-input-on-us-other
  -j DROP
---------------------------------------------------------------------
Incoming packets (transit) for another host:          (iptables only)
-t mangle FORWARD
  empty
-t filter FORWARD
  -m state --state ESTABLISHED,RELATED -j ACCEPT
  -m state --state INVALID,UNTRACKED -j DROP

  -o enp2s0 -p tcp  -j tcp-flags-check
  -o enp2s0 -p tcp  -j filter-forward-inbound-tcp
  -o enp2s0 -p udp  -j filter-forward-inbound-udp
  -o enp2s0 -p icmp -j filter-forward-inbound-icmp
  -o enp2s0         -j filter-forward-inbound-other

  -o enp1s0 -p tcp  -j filter-forward-outbound-tcp
  -o enp1s0 -p udp  -j filter-forward-outbound-udp
  -o enp1s0 -p icmp -j filter-forward-outbound-icmp
  -o enp1s0         -j filter-forward-outbound-other

  -j DROP

---------------------------------------------------------------------
Incoming packets (transit) for another host:         (ip6tables only)
-t mangle FORWARD
  empty
-t filter FORWARD
  -m state --state ESTABLISHED,RELATED -j ACCEPT
  -m state --state INVALID,UNTRACKED -j DROP

  -i enp1s0 -o enp2s0 -d 2600:1700:79b0:ddc0::1:0/112 -j DROP -m comment --comment "block internal LAN IPv6 addresses"

  -o enp2s0 -p tcp  -j tcp-flags-check
  -o enp2s0 -p tcp  -j filter-forward-inbound-tcp
  -o enp2s0 -p udp  -j filter-forward-inbound-udp
  -o enp2s0 -p icmp -j filter-forward-inbound-icmp
  -o enp2s0         -j filter-forward-inbound-other

  -o enp1s0 -p tcp  -j filter-forward-outbound-tcp
  -o enp1s0 -p udp  -j filter-forward-outbound-udp
  -o enp1s0 -p icmp -j filter-forward-outbound-icmp
  -o enp1s0         -j filter-forward-outbound-other

  -j DROP

---------------------------------------------------------------------
Locally-generated packets:
-t raw OUTPUT
  empty
     (connection tracking)
-t mangle OUTPUT
  empty
-t nat OUTPUT
  empty
     (routing decision)
-t filter OUTPUT
  -m state --state ESTABLISHED,RELATED -j ACCEPT
  -o lo -m state --state NEW -j ACCEPT
  -o enp1s0 -m state --state NEW -j ACCEPT
  -o enp2s0 -m state --state NEW -j ACCEPT
  -o tun0   -m state --state NEW -j ACCEPT
  -j DROP
---------------------------------------------------------------------
Includes rp_filter check on packets to be sent,
and does source-address substituion for NAT.

All outgoing packets:
-t mangle POSTROUTING                                (ip6tables only)
  -o enp1s0 -m rpfilter --loose --invert -j DROP
-t nat POSTROUTING                                    (iptables only)
  -o enp1s0 -j MASQUERADE
  -o enp1s0 -m rpfilter --loose --invert -j DROP
     (output packet to interface)
---------------------------------------------------------------------
I need to ponder exactly what rules would work well
with my firewall design.  One thing for sure: I'd like
to eliminate the IP addressing, and instead use the
interface name.  This many not be possible without
making openvpn-iptables a program instead of just
some scattered commands; for example, finding out
which of the tun* devices is available.

Also, need to add IPv6-specific rules for ip6tables.

NOTE: existing systemd control files add no rules to
ip6tables.

From /etc/systemd/system/openvpn-iptables.service:
-t filter INPUT
  -p tcp --dport 443 -j ACCEPT
-t filter FORWARD
  -s 10.8.0.0/24 -j ACCEPT
  -m state --state RELATED,ESTABLISHED -j ACCEPT
-t nat POSTROUTING
  -s 10.8.0.0/24 -d 10.1.1.0/24 -o enp2s0 -j MASQUERADE
  -t nat -A POSTROUTING -s 10.8.0.0/24 ! -d 10.8.0.0/24 -j SNAT --to 76.209.1.165
---------------------------------------------------------------------
---------------------------------------------------------------------
Implied in all the non-standard chains is that the conntrack
state is always NEW.
DROP stops the processing and drops the packet.
REJECT stops the processing and returns an ICMP packet.
RETURN lets processing resume where the chain was called.
ACCEPT stops the processing, accepting the packet and bypasses
 the rest of the rules in the calling chain.
---------------------------------------------------------------------
To ease development in this firewall, the skeleton consists of valid
nftables(8) table structures, and macro language elements.  The macro
language borrows ideas from the old IBM PL/1 macro preprocessor,
from 1970.  NOTE: the nftables(8) table structure, to the best of
my knowledge, has no requirements for indenting.  Indents are for
humans, not the nft(8) loader.  Experiments on a virtual machine
will show if this is verity.

LINE SPLICING:
The macro processor recognizes the regexp "[ \t]*/\n[ \t]*", and
replaces all occurrences with a single space.  This permits line
continuation in the standard UNIX/LINUX way.

MACRO SYMBOLS:
Macro symbols consist of any number of characters satisfying the
regexp experssion "%[A-Za-z][A-Za-z0-9_-]*".

MACRO SUBSTITUTION:
Macro symbol substition is done when the processor sees the reference
"%symbol", or "%symbol." where the end of the substitution is
followed by a valid symbol characters: "port %start.-%end"

MACRO DUPLICATES ELIMINATOR:
There is a "special" symbol provided to make the parameters
easier to deal with:

    %uniq <string>

First, all leading and trailing <space> and <tab> are stripped.  Then,
substitute each  "[ /t]*,?[ \t]*" with " ".  Parse the string,
using <space> as a delimiter; if the string is a duplicate, don't
include the duplicate(s).  Note that if you use a name from
/etc/service, and later the number, both will still be included in
the output.  Examples:

    %uniq " 1   2 3 2 4" -> "1 2 3 4"
    %uniq "53 domain" -> "53 domain"

MACRO LIST GENERATOR FUNCTION:
There is a "special" symbol, which is required to satisfy the
syntax requirements of nftabls(8):

     %list <string>

First, all leading and trailing <space> and <tab> are stripped.  Then,
substitute each  "[ /t]*,?[ \t]*" with ",".  Example:

   %PORTS= "  1    2 3  "
   %list %PORTS --> "1,2,3"
   %list "  alpha  bravo charlie  " -> "alpha,bravo,charlie"

With string replacement, this lets you have a "constant" and a
variable.  For example:

    PROTOCOLS="  gre esp aj   "
    %list "icmp %PROTOCOLS" -> "icmp,gre,esp,aj"

MACRO UNIQUE-LIST FUNCTION:
The two special symbols can be used together, to strip duplicates and
generate a comma-separate list:

    %uniqlist "  1 5 1 2 1 " -> "1,5,2"

(Yes, there could be a %sort function if it ever becomes necessary.)

MACRO LANGUAGE:
Macro statements start with %%.  Valid statements are
    %%include <filepath>
    %%<symbol>=<text>    assignment
    %%<symbol>="text"    string assignment (escape '"' with '\"')
    %%<symbol>+="text"   String extend: original <space> new
    %%if <expression>    conditional test
    %%else               conditional else
    %%fi <expression>    conditional end
    %%<space>            comment (delete from %% to \n or /\n, preserve
                           \n or /\n)
    
EXPRESSION EVALUATION:
<expression> is FALSE if the referenced symbol is zero length, and is
TRUE if the referenced symbol is non-zero length.  (Other forms of
expression will be added when needed.)

DIAGNOSTICS (--lint):
  * symbol reassignment (= not +=)
  * un-referenced symbols
  * undefined symbols
  * unknown service (%service-to-number)
  * unknown protocol (%protocol-to-number)
  * unterminated string at \n

SEQUENCE:
  1. Remove comments, preserving trailing '/'
  2. Line splicing
  3. Macro processing. line by line, three passes:
       Macro symbol assignment and substitution
       Macro function symbol processing
       Macro statements
  4. Output nftable firewall script

SKELETON for nftables (Python coding):
======================================

SKELETON = """\
%%include "fw_parameters"
%%strict_routing=1

table ip raw {
     %% NOTE: the raw/PREROUTING table/chain comes before conntrack.
     %%       This is a good thing.
     chain PREROUTING {
          type filter hook prerouting priority mangle; policy accept;

          %% Drops all saddrs = broadcast, multicast, unroutable, black-hole
          iifname "enp1s0" fib saddr type != unicast drop

          %% This rule  is redundant
          %%     fib saddr oif != 0 counter drop

          %% Limits echo and reply from the Internet
          iifname "enp1s0" icmp type { echo-request, echo-reply } jump limit-ping

          %% Blocks UDP ports that are used in DDos attacks
          %% (bootps, bootpc, domain, ntp)
          iffname "enp1s0" udp dport { 67-68, 53, 123 } drop
          
          %% Drop fragmented ICMP packets from public
          iifname "enp1s0" ip frag-off & 0x1fff != 0 ip protocol icmp counter drop
          
     }

     chain OUTPUT {
          type route hook output priority mangle; policy accept;
     }

     chain limit-ping {
          %% ping/reply rate too high, drop the packet
          limit rate 10/second burst 5 packets return
          counter drop
     }
}

table ip6 {
     %% NOTE: the raw/PREROUTING table/chain comes before conntrack.
     %%       This is a good thing.
     chain PREROUTING {
          type filter hook prerouting priority mangle; policy accept;

          %% Drops all saddrs = broadcast, multicast, unroutable, black-hole
          iifname "enp1s0" fib saddr type != unicast drop

          %% This rule  is redundant
          %%     fib saddr oif != 0 counter drop

          %% Limits echo and reply from the Internet
          iifname "enp1s0" icmpv6 type { echo-request, echo-reply } jump limit-ping

          %%if %IPV6_PRIVATE   %% Blocks daddr to my LAN IPv6 addresses
          iifname "enp1s0" oifname "enp2s0" ip daddr %IPV6_PRIVATE/$IPV6_SUBNETX_LENGTH counter drop
          %%fi

          %% Blocks UDP ports that are used in DDos attacks
          %% (bootps, bootpc, domain, ntp)
          iffname "enp1s0" udp dport { 67-68, 53, 123 } drop
          
          %% Drop fragmented ICMP packets from public
          iifname "enp1s0" ip frag-off & 0x1fff != 0 ip protocol icmp counter drop
          
          %% Accept a subset of ICMPV6
          icmpv6 type != { destination-unreachable,
		            packet-too-big,
		            time-exceeded,
		            parameter-problem,
		            nd-router-advert,
		            nd-neighbor-solicit,
		            nd-neighbor-advert, 
		            nd-redirect
		          } counter drop
          icmpv6 type    { nd-router-advert,
		            nd-neighbor-solicit,
		            nd-neighbor-advert,
		            nd-redirect
		          } ip6 hoplimit 255 counter drop
     }

     chain OUTPUT {
          type route hook output priority mangle; policy accept;
     }

     chain limit-ping {
          %% ping/reply rate too high, drop the packet
          limit rate 10/second burst 5 packets return
          counter drop
     }
}
table inet mangle {
     chain PREROUTING {
          type filter hook prerouting priority mangle; policy accept;
     }

     chain INPUT {
          type filter hook input priority mangle; policy accept;

          %%if %strict_routing
          fib saddr . iif oif != 0 counter drop
          %%fi
     }

     chain FORWARD {
          type filter hook forward priority mangle; policy accept;

          %%if %strict_routing
          fib saddr . iif oif != 0 counter drop
          %%fi
     }

     chain OUTPUT {
          type route hook output priority mangle; policy accept;
     }

     %% mangle/POSTROUTING is where OUTPUT and FORWARD converge for
     %%      the first time to send a packet
     chain POSTROUTING {
          type filter hook postrouting priority mangle; policy accept;

          oifname "enp1s0" fib saddr type != unicast counter drop
          %%if %strict_routing
          fib saddr . iif oif != 0 counter drop
          %%else
          fib saddr oif != 0 counter drop
          %%fi
     }
}
table ip nat {
     chain PREROUTING {
          type nat hook prerouting priority dstnat; policy accept;
          
          %% Allow only a subset of the ICMP messages
          iifname "eth1s0" icmp type != {   \
			source-quench        \
                       parameter-problem    \
			fragmentation-needed \
			port-unreachable     \
			host-unreachable     \
			host-prohibited      \
			network-prohibited   \
                    } drop
     }

     chain OUTPUT {
          type nat hook output priority -100; policy accept;
     }

     chain POSTROUTING {
          type nat hook postrouting priority srcnat; policy accept;

          iifname "enp2s0" oifname "enp1s0" ip protocol { tcp, udp, icmp, gre, esp, aj } counter masquerade
     }
}
table ip6 nat {
     chain PREROUTING {
          type nat hook prerouting priority dstnat; policy accept;

          iifname "eth1s0" icmp type != {   \
			\
                    } drop
     }

     chain OUTPUT {
          type nat hook output priority -100; policy accept;
     }

     chain POSTROUTING {
          type nat hook postrouting priority srcnat; policy accept;
     }
}
table ip filter {
     chain INPUT {
          type filter hook input priority filter; policy drop;

          ct state related,established accept
          ct state invalid,untracked  counter drop

          iifname "lo" counter accept

            # block small services, DHCP, DNS, NTP
          tcp dport != 20-65534 counter drop
          udp dport != 20-65534 counter drop
          iffname "enp1s0" udp dport { 67-68, 53, 123 } counter drop

          # Put the packet through the hoops
          tcp jump tcp-flags-check
          tcp jump filter-input-on_us-tcp
          udp jump filter-input-on_us-udp
          icmp jump filter-input-on_us-icmp
          jump filter-input-on_us-other

          limit rate 5/minute log prefix "fw-IN:" level info
          counter drop
     }

     chain FORWARD {
          type filter hook forward priority filter; policy drop;

          ct state related,established  counter accept
          ct state invalid,untracked  counter drop
          iifname "lo" counter accept

            # block small services, DHCP, DNS, NTP
          tcp dport != 20-65534 counter drop
          udp dport != 20-65534 counter drop

          oifname "enp2s0" tcp jump tcp-flags-check
          oifname "enp2s0" tcp jump filter-forward-inbound-tcp
          oifname "enp2s0" udp jump filter-forward-inbound-udp
          oifname "enp2s0" icmp jump filter-forward-inbound-icmp
          oifname "enp2s0" jump filter-forward-inbound-other

          oifname "enp1s0" tcp jump tcp-flags-check
          oifname "enp1s0" tcp jump filter-forward-outbound-tcp
          oifname "enp1s0" udp jump filter-forward-outbound-udp
          oifname "enp1s0" icmp jump filter-forward-outbound-icmp
          oifname "enp1s0" jump filter-forward-outbound-other

          limit rate 5/minute log prefix "fw-FWD:" level info
          counter drop
     }

     chain OUTPUT {
          type filter hook output priority filter; policy drop;

          ct state related,established  accept
          ct state invalid,untracked  counter drop

          accept
     }

     chain tcp-flags-check {
          tcp flags & (fin | syn | rst | psh | ack | urg) == 0x0 limit rate 5/minute log prefix "fw>ALL-NONE:" level info
          tcp flags & (fin | syn | rst | psh | ack | urg) == 0x0 counter drop
          tcp flags & (fin | syn | rst | psh | ack | urg) == fin | syn | rst | psh | ack | urg limit rate 5/minute log prefix "fw>ALL-ALL:" level info
          tcp flags & (fin | syn | rst | psh | ack | urg) == fin | syn | rst | psh | ack | urg counter drop
          tcp flags & (fin | syn) == fin | syn limit rate 5/minute log prefix "fw>SYN,FIN-SYN,FIN:" level info
          tcp flags & (fin | syn) == fin | syn counter drop
          tcp flags & (syn | rst) == syn | rst limit rate 5/minute log prefix "fw>SYN,RST-SYN,RST:" level info
          tcp flags & (syn | rst) == syn | rst counter drop
          tcp flags & (fin | rst) == fin | rst limit rate 5/minute log prefix "fw>FIN,RST-FIN,RST:" level info
          tcp flags & (fin | rst) == fin | rst counter drop
          tcp flags & (fin | ack) == fin limit rate 5/minute log prefix "fw>ACK,FIN-FIN:" level info
          tcp flags & (fin | ack) == fin counter drop
          tcp flags & (psh | ack) == psh limit rate 5/minute log prefix "fw>ACK,PSH-PSH:" level info
          tcp flags & (psh | ack) == psh counter drop
          tcp flags & (ack | urg) == urg limit rate 5/minute log prefix "fw>ACK,URG-URG:" level info
          tcp flags & (ack | urg) == urg counter drop
     }

     %% In each of these chains, the ct state is always NEW

     chain filter-input-on_us-tcp {
         %%if %LAN_TCP
         inet protocol tcp dport { %uniqlist %LAN_TCP } return
         %%fi
         counter drop
     }

     chain filter-input-on_us-udp {
         %%if %LAN_UDP
         inet protocol udp dport { %uniqlist %LAN_UDP } return
         %%fi
         counter drop
     }

     chain filter-input-on_us-icmp {
     }

     chain filter-input-on_us-other {
         %%if %LAN_PROTOCOLS
         inet protocol { %uniqlist %LAN_PROTOCOLS } return
         %%fi
         counter drop
     }

     %% Being routed to PUB "enp1s0"
     chain filter-forward-outbound-tcp {
         %%if %FORWARD_TCP
         inet protocol tcp dport { %uniqlist %FORWARD_TCP } return
         %%fi
         counter drop
     }

     chain filter-forward-outbound-udp {
         %%if %FORWARD_UDP
         inet protocol tcp dport { %uniqlist %FORWARD_UDP } return
         %%fi
         counter drop
     }

     chain filter-forward-outbound-icmp {
     }

     chain filter-forward-outbound-other {
         %%if %LAN_PROTOCOLS
         inet protocol { %uniqlist %FORWARD_PROTOCOLS } return
         %%fi
         counter drop
     }

     %% Being routed to LAN "enp1s0"
     chain filter-forward-inbound-tcp {
     	counter drop
     }

     chain filter-forward-inbound-udp {
     	counter drop
     }

     chain forward-input-inbound-icmp {
     	counter drop
     }

     chain filter-forward-inbound-other {
     	counter drop
     }

}
table ip6 filter {
     chain INPUT {
          type filter hook input priority filter; policy drop;

          ct state related,established accept
          ct state invalid,untracked  counter drop

          iifname "lo" counter accept

            # block small services, DHCP, DNS, NTP
          meta l4proto tcp dport != 20-65534 counter drop
          meta l4proto udp dport != 20-65534 counter drop
          iffname "enp1s0" meta l4proto udp dport { 67-68, 53, 123 } counter drop

          # Put the packet through the hoops
          meta l4proto tcp jump tcp-flags-check
          meta l4proto tcp jump filter-input-on_us-tcp
          meta l4proto udp jump filter-input-on_us-udp
          meta l4proto icmp jump filter-input-on_us-icmp
          jump filter-input-on_us-other

          limit rate 5/minute log prefix "fw-IN:" level info
          counter drop
     }

     chain FORWARD {
          type filter hook forward priority filter; policy drop;

          ct state related,established  counter accept
          ct state invalid,untracked  counter drop
          iifname "lo" counter accept

            # block small services, DHCP, DNS, NTP
          nexthdr tcp dport != 20-65534 counter drop
          nexthdr udp dport != 20-65534 counter drop

          oifname "enp2s0" nexthdr tcp jump tcp-flags-check
          oifname "enp2s0" nexthdr tcp jump filter-forward-inbound-tcp
          oifname "enp2s0" nexthdr udp jump filter-forward-inbound-udp
          oifname "enp2s0" nexthdr icmp jump filter-forward-inbound-icmp
          oifname "enp2s0" jump filter-forward-inbound-other

          oifname "enp1s0" nexthdr tcp jump tcp-flags-check
          oifname "enp1s0" nexthdr tcp jump filter-forward-outbound-tcp
          oifname "enp1s0" nexthdr udp jump filter-forward-outbound-udp
          oifname "enp1s0" nexthdr icmp jump filter-forward-outbound-icmp
          oifname "enp1s0" jump filter-forward-outbound-other

          limit rate 5/minute log prefix "fw-FWD:" level info
          counter drop
     }

     chain OUTPUT {
          type filter hook output priority filter; policy drop;

          ct state related,established  accept
          ct state invalid,untracked  counter drop

          accept
     }

     chain tcp-flags-check {
          nexthdr tcp flags & (fin | syn | rst | psh | ack | urg) == 0x0 limit rate 5/minute log prefix "fw>ALL-NONE:" level info
          nexthdr tcp flags & (fin | syn | rst | psh | ack | urg) == 0x0 counter drop
          nexthdr tcp flags & (fin | syn | rst | psh | ack | urg) == fin | syn | rst | psh | ack | urg limit rate 5/minute log prefix "fw>ALL-ALL:" level info
          nexthdr tcp flags & (fin | syn | rst | psh | ack | urg) == fin | syn | rst | psh | ack | urg counter drop
          nexthdr tcp flags & (fin | syn) == fin | syn limit rate 5/minute log prefix "fw>SYN,FIN-SYN,FIN:" level info
          nexthdr tcp flags & (fin | syn) == fin | syn counter drop
          nexthdr tcp flags & (syn | rst) == syn | rst limit rate 5/minute log prefix "fw>SYN,RST-SYN,RST:" level info
          nexthdr tcp flags & (syn | rst) == syn | rst counter drop
          nexthdr tcp flags & (fin | rst) == fin | rst limit rate 5/minute log prefix "fw>FIN,RST-FIN,RST:" level info
          nexthdr tcp flags & (fin | rst) == fin | rst counter drop
          nexthdr tcp flags & (fin | ack) == fin limit rate 5/minute log prefix "fw>ACK,FIN-FIN:" level info
          nexthdr tcp flags & (fin | ack) == fin counter drop
          nexthdr tcp flags & (psh | ack) == psh limit rate 5/minute log prefix "fw>ACK,PSH-PSH:" level info
          nexthdr tcp flags & (psh | ack) == psh counter drop
          nexthdr tcp flags & (ack | urg) == urg limit rate 5/minute log prefix "fw>ACK,URG-URG:" level info
          nexthdr tcp flags & (ack | urg) == urg counter drop
     }

     %% In each of these chains, the ct state is always NEW

     chain filter-input-on_us-tcp {
         %%if %LAN_TCP
         nexthdr  tcp dport { %uniqlist %LAN_TCP } return
         %%fi
         counter drop
     }

     chain filter-input-on_us-udp {
         %%if %LAN_UDP
         nexthdr  udp dport { %uniqlist %LAN_UDP } return
         %%fi
         counter drop
     }

     chain filter-input-on_us-icmp {
     }

     chain filter-input-on_us-other {
         %%if %LAN_PROTOCOLS
         inet protocol { %uniqlist %LAN_PROTOCOLS } return
         %%fi
         counter drop
     }

     %% Being routed to PUB "enp1s0"
     chain filter-forward-outbound-tcp {
         %%if %FORWARD_TCP
         inet protocol tcp dport { %uniqlist %FORWARD_TCP } return
         %%fi
         counter drop
     }

     chain filter-forward-outbound-udp {
         %%if %FORWARD_UDP
         inet protocol tcp dport { %uniqlist %FORWARD_UDP } return
         %%fi
         counter drop
     }

     chain filter-forward-outbound-icmp {
     }

     chain filter-forward-outbound-other {
         %%if %LAN_PROTOCOLS
         inet protocol { %uniqlist %FORWARD_PROTOCOLS } return
         %%fi
         counter drop
     }

     %% Being routed to LAN "enp1s0"
     chain filter-forward-inbound-tcp {
     	counter drop
     }

     chain filter-forward-inbound-udp {
     	counter drop
     }

     chain forward-input-inbound-icmp {
     	counter drop
     }

     chain filter-forward-inbound-other {
     	counter drop
     }

}
"""

Comments, suggestions, and error reports are welcome.
Send them to: spamfilter (at) satchell (dot) net)
© 2021 Stephen Satchell, Reno NV