Use FreeBSD

『在 AI 盛行的今天,老手艺人还在坚持手搓。』

2026 年使用 FreeBSD,或许就是这样的描述。早年刚开始用 Linux 时,就接触过 BSD 系列。毕竟它们都和 Unix 脱不开干系。这些年偶尔回头看一眼,像小时候街中心最显眼位置的玩具,心里一直惦记大人却不让买。下载镜像,看看命令,看看界面,然后浅尝辄止。我始终回答不了一个实际的问题:它能解决什么麻烦?如果已经能用 NixOS,或者别的什么 Linux 把事情做好,为什么还要花时间重新适应。或许单纯可以是好奇心,但真正能让事情发生的,通常不是兴趣,而是问题。直到今年才遇到了这样一个问题,它足够顽固地恼人,终于我想也许这件事该交给 BSD 来做。

几年前,在一篇博客里提到过,自己用 NixOS DIY 了一台家庭网络路由器。它负责 VLAN 管理、IP 分配、防火墙策略、流量管理等一系列任务,作为核心路由器勤勤恳恳地工作了三年,稳定可靠,没掉过什么链子。其实我还想用它干更多事儿:不仅是路由器,也是透明代理网关。也就是说,根据 geo 或 domain list 之类的策略,把家里所有设备的流量自动分流到不同的出口隧道。

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

后来加入了另一台机器,是配置相当老土的 2014 年款 Mac,原装 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 访问时,事情就不再那么直观容易。它不是不能工作,但它更像一个封装得很好的 all-in-one 独立产品,不是一个可以完全支配的网络组件。

所以,我想要一台独自工作但能完美融合到现有网络的透明网关。用 Linux 没能干成,Surge 同样。既然如此,为什么不让 BSD 试试看呢?

没怎么花时间纠结 BSD 的选择,看到社区更活跃的 FreeBSD,把它安装到了这台 Mac mini 上。作为流量网关,在思路上和 Linux 其实是一致的:都是由防火墙先负责重定向流量,再把流量交给分流服务,由后者根据域名或 IP 策略把请求导向不同出口。真正的区别在于防火墙和网络配置的方式,FreeBSD 使用了 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 之间来回推敲。它只是按照预期开始工作了起来。

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

每个人的家庭网络都不太一样,我并不打算在这里贴出所有细节配置。但如果你和我一样,一直想试试 BSD 但缺少一个理由,不妨听听我的回答:BSD 能带你给朴素的信心