# refw rule-ordering file (nft backend).
#
# Each target is a category; deps establish ordering (topological sort in
# order()). A target's tab-indented lines are VERBATIM nft rule fragments -
# they are emitted, in dependency order, into the body of chain "screen"
# (the base chain hooked to input, policy drop). Fragments are literal nft
# syntax minus the "add rule <table> <chain>" prefix, which render() adds.
#
# Targets named set-<scope>-<action> carry no verbatim text; render()
# generates match lines for them (port-aware for user/system when --ports
# was used; otherwise plain ip saddr @set).
#
# A trailing "?" marks a host-type / feature-conditional target. render()
# skips inactive ones via target_active():
#   preamble-realed? / https-public-realed? → isrealed()
#   preamble-web? / http-public-web? → isweb() (not realed/dev/vpn; apache
#                                               sites-enabled)
#   fail2ban?            → feature "fail2ban" (etc/features or auto-detect)
#   ratelimit?           → feature "ratelimit" (soft crawl; needs apply + tc)
#   set-feed-{allow,drop}?→ feature "feed" (etc/features; default on type web)
#
# Precedence policy (first match wins, so category order is precedence):
#   0. structural preamble - lo, established/related (explicit drops
#      therefore affect new connections only), icmp, admin SSH pinholes
#   1. explicit allows - user, system, feed sets; an allow is a full
#      accept and always wins, including over a fail2ban ban
#   2. explicit drops/rejects - fail2ban ban, user/system/feed drops,
#      user/system rejects
#   3. generic rules - ratelimit, public port accepts, policy drop
#
# Scopes:
#   user   - named logins (reuser accounts / userip); key = username
#   system - staff fwrule/fwnet (and similar site policy); key = rule name
#   feed   - threat feeds (geo lists, IP-blocking services)
#   fail2ban volatile sets (ban / ratelimit)

preamble:
	iif lo accept
	# Internal VPC net on the default-route NIC: render() substitutes
	# <default-iface> (falls back to the device from the last routed
	# apply, then to no interface qualifier at all - see render()).
	iifname "<default-iface>" ip saddr 10.0.0.0/8 accept
	ct state related,established accept
	# code 4 = fragmentation-needed (Buster nft 0.9 has no frag-needed keyword)
	icmp type destination-unreachable icmp code 4 accept
	ip protocol icmp ct state new limit rate 2/second accept

preamble-realed?: preamble
	ip saddr 146.148.47.157 tcp dport 59824 accept
	ip saddr 146.148.47.157 tcp dport 59822 accept

# Web hosts: admin SSH is port 22 from a1; internal 10/8 rides the preamble.
# Without this, activate on a web host cuts off SSH (no DB-driven allows).
preamble-web?: preamble
	ip saddr 146.148.47.157 tcp dport 22 accept

# Tier 1: explicit allows.
allow-user-set: preamble-realed? preamble-web? set-user-allow

allow-system-set: allow-user-set set-system-allow

feed-allow-set: allow-system-set set-feed-allow?

# Tier 2: explicit drops/rejects, all before any generic accept.
fail2ban?: feed-allow-set
	ip saddr @f2b_ban drop

drop-user-set: fail2ban? set-user-drop

drop-system-set: drop-user-set set-system-drop

feed-drop-set: drop-system-set set-feed-drop?

reject-user-set: feed-drop-set set-user-reject

reject-system-set: reject-user-set set-system-reject

# Tier 3: generic rules.
# Ambiguous flood: cap new HTTP(S) connections; replies marked for tc 1mbit
# crawl (output chain installed by render when feature "ratelimit" is on).
# Before public 80/443 accepts so listed IPs are limited, not open.
ratelimit?: reject-system-set
	ip saddr @f2b_ratelimit tcp dport { 80, 443 } ct state new limit rate over 10/minute burst 20 packets drop

# Public listeners: RealEd sites serve the app on 443; pure web hosts also
# open 80. After every explicit drop so listed sources stay blocked.
https-public-realed?: ratelimit?
	tcp dport 443 accept

http-public-web?: ratelimit?
	tcp dport { 80, 443 } accept

epilogue: <previous-rules>
