Skip to content

Instantly share code, notes, and snippets.

@Vicfred
Last active February 22, 2026 14:30
Show Gist options
  • Select an option

  • Save Vicfred/d844efd23a99b797dc0ab013f3f36fd3 to your computer and use it in GitHub Desktop.

Select an option

Save Vicfred/d844efd23a99b797dc0ab013f3f36fd3 to your computer and use it in GitHub Desktop.
openbsd pf firewall and nftables config

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
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment