Use FreeBSD

在 AI 盛行的今天,老手艺人还在坚持手搓。到了 2026 年还在使用 FreeBSD,大概就有这样的味道。早年我刚开始用 Linux 时,就接触过 BSD 系列。毕竟它们都和 Unix 脱不开干系。这些年我也偶尔回头看一眼,像小时候路过街中心最显眼的橱窗,心里一直惦记,可大人又不让买的那件玩具。每次我都会停下来,下载镜像,看看命令,看看界面,然后浅尝辄止。原因很简单,我始终回答不了一个足够实际的问题:它能替我解决什么麻烦?如果我已经能用 NixOS,或者别的 Linux 发行版把事情做完,那我为什么还要花时间重新适应一套系统?对一个工程师来说,兴趣当然重要,但真正能让迁移成本落地的,通常不是兴趣,而是问题。直到今年,我终于遇到了这样一个问题。它不算惊天动地,但它足够顽固,顽固到让我开始重新审视:也许这件事该交给 BSD 来做。

几年前,我在一篇博客里提到过,自己用 NixOS DIY 了一台家庭网络路由器。它负责 VLAN 管理、IP 分配、防火墙策略、流量管理等一系列任务,作为核心路由器勤勤恳恳地工作了三年,稳定、可靠,几乎没出过什么毛病。但其实,我当时还想给它更大的职责范围:我希望它不仅是一台路由器,也是一台可靠的透明代理网关。也就是说,在负责基础路由的同时,它还能根据 geo 或 domain list 之类的策略,把家里所有设备的流量自动分流到不同的出口隧道,而不是依赖客户端自己去配置代理。

在 Linux 下,我当时主要尝试的是 TPROXY,用它来承担流量标记和重定向等工作。我花了很多时间去理解 iptables 的各种链路,也反复调整过 nftables 规则。但说实话,我始终没能让 TPROXY 按照预期稳定地工作:有时候 DNS 劫持不正常,有时候某些应用的流量表现不对。最折磨人的地方在于,它不是完全不能用,而是总差一点点。这其实比彻底失败更折磨人,因为你总觉得再花两个小时就能调好,可一次次的尝试后,问题(或者新的问题)还在那里。在反复微调 nftables 的过程中,我越来越烦躁,最终放弃,退而求其次,只开放了 HTTP Proxy 端口,让客户端自己决定是否使用代理。

转机出现在我得到了一台 Mac mini。这台 Mac mini 是 2014 年款,配置已经相当老了,原装的 2.5 寸机械硬盘慢得令人发指。我给它换上了 512G SSD,又把内存扩到了 16 GB。一顿折腾之后,它勉强能跑起 macOS,但离“适合日常工作”还差得很远。思来想去,我决定让它试试 NixOS 一直没能胜任的那个角色:流量网关。

Surge 的名字我很早就听过,到现在我依然认为它是 macOS 上体验最好的同类产品之一。买好许可证,点几下,它就开始工作了。什么 Fake DNS、流量标记、forward/input chain,这些在 Linux 上让我反复头疼的细节,在这里都被收拾得妥妥当当。它只是安静地接管流量。开箱即用做得太好了,以至于如果不是后来我的网络需求变得更复杂,Surge 大概就会成为这件事的终点。

我的网络里有多个 VLAN:server/homelab 所在的 main VLAN,电脑和手机使用的 go VLAN,以及给物联网设备准备的 iot VLAN。它们之间有明确的访问边界,有些设备还会同时出现在多个网段。对这类环境来说,代理网关不是一个孤立设备,它必须服从整套网络治理逻辑。也正是在这里,Surge 和我的原有网络架构产生摩擦。它接管了 go VLAN 的主要流量,甚至连 DHCP 这样的地址分配工作也一并接过去了。默认配置当然好用,但当我想让它更细致地配合现有网络,例如处理跨 VLAN 访问、保留原有的职责边界时,事情就不再那么直观了。它不是不能工作,而是它更像一个封装得很好的独立产品,而不是一个我可以完全支配的网络组件。

所以,我需要的不仅仅是一个很好用的代理软件,而是一台行为可预期、职责边界清楚、能够服从我现有网络设计的透明网关。Linux 没有把这件事解决到让我满意,商业软件也没有。既然如此,为什么不让 BSD 试试看呢?

我没怎么花时间纠结 BSD 的选择,很快就下载了社区更活跃的 FreeBSD,并把它安装到了这台老 Mac mini 上。作为流量网关,这件事在思路上和 Linux 其实是一致的:都是由防火墙先负责重定向流量,再把流量交给分流服务,由后者根据域名或 IP 策略把请求导向不同出口。真正的区别在于防火墙和网络配置的方式,FreeBSD 使用了 pf,也正是在这里,我第一次觉得事情重新变得顺手了。pf 的规则简单、清楚,而且符合直觉。更重要的是,它写出来是什么样,运行起来大体也就是什么样。换句话说,pf 配置文件能够比较准确地表达我的需求,而不是像之前那样,要在一堆复杂规则里反复猜到底是哪一环出了问题。那种熟悉的工程直觉终于回来了:配置文件不再像某种需要反复试探的仪式,而更像是一份可以被信任的说明书。

这套配置里,真正核心的规则其实只有三条:NAT、DNS redirect 和 TCP redirect。这不是精简后的示意,全部 pf 规则确实只有这些:

# PF firewall rules for transparent proxy gateway
# Transparent proxy for VPN VLAN (10.10.40.0/24)
#
# Traffic flow:
#   DNS:  VPN VLAN clients -> PF rdr :53 -> AdGuard Home (filtering + rewrites)
#         -> Xray DNS :15353 (split domains) -> internet
#   TCP:  VPN VLAN clients -> PF rdr -> Xray tproxy (127.0.0.1)
#         -> Xray outbound via ext_if (default route) -> internet

# Interfaces
ext_if = "{{ vpn_interface }}"
int_if = "{{ vpn_interface }}"

# Xray ports
xray_port = "{{ xray_tproxy_port }}"
dns_port = "{{ xray_dns_port }}"

# AdGuard Home DNS port
adguard_dns_port = "{{ adguard_dns_port }}"

# --- NAT ---
# Masquerade outbound traffic from VPN VLAN (for direct connections via Xray)
# Exclude same-subnet destinations so DNS replies to local clients keep source port 53 on single-NIC hosts.
nat on $ext_if from {{ vpn_network }}/24 to !{{ vpn_network }}/24 -> ($ext_if)

# --- Redirects ---
# DNS: redirect all DNS from VPN VLAN clients to AdGuard Home (ad filtering + local records).
rdr on $int_if proto { udp, tcp } from {{ vpn_network }}/24 to any port 53 -> 127.0.0.1 port $adguard_dns_port

# TCP: redirect all TCP from VPN VLAN clients to Xray transparent proxy
# Skip traffic destined to the gateway itself (avoid redirect of local management)
rdr on $int_if proto tcp from {{ vpn_network }}/24 to !{{ vpn_ip }} -> 127.0.0.1 port $xray_port

# Pass everything else (traffic filtering is handled by Xray routing rules)
pass all

事情一下子变得很安静。没有额外的流量标记,不需要在不同 chain 之间来回推敲,也不需要在 DNS 和 TCP 两条路径上分别猜测是不是又有哪里没接上。它就是稳定地工作了起来。

对基础设施来说,安静是很高的评价。因为它意味着你终于可以把注意力从排障转移到建设上:我在这台机器上接着部署了 ntopng 做流量监控,又写了新的 dashboard 来看 metrics。在 AI 的帮助下,这些配套工作都推进得很顺。不是因为它们本身更简单了,而是因为底层系统终于不再持续制造噪音。

由于每个人的家庭网络都不一样,这篇文章不打算贴出所有细节配置。对这类系统来说,能工作的方案往往高度依赖现场条件,抄作业的价值未必高。但如果你和我一样,一直想试试 BSD 但缺少一个理由,不妨听听我的回答:朴素的信心。