OpenBSD PF config at /etc/pf.conf:
# -------------------------------------------------------------------------
# PF ruleset: 2 LANs (em1/em2) -> NAT out WAN (em0)
# - Default deny
# - Quiet WAN (drop inbound)
# - NAT for both LANs
# - Force clients to use router Unbound (block outbound 53/853 from LANs)
# - Isolate LAN A and LAN B
# - Unbound upstreams: explicit DNS-over-TLS (TCP/853) to chosen providers
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
# LAN ISOLATION (Crucial Step)
# -------------------------------------------------------------------------
# Explicitly block traffic between LANs.
# Since we have "block all" above, we just need to make sure our PASS rules
# below don't accidentally allow it.
# The safest way is to use "! <lan_nets>" in the pass rules.
# -------------------------------------------------------------------------
# MACROS
# -------------------------------------------------------------------------
lan_if_a = "em1"
lan_if_b = "em2"
lan_ifs = "{ em1 em2 }"
lan_net_a = "172.16.0.0/24"
lan_net_b = "172.16.1.0/24"
wan_if = "em0"
# 80 -> HTTP
# 443 -> HTTPS
# 853 -> DoT: DNS over TLS
# 1119 -> Blizzard client
# 1935 -> RTMP
# 3724 -> WoW login
tcp_services = "{ 80, 443, 1119, 1935, 3724 }"
# 53 -> UDP (Only allowed to the router)
# 123 -> NTP
# 443 -> QUIC (HTTPS-like)
# 3478 -> Tailscale STUN (Session Traversal Utilities for NAT)
# 21027 -> Syncthing Local Discovery (Broadcast in LAN only)
# 41641 -> Tailscale Data-plane
# 51820 -> Wireguard
udp_services = "{ 123, 443, 3478, 41641, 51820 }"
table <lan_nets> const { $lan_net_a $lan_net_b }
table <dot_upstreams> const { 9.9.9.9 194.242.2.4 }
# -------------------------------------------------------------------------
# OPTIONS & NORMALIZATION
# -------------------------------------------------------------------------
block quick inet6
set skip on lo
match in all scrub (no-df)
set block-policy drop
antispoof quick for $wan_if
antispoof quick for $lan_ifs
# -------------------------------------------------------------------------
# NAT (LAN -> WAN)
# -------------------------------------------------------------------------
match out on $wan_if inet from <lan_nets> to any nat-to ($wan_if)
# -------------------------------------------------------------------------
# DEFAULT POLICY
# -------------------------------------------------------------------------
block all
block in quick on $wan_if
# -------------------------------------------------------------------------
# LAN -> Router (services you provide)
# -------------------------------------------------------------------------
# SSH to router from LANs
pass in on $lan_ifs inet proto tcp from <lan_nets> to self port 3322 keep state
# DNS to router (Unbound on the router)
pass in on $lan_ifs inet proto udp from <lan_nets> to self port 53 keep state
pass in on $lan_ifs inet proto tcp from <lan_nets> to self port 53 keep state
# Syncthing local discovery (broadcast per LAN)
pass in quick on $lan_if_a inet proto udp from $lan_net_a to 172.16.0.255 port 21027 no state
pass in quick on $lan_if_b inet proto udp from $lan_net_b to 172.16.1.255 port 21027 no state
# -------------------------------------------------------------------------
# LAN -> WAN (client egress)
# -------------------------------------------------------------------------
# Allow LAN clients to ping out (and PMTU stuff)
pass in on $lan_ifs inet proto icmp from <lan_nets> to !<lan_nets> keep state
# Allow router to ping LAN hosts
pass out on $lan_ifs inet proto icmp from self to <lan_nets> keep state
# Allow clients to ping the router
pass in on $lan_ifs inet proto icmp from <lan_nets> to self keep state
# HTTPS / RTMP(S) etc
pass in on $lan_ifs inet proto tcp from <lan_nets> to !<lan_nets> port $tcp_services keep state
pass in on $lan_ifs inet proto udp from <lan_nets> to !<lan_nets> port $udp_services keep state
# -------------------------------------------------------------------------
# DNS hardening hooks
# -------------------------------------------------------------------------
# Allow ONLY DoT from the router to known upstreams (common):
pass out quick on $wan_if inet proto tcp from ($wan_if) to <dot_upstreams> port 853 keep state
block out quick on $wan_if inet proto tcp from ($wan_if) to any port 853
# Router's own outbound (updates, DoT, etc.)
pass out on $wan_if inet from ($wan_if) to any keep state
Common to all, load from /etc/nftables.conf:
#!/sbin/nft -f
flush ruleset
include "/etc/nftables.d/*.nft"
Magi desktop at /etc/nftables.d/magi.nft:
table inet base {
chain input {
type filter hook input priority -200; policy drop;
# Kill IPv6 completely
meta nfproto ipv6 reject
# Enable traces for nft monitor trace
# meta nftrace set 1
# Return traffic
ct state { established, related } accept
# Loopback
iifname "lo" accept
# ICMPv4 (optional but useful)
ip protocol icmp accept
# DHCP client
iifname "br0" udp sport 67 udp dport 68 accept
#
# --- LAN (br0) ---
#
# Tailscale direct UDP needs to hit the physical interface too
iifname "br0" udp dport 41641 accept
# Allow AzireVPN
iifname "br0" udp dport 51820 accept
# SSH from LAN
#iifname "eno0" ip saddr 172.16.0.0/24 tcp dport 3322 accept
iifname "br0" ip saddr 172.16.0.0/24 tcp dport 3322 accept
#ip saddr 172.16.0.0/24 tcp dport 3322 accept
# Allow SSH from Tailscale
iifname "tailscale0" tcp dport 3322 accept
# HTTPS service (nginx on :443) - allow from LAN and from Tailscale
iifname "br0" ip saddr 172.16.0.0/24 tcp dport 443 accept
iifname "tailscale0" tcp dport 443 accept
# Syncthing (device-to-device)
iifname "br0" ip saddr 172.16.0.0/24 tcp dport 22000 accept
iifname "br0" ip saddr 172.16.0.0/24 udp dport 22000 accept # only needed if you use QUIC
iifname "azirevpn-mx-qro" tcp dport 22000 accept
iifname "azirevpn-mx-qro" udp dport 22000 accept # only if QUIC enabled
#tcp dport 22000 accept
#udp dport 22000 accept # only if QUIC enabled
# Syncthing Relay WAN (necessary?)
#tcp dport 22067 accept
#udp dport 22067 accept # only if QUIC enabled
# Syncthing local discovery (LAN broadcast/multicast)
#iifname "br0" ip saddr 172.16.0.0/24 udp dport 21027 accept
#iifname "azirevpn-mx-qro" udp dport 21027 accept
udp dport 21027 accept
#
# --- libvirt (virbr0) guest -> host DNS/DHCP ---
#
iifname "virbr0" udp dport { 53, 67 } accept
iifname "virbr0" tcp dport 53 accept
# Log what gets blocked
# limit rate 5/second burst 20 packets log prefix "nft drop input: " flags all
}
chain forward {
type filter hook forward priority -200; policy drop;
# Kill IPv6 completely
meta nfproto ipv6 reject
# Return traffic
ct state { established, related } accept
# Allow libvirt guests to go out via LAN bridge
iifname "virbr0" oifname "br0" accept
# Allow guest-to-guest on virbr0
iifname "virbr0" oifname "virbr0" accept
}
chain output {
type filter hook output priority -200; policy drop;
# Kill IPv6 completely
meta nfproto ipv6 reject
# Enable traces for nft monitor trace
# meta nftrace set 1
# Return traffic
ct state { established, related } accept
# Loopback
oifname "lo" accept
# ICMPv4 (ping, PMTU discovery-ish)
ip protocol icmp accept
# DHCP client
oifname "br0" udp sport 68 udp dport 67 accept
#
# Base stuff
#
# DNS
udp dport 53 accept
tcp dport 53 accept
# NTP
udp dport 123 accept
# HTTP/HTTPS
tcp dport { 80, 443 } accept
# QUIC (HTTP/3)
udp dport 443 accept
#
# VPN overlays
#
oifname "br0" udp dport 51820 accept # AzireVPN WG
udp dport 41641 accept # Tailscale direct UDP
udp dport 3478 accept # Tailscale DERP/STUN
# Syncthing outbound (device-to-device + discovery)
oifname "br0" ip daddr 172.16.0.0/24 tcp dport 22000 accept
oifname "br0" ip daddr 172.16.0.0/24 udp dport 22000 accept # if QUIC enabled
#oifname "azirevpn-mx-qro" tcp dport 22000 accept
#oifname "azirevpn-mx-qro" udp dport 22000 accept # only if QUIC enabled
#tcp dport 22000 accept
#udp dport 22000 accept # if QUIC enabled
# Syncthing Relay WAN
#tcp dport 22067 accept
#udp dport 22067 accept # only if QUIC enabled
# Syncthing Discovery on LAN (broadcast/multicast)
oifname "br0" udp dport 21027 accept
#oifname "azirevpn-mx-qro" udp dport 21027 accept
#udp dport 21027 accept
# SSH
tcp dport { 22, 3322 } accept
# Allow sshd replies (if conntrack is missing/broken)
ip daddr 172.16.0.0/24 tcp sport 3322 accept
# Portage rsync
tcp dport 873 accept
# NFS
tcp dport 2049 accept
# Sometimes I need this e.g., speedtest
# tcp dport { 5060, 8080 } accept
# Log what gets blocked
# limit rate 5/second burst 20 packets log prefix "nft drop output: " flags all
}
}
sylvanas laptop at /etc/nftables.d/sylvanas.nft:
table inet base {
chain input {
type filter hook input priority -200; policy drop;
# Kill IPv6 completely
meta nfproto ipv6 reject
# Return traffic
ct state { established, related } accept
# Loopback
iifname "lo" accept
# ICMPv4 (optional but useful)
ip protocol icmp accept
# DHCP client
iifname "wlp0s20f3" udp sport 67 udp dport 68 accept
# Tailscale needs this on the *physical* interface to form direct connections
iifname "wlp0s20f3" udp dport 41641 accept
# Allow AzireVPN
iifname "wlp0s20f3" udp dport 51820 accept
# SSH ONLY on these interfaces
#iifname "tailscale0" tcp dport 3322 accept
#iifname "azirevpn-mx-qro" tcp dport 3322 accept
#iifname "wlp0s20f3" tcp dport 3322 accept
iifname "wlp0s20f3" ip saddr 172.16.0.0/24 tcp dport 3322 accept
#ip saddr 172.16.0.0/24 tcp dport 3322 accept
#tcp dport 3322 accept
# Syncthing (device-to-device)
iifname "wlp0s20f3" ip saddr 172.16.0.0/24 tcp dport 22000 accept
iifname "wlp0s20f3" ip saddr 172.16.0.0/24 udp dport 22000 accept # only needed if you use QUIC
iifname "azirevpn-mx-qro" tcp dport 22000 accept
iifname "azirevpn-mx-qro" udp dport 22000 accept # only if QUIC enabled
#tcp dport 22000 accept
#udp dport 22000 accept # only if QUIC enabled
# Syncthing Relay WAN (necessary?)
#tcp dport 22067 accept
#udp dport 22067 accept # only if QUIC enabled
# Syncthing local discovery (LAN broadcast/multicast)
#iifname "wlp0s20f3" ip saddr 172.16.0.0/24 udp dport 21027 accept
#iifname "azirevpn-mx-qro" udp dport 21027 accept
udp dport 21027 accept
# Allow libvirt guest to reach the host.
iifname "virbr0" udp dport { 53, 67 } accept
iifname "virbr0" tcp dport 53 accept
#iifname "virbr0" accept
# Log what gets blocked
limit rate 5/second burst 20 packets log prefix "nft drop input: " flags all
}
chain forward {
type filter hook forward priority -200; policy drop;
# Kill IPv6 completely
meta nfproto ipv6 reject
# Return traffic
ct state { established, related } accept
# Allow VMs on virbr0 to go out to the internet
iifname "virbr0" oifname "wlp0s20f3" accept
# Allow forwarding between VMs on virbr0
iifname "virbr0" oifname "virbr0" accept
}
chain output {
type filter hook output priority -200; policy drop;
# Kill IPv6 completely
meta nfproto ipv6 reject
# Enable traces for nft monitor trace
# meta nftrace set 1
# Return traffic
ct state { established, related } accept
# Loopback
oifname "lo" accept
# ICMPv4 (ping, PMTU discovery-ish)
ip protocol icmp accept
# DHCP client
oifname "wlp0s20f3" udp sport 68 udp dport 67 accept
# DNS
udp dport 53 accept
tcp dport 53 accept
# NTP
udp dport 123 accept
# Web essentials (package mirrors, git over https, general browsing)
tcp dport { 80, 443 } accept
# QUIC (HTTP/3)
udp dport 443 accept
# Allow AzireVPN
oifname "wlp0s20f3" udp dport 51820 accept
# Tailscale control + DERP + direct UDP
udp dport 41641 accept
udp dport 3478 accept
# Syncthing outbound (device-to-device + discovery)
oifname "wlp0s20f3" ip daddr 172.16.0.0/24 tcp dport 22000 accept
oifname "wlp0s20f3" ip daddr 172.16.0.0/24 udp dport 22000 accept # if QUIC enabled
#oifname "azirevpn-mx-qro" tcp dport 22000 accept
#oifname "azirevpn-mx-qro" udp dport 22000 accept # only if QUIC enabled
#tcp dport 22000 accept
#udp dport 22000 accept # if QUIC enabled
# Syncthing Relay WAN
#tcp dport 22067 accept
#udp dport 22067 accept # only if QUIC enabled
# Syncthing Discovery on LAN (broadcast/multicast)
oifname "wlp0s20f3" udp dport 21027 accept
#oifname "azirevpn-mx-qro" udp dport 21027 accept
#udp dport 21027 accept
# SSH
tcp dport { 22, 3322 } accept
# Allow sshd replies (if conntrack is missing/broken)
ip daddr 172.16.0.0/24 tcp sport 3322 accept
# Portage rsync
tcp dport 873 accept
# NFS
tcp dport 2049 accept
# Sometimes I need this e.g., speedtest
# tcp dport { 5060, 8080 } accept
# Log what gets blocked
# limit rate 5/second burst 20 packets log prefix "nft drop output: " level info
}
}
Japan VPS server at /etc/nftables.d/cyberia.nft:
table inet cyberia_fw {
set ssh_allow_v4 {
type ipv4_addr
flags interval
elements = {
10.10.0.0/24,
69.166.236.89,
79.127.229.245
}
}
set dns_v4 {
type ipv4_addr
elements = { 9.9.9.9, 194.242.2.4 }
}
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif "lo" accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# public services
tcp dport { 80, 443, 1935 } accept
# WireGuard
udp dport 51820 accept
# SSH only from allowlist (IPv4 only)
tcp dport 3322 ip saddr @ssh_allow_v4 accept
}
chain forward {
type filter hook forward priority 0; policy accept;
}
chain output {
type filter hook output priority 0; policy drop;
ct state established,related accept
oif "lo" accept
ip daddr 127.0.0.0/8 accept
ip6 daddr ::1 accept
# TEST: allow host -> containers / private networks
ip daddr { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } accept
ip6 daddr fc00::/7 accept
# TEST: allow anything leaving docker bridges (covers weird container IPs)
oifname "docker0" accept
oifname "br-*" accept
# hard block SMTP (keep this even for testing)
tcp dport { 25, 465, 587 } drop
# (keep the rest as-is)
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
udp dport 53 ip daddr @dns_v4 accept
tcp dport 53 ip daddr @dns_v4 accept
tcp dport 853 ip daddr @dns_v4 accept
udp dport 123 accept
tcp dport { 80, 443 } accept
tcp dport 873 accept
}
}