我心目中最強的 FreeBSD 封包過濾器 (pf.conf)

8 min

language: ja en es hi zh-cn zh-tw

你好,我是無能。

這次我想介紹一下我最近在折騰的封包篩選器(Packet Filter)設定。

目前的設定

目前是這樣的。
在 Packet Filter 的情況下,
NAT、rdr 等轉換系規則會套用最先匹配(First Match)的規則,
而 pass、block 等篩選規則則是套用 最後匹配(Last Match) 的規則,
這是它的規格設計。

在這種情況下,為了易讀性,我將所有的篩選規則都加上了 quick,讓它在匹配到的當下就套用規則。
不過,對於想要廣域記錄日誌的 fail2ban block 規則,我會將其放在最後,以確保能完整記錄。
我會盡可能在各處留下註釋。

set skip on lo
set block-policy drop
set optimization conservative
set state-policy if-bound
set ruleset-optimization basic
wanint="vtnet0"
# WireGuard Configuration
wg_ifs  = "{ wg0, wg1 }"
wg_net = "10.1.0.0/24"
wg_ports="{51820}"
table <wg_clients> const { 10.1.0.2, 10.1.0.4, 10.1.0.22 }
#### -- Scrub Rules -- ####
scrub in on $wanint all random-id max-mss 1360
scrub out on $wanint all random-id max-mss 1360
scrub in all
scrub out all
#### -- NAT Rules -- ####
# WireGuard Clients NAT
nat on $wanint inet from <wg_clients> to any -> ($wanint)
block all
#### -- Fail2Ban Rules -- ####
anchor "f2b/*"
pass out quick keep state
#### -- UDP Rules -- ####
pass in quick on $wg_ifs from $wg_net to any keep state
pass in quick on $wanint proto udp from any to ($wanint) port $wg_ports keep state
# HTTP/3 Protocol
pass in quick on $wanint proto udp to ($wanint) port 443 keep state
#### -- TCP Rules -- ####
# Mail Protocols
pass in quick proto tcp from any to ($wanint) port {25, 465, 993, 995} synproxy state
# HTTP
pass in quick on $wanint proto tcp to ($wanint) port {80, 443} modulate state

set block-policy drop

所有不符合規則的封包都會被 drop(丟棄)。
如果是 return,在 TCP 的情況下會回傳 RST 通知對方並關閉連線。
至於 UDP,因為本來就沒有 TCP 連線,所以只會回傳 ICMP port unreachable

set optimization conservative

這是優化規則,在文件中被視為保守設定。
如果是 aggressive,會優先讓連線過期,但在像 WireGuard 這種需要維持常時連線的環境中,可能會因為暫時沒有新封包而導致連線中斷。

另外還有 satellite 這種設定,感覺挺浪漫的呢。

       high-latency
             A high-latency environment (such  as  a  satellite  connec-
             tion).
       satellite
             Alias for high-latency.

set state-policy if-bound

透過維持並記憶 state(狀態)來允許封包通過,以防止不一致。
pfctl -ss 的結果就是 state,例如在以下配置中:

用戶端 ←→ wg ←→ 伺服器

由於 state 會關聯「是在哪個介面建立的」這項資訊,因此只允許通過相同介面的通訊,藉此防止不一致。

在一般的 floating 情況下:
入口:wg0
出口:vtnet0
回程:vtnet0 或 wg0 都可以

但在 state-policy if-bound 的情況下:
入口:wg0
出口:vtnet0
回程:必須是 wg0,否則就不行(NG)

set ruleset-optimization basic

它會自動幫你優化規則。
節錄自官方文件:

  1. remove duplicate rules

  2. remove rules that are a subset of another rule

  3. combine multiple rules into a table when advanta-geous

  4. re-order the rules to improve evaluation perfor-mance

中文翻譯:

  1. 刪除重複的規則

  2. 刪除被其他規則包含的規則

  3. 根據需要將多個規則彙整到表格中

  4. 為了提升評估效能而重新排列規則順序

也就是說,即使是隨便寫寫的 pf 規則,它也能幫你整理得很乾淨。只要啟用這個功能,就算你按照自己覺得好讀的方式來寫 pf 規則,它也會自動幫你整理好,真是個神功能。

變數定義

指定 WAN 介面名稱

wanint="vtnet0"

定義為分配到的 IPv4 位址,且作為明確的出口 IP

exsrv1 = "163.44.113.145"

WireGuard 上的變數定義與表格定義

wg_ifs  = "{ wg0, wg1 }"
wg_net = "10.1.0.0/24"
wg_ports="{51820}"
table <wg_clients> const { 10.1.0.2, 10.1.0.4, 10.1.0.22 }

Scrub Rules

在這種情況下,不論是從 WAN 進來的封包還是出去的封包,都會指定 MSS 值。
"}all ramdom-id(原文如此,應為 random-id)似乎是為了防止 OS 本身從封包中被識別,但在 GNU/Linux 上透過 sudo tcpdump -n -v -i $interface 確認後,預設似乎已經啟用了

04:15:18.538048 IP (tos 0x0, ttl 52, id 61686, offset 0, flags [DF], proto TCP (6), length 52)
04:15:18.538049 IP (tos 0x0, ttl 52, id 61687, offset 0, flags [DF], proto TCP (6), length 131)
04:15:18.538179 IP (tos 0x0, ttl 64, id 65430, offset 0, flags [DF], proto TCP (6), length 52)
04:15:18.538223 IP (tos 0x0, ttl 52, id 61688, offset 0, flags [DF], proto TCP (6), length 131)

此外,為了讓 WAN 介面以外也能進行 scrub,也設定了 in allout all

scrub in on $wanint all random-id max-mss 1360
scrub out on $wanint all random-id max-mss 1360
scrub in all
scrub out all

NAT Rules

特定的 WireGuard 用戶端會透過 WireGuard 伺服器端的 WAN 介面進行 NAT 出網
不過,在這種情況下只能透過 IPv4 出網。這是因為 WireGuard 伺服器端沒有分配 IPv6 位址,所以只使用了 inet from

nat on $wanint inet from <wg_clients> to any -> ($wanint)

Fail2Ban Rules

這是為了讓 Fail2Ban 將攻擊來源 IP 加入 pf 表格並進行封鎖的規則。放在最後,透過 anchor "f2b/*" 將所有未匹配到規則的封包流向 Fail2Ban 規則中已加入 pf 表格的 IP。
因為它需要比設定為 quick 的過濾規則更早匹配,所以放在上方。
在此之前有預設規則 block all,因此不匹配此處的封包都將被 block

/usr/local/etc/fail2ban/action.d/pf.conf:

# Option: block
#
# The action you want pf to take.
# Probably, you want "block quick", but adjust as needed.
# If you want to log all blocked use "blog log quick"
block = block quick

根據此設定,會加入 block quick 的 Fail2ban 規則,因此將其放在上位。

也就是說,首先透過 anchor "f2b/*" 將來自 pf 表格中 IP 的封包流向 Fail2Ban 規則,接著

anchor "f2b/*"

作為過濾規則,首先放置允許流出封包的規則。

pass out quick keep state

UDP Rules

WireGuard 伺服器用的允許規則。
允許 51820/udp 並允許 $wg_ifs 上所有的 WireGuard 虛擬網卡
這裡為了以防萬一,或許應該設定得更嚴格一點?

pass in quick on $wg_ifs from $wg_net to any keep state
pass in quick on $wanint proto udp from any to ($wanint) port $wg_ports keep state

在這種情況下,由於已經維持了 state,所有通過 WireGuard 隧道的封包都將被允許。如果順序相反,pass in quick on $wg_ifs from $wg_net to any keep state 規則是以 WireGuard 隧道為前提的,所以反過來就無法成立。

為了啟用 HTTP/3,允許 443/udp

# HTTP/3 Protocol
pass in quick on $wanint proto udp to ($wanint) port 443 keep state

TCP Rules

用過 nmap 等工具的人應該知道,TCP 的埠掃描速度非常快,僅僅更改埠號,攻擊者很快就能發現。而 UDP 埠的 nmap 掃描非常耗時,因此為了更安全,我設定為只能透過 WireGuard 隧道存取 SSH。換句話說,實質上將 TCP 封裝在 UDP 中的配置應該是最安全的。因為引入了 Fail2ban,如果發生 SSH 攻擊,攻擊來源 IP 會被加入 pf 表格並封鎖。這實質上就像是一個蜜罐。

關於郵件協定,啟用了 synproxy 以防止 SYN Flood 攻擊。
在這種情況下的 HTTP,使用 ($wanint) 指定目的地 IP 是為了動態匹配分配給 WAN 介面的 IP 位址。沒有明確指定 inet 是為了同時允許 IPv4 和 IPv6。

來自 IPv6 的封包因為使用了 ($wanint) 指定目的地 IP,將會動態匹配分配給 WAN 介面的 IPv6 位址。

# Mail Protocols
pass in quick proto tcp from any to ($wanint) port {25, 465, 993, 995} synproxy state
# HTTP
pass in quick on $wanint proto tcp to ($wanint) port {80, 443} modulate state

以上,寫得有點長了...。

Related Posts