Parameter-driven Firewall Generator Specification Last revised 22 July 2021, 4 pm PDT This is the specification for the new firewall for the satchell.net gateway router between LAN and Internet. The IP addresses shown in this specification are not accurate, as the information was pulled from an OpenVPN router that will be retired. GOAL: Use interface specifications only, not IP addresses. Reserved networks are listed as black-hole routes in the FIB (Forward Information Base, aka routing table) via another means. The detailed pinholes are not included in this specification, as they will be determined by specific application; example pinhole definitions are included for reference. 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). 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 ------------------------------------------------------------------------ * Need to understand if I have to worry about -m state --state SNAT or DNAT. #====================================================================== #====================================================================== #====================================================================== %% 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+="domain" %% DNS/BIND %FORWARD_TCP+="whois" %% whois %FORWARD_TCP+="snmp" %% Simple Net Mgmt %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+="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+="ms-wbt-server" %% Microsoft WBT (Remote desktop) %FORWARD_TCP+="radius" %% Remote Auth Dial_In User Service %FORWARD_TCP+="" %% [INE SSH to *.ine.com] %FORWARD_TCP+="snpp" %% Simple Network Paging Protocol %FORWARD_UDP+="domain" %% DNS/Bind %FORWARD_UDP+="ntp" %% Network Time Protocol %FORWARD_UDP+="snmp" %% Simple Net Mgmt %FORWARD_UDP+="domain ntp snmp 407 443 500 1419" %FORWARD_UDP+="domain ntp snmp 407 443 500 1419" %FORWARD_UDP+="domain ntp snmp 407 443 500 1419" %FORWARD_UDP+="domain ntp snmp 407 443 500 1419" %FORWARD_UDP+="1701 1812 4500 snmp 3389 10000" %FORWARD_UDP+="407" %% [Timbuktu] %FORWARD_UDP+="55555" %% [amhosting.com monitor] %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. #======================================================================= #======================================================================= #======================================================================= 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 First, all leading and trailing and are stripped. Then, substitute each "[ /t]*,?[ \t]*" with " ". Parse the string, using 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 First, all leading and trailing and 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 %%= assignment %%="text" string assignment (escape '"' with '\"') %%+="text" String extend: original new %%if conditional test %%else conditional else %%fi conditional end %% This is a comment, to end of line. EXPRESSION EVALUATION: 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.) SKELETON for nftables (Python coding): ====================================== SKELETON = """\ %%include "fw_parameters" %%strict_routing=1 table inet 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 %%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 } 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; } 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; } chain OUTPUT { type nat hook output priority -100; policy accept; } chain POSTROUTING { type nat hook postrouting priority srcnat; policy accept; } } table inet 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_UDP } 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 } } """