在 macOS 上,断网开关的好坏取决于其背后的包过滤策略,而 pf 正是执行该策略的部分。策略必须同时做两件相反的事:让客户端能到达自己的服务器,从而建立隧道;并阻止其他一切流量直接外发。大多数公开的规则集做到了第二点却忘了第一点,于是机器失去的是连通性而不是获得保护。真正可用的版本只点名端点、解析器和隧道,不多不少。

包过滤器能决定什么,不能决定什么

pf 只看接口、地址、协议号和端口,到此为止。它永远不知道是哪个进程打开了套接字,因此为某个程序写的许可,会变成所有能到达同一端口的程序的许可。一条宽松的 UDP 例外,就这样把断网开关变回普通网络。能感知应用的阻断属于客户端或系统扩展,而不是这个文件。

过滤器也不创建路由。客户端掉线时,默认路由可能仍指向一条死隧道,pf 会把这种情况变成被阻断的数据包,而不是悄悄回退到 Wi-Fi。阻断是预期结果,不是需要绕开的故障。

决定结果的往往不是规则文字,而是顺序。没有 quick 时,pf 在命中后仍继续评估,最后一条匹配的规则胜出,于是靠下的一条宽泛放行会覆盖上面那些谨慎的例外。给放行加上 quick 会让首次匹配即定案,这通常才是策略作者的本意。

先读取接口和端点的事实

无线接口在不同 Mac 上编号并不相同,隧道设备的编号也可能在多次启动之间变化。点名不存在设备的规则会在解析阶段失败,这种错误很响、容易发现;而点名错误但确实存在的设备,规则会干净地解析通过,并放行你本想阻断的流量。

要在客户端连接时收集事实,因为隧道设备只在那个时候存在。你需要持有默认路由的接口、客户端创建的设备,以及它拨号的服务器地址和端口。最后这个值正是大多数规则集漏掉的,而漏掉它正是照抄的规则集会阻断自己隧道的原因。

route -n get default
ifconfig -l
scutil --nc list

第一条命令指出当前胜出的接口,第二条列出内核知道的所有设备,第三条显示系统能启动的 VPN 配置。把这些答案记下来,然后从客户端自身的配置里读出服务器地址和端口。

围绕隧道通路来写规则集

下面的文件默认拒绝,并重新放行五样东西:回环、隧道设备、客户端到其服务器的通路、隧道建立前使用的解析器,以及维持无线链路可用的两次交换。接口名和服务器地址来自你的机器,请替换成你确认过的值。隧道编号也不会和你的相同。

set skip on lo0
block all
pass out quick on utun0
# maintain this line when the server address changes
pass out quick on en1 inet proto udp to 203.0.113.10 port 1194
# only the resolver the client uses before the tunnel exists
pass out quick on en1 inet proto udp to 192.0.2.53 port 53
pass out quick on en1 inet proto udp from any port 68 to any port 67
pass out quick on en1 inet proto udp to 224.0.0.251 port 5353

DHCP 那一行存在,是因为 Mac 在拿到地址之前就会请求地址,所以请求从端口 68 发往端口 67,且没有可用的源地址。多播那一行让系统判断无线链路是否值得使用,去掉它会导致机器在正常网络上报告没有连接。端点那一行 OpenVPN 能否真正连上服务器的关键;没有它,block all 会丢掉外层握手,隧道永远起不来。解析器那一行比一般 DNS 许可更窄,因为它只点名一个地址和一个端口,而不是机器发出的每一次查询。

端点和解析器这两条例外是必须存在的,其余一律保持阻断。如果客户端拨的是字面地址,就去掉解析器那行、保留端点那行。如果服务器地址变了,就在下次连接前更新那行,或者用客户端自己的钩子填充一张 pf 表,改为匹配这张表。

先关闭隧道,然后重启

加载前先解析,因为语法错误在试运行中很便宜,在远程会话里却代价高昂。然后加载文件,若过滤器原本关闭就开启,再回读实际生效的内容。回读不是走过场:另一个锚点可能加入你没写过的规则,所以要把打印出的集合与自己的文件对照。

sudo pfctl -n -v -f /etc/pf.conf
sudo pfctl -f /etc/pf.conf
sudo pfctl -e
sudo pfctl -s rules
sudo pfctl -s Anchors
sudo pfctl -a <anchor> -s rules
sudo pfctl -s info

规则加载后,关闭隧道并尝试加载页面。普通流量应当停止,而且在机器休眠再唤醒之后仍应保持停止,因为正是在那个转换点,陈旧状态会重新渗入。重新连接并确认无需改动规则集流量就能恢复。如果隧道关闭时浏览器仍能工作,说明某条规则比你想象的更宽,回读通常能点出它。

重启正是人们会跳过的一步。macOS 在启动时读取 /etc/pf.conf,但读取文件并不等于启用过滤器,所以 现在能用的规则集在重启后可能失效。之后再运行 sudo pfctl -s info,确认状态行报告过滤器已启用。

让退出和重启路径保持诚实

全部阻断的规则集也可能把你锁在远程管理的机器之外。能本机操作时就本机编辑,把旧文件留在新文件旁边,并在需要之前就清楚回退方式。下面的命令会关闭过滤器并重新加载你保留的副本,这是应该从控制台而不是刚切断的隧道执行的顺序。五秒钟的回退每次都胜过聪明的规则。

sudo pfctl -d
sudo pfctl -f /etc/pf.conf.previous
sudo pfctl -s rules

要持久化就需要一个 launch daemon,在启动时运行 pfctl -e 并加载你的文件。像对待任何特权作业一样对待它:绝对路径、事后能读的日志,以及在你能物理接触的机器上测试。静默失败的守护进程会让机器毫无防护却看起来正常。

记录这些缺口,先从创建自己隧道设备的第二个 VPN 客户端开始。你的规则只点名一个隧道接口,所以第二个要么保持被阻断,要么被后来加入的宽泛规则命中。网络共享和热点接口只是偶尔出现,也值得同样记一笔。这些缺口都不是跳过策略的理由,它们是策略仍然成立的条件。

相关文章