前言
本文介绍如何在 macOS 下使用 sing-box 替代 wireguard-tools + Xray + Proxifier 的组合方案,实现全局代理、多 VPN 网络共存与开机自启。
旧方案存在几个痛点:
- 工具分散:需要 Xray(代理)+ Proxifier(全局代理)+ wireguard-tools(多 VPN)三个工具配合
- 手动启动:每次开机需要分别启动 Proxifier,并手动执行
sudo wg-quick up加入各个 VPN 网络 - 虚拟机不兼容:Proxifier 基于 NetworkExtension 实现,无法将代理共享给基于 NAT 的 macOS 虚拟机(比如基于 Tart 的虚拟机)
- 官方 WireGuard 限制:Mac App Store 的 WireGuard 客户端不支持同时连接多个 VPN 网络,所以 userspace wireguard 暂时是唯一解
sing-box 通过 TUN 模式在 L3 层接管流量,本质上是对系统路由表的配置。意味着:
- 一个工具即可同时实现全局代理并实现多 WireGuard VPN 兼容
- 基于 NAT 的虚拟机可自动继承宿主机的代理和 VPN 配置
- 可通过
brew services实现开机自启,无需手动介入
环境说明
网段规划
我需要同时接入两个 VPN 网络:
- 家庭网络:192.168.188.0/24
- 公司网络:172.31.0.0/16
DNS 解析策略
- 内网 DNS:我在我家的内网,部署了内网 DNS 服务,服务器地址是:192.168.188.140,负责解析 .jinmiaoluo.com 下的内网服务,当域名在我自己的 DNS 上没有内网记录时,DNS 服务会自动通过公网获取可能存在的公网解析记录
- 公司内网服务 都是 .company.com 域名,域名解析到 172.31.0.0/16 这类内网 IP,必须通过 VPN 才能访问
SSH 安全策略
所有公网服务器的 SSH 登录请求,默认走代理服务器,从而提供唯一的出口 IP。
所有公网暴露 SSH 服务的服务器,通过防火墙限制 SSH 端口只有代理服务器的出口 IP 才能访问。好处:
- 彻底隐藏 SSH 服务:SSH 端口对互联网不可见,端口扫描无法发现
- 消除暴力破解风险:攻击者无法尝试密码或密钥爆破
- 简化密钥管理:即使使用弱密码或旧密钥,攻击面也被网络层阻断
下面的 sing-box 配置基于以上策略。
操作
- 通过 brew 安装 sing-box
brew install sing-box
- 移除了敏感性的 sing-box 配置如下。参考下面的配置添加你的配置即可。
{
"log": {
"level": "error",
"timestamp": true
},
"dns": {
"servers": [
{
"tag": "local-dns",
"type": "udp",
"server": "192.168.188.140",
"server_port": 53,
"detour": "home"
},
{
"tag": "china-dns",
"type": "https",
"server": "223.5.5.5",
"server_port": 443,
"tls": {
"server_name": "dns.alidns.com"
}
},
{
"tag": "proxy-dns",
"type": "https",
"server": "1.1.1.1",
"detour": "proxy"
},
{
"tag": "fakeip-dns",
"type": "fakeip",
"inet4_range": "198.18.0.0/15"
}
],
"rules": [
{
"domain_suffix": [".jinmiaoluo.com"],
"action": "route",
"server": "local-dns"
},
{
"domain_suffix": [".company.com"],
"action": "route",
"server": "china-dns"
},
{
"rule_set": "geosite-cn",
"action": "route",
"server": "china-dns"
},
{
"query_type": ["A", "AAAA"],
"action": "route",
"server": "fakeip-dns"
},
{
"action": "route",
"server": "proxy-dns"
}
],
"strategy": "ipv4_only",
"independent_cache": true
},
"endpoints": [
{
"tag": "home",
"type": "wireguard",
"private_key": "xxx",
"address": [
"10.66.153.175/16"
],
"mtu": 1280,
"peers": [
{
"public_key": "xxx",
"pre_shared_key": "xxx",
"address": "xxx",
"port": 51820,
"allowed_ips": [
"10.66.0.0/16",
"192.168.0.0/16"
],
"persistent_keepalive_interval": 15
}
]
},
{
"tag": "company",
"type": "wireguard",
"private_key": "xxx",
"address": ["10.99.128.68/16"],
"mtu": 1280,
"peers": [
{
"public_key": "xxx",
"pre_shared_key": "xxx",
"address": "xxx",
"port": 51820,
"allowed_ips": [
"10.99.0.0/16",
"172.31.0.0/16"
],
"persistent_keepalive_interval": 15
}
]
}
],
"inbounds": [
{
"tag": "tun-in",
"type": "tun",
"address": [
"172.19.0.1/30"
],
"mtu": 9000,
"auto_route": true,
"strict_route": true,
"stack": "mixed"
}
],
"outbounds": [
{
"tag": "proxy",
"type": "vless",
"server": "xxx",
"server_port": 443,
"uuid": "xxx",
"flow": "xtls-rprx-vision",
"tls": {
"enabled": true,
"server_name": "proxy.example.com",
"alpn": ["h2", "http/1.1"],
"utls": {
"enabled": true,
"fingerprint": "chrome"
}
}
},
{
"tag": "direct",
"type": "direct"
}
],
"route": {
"auto_detect_interface": true,
"default_domain_resolver": "proxy-dns",
"rules": [
{
"inbound": "tun-in",
"action": "sniff",
"timeout": "1s"
},
{
"type": "logical",
"mode": "or",
"rules": [
{ "protocol": "dns" },
{ "port": 53 }
],
"action": "hijack-dns"
},
{
"rule_set": "geosite-category-ads-all",
"action": "reject"
},
{
"ip_cidr": [
"10.66.0.0/16",
"192.168.0.0/16"
],
"outbound": "home"
},
{
"ip_cidr": [
"10.99.0.0/16",
"172.31.0.0/16"
],
"outbound": "company"
},
{
"port": 123,
"network": ["udp"],
"outbound": "direct"
},
{
"type": "logical",
"mode": "or",
"rules": [
{ "port": 853 },
{ "network": "udp", "port": 443 },
{ "protocol": "stun" }
],
"action": "reject"
},
{
"rule_set": [
"geosite-google"
],
"outbound": "proxy"
},
{
"ip_is_private": false,
"port": 22,
"outbound": "proxy"
},
{
"rule_set": ["geosite-cn", "geosite-private"],
"outbound": "direct"
},
{
"rule_set": "geoip-cn",
"outbound": "direct"
},
{
"domain_suffix": [
".jinmiaoluo.com",
".company.com"
],
"outbound": "direct"
},
{
"ip_is_private": true,
"outbound": "direct"
}
],
"rule_set": [
{
"tag": "geosite-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
"download_detour": "proxy"
},
{
"tag": "geosite-private",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-private.srs",
"download_detour": "proxy"
},
{
"tag": "geosite-google",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-google.srs",
"download_detour": "proxy"
},
{
"tag": "geosite-category-ads-all",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ads-all.srs",
"download_detour": "proxy"
},
{
"tag": "geoip-cn",
"type": "remote",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
"download_detour": "proxy"
}
],
"final": "proxy"
},
"experimental": {
"cache_file": {
"enabled": true,
"path": "cache.db",
"store_fakeip": true
}
}
}
- 创建数据目录并写入配置文件
mkdir -p /opt/homebrew/var/lib/sing-box
tee /opt/homebrew/etc/sing-box/config.json > /dev/null << 'EOF'
# 将上面的 JSON 配置粘贴到这里
EOF
- (可选)调试配置
在启动服务前,可以通过手动运行 sing-box 来验证配置是否正确:
sudo /opt/homebrew/opt/sing-box/bin/sing-box run \
--config /opt/homebrew/etc/sing-box/config.json \
--directory /opt/homebrew/var/lib/sing-box
手动运行时日志会直接输出到终端,便于排查问题。如需更详细的日志,可以将配置中的 log.level 从 error 改为 debug。
- 启动服务
sudo brew services start sing-box
启动后即可完成全局代理 + 多 VPN 网络的配置。后续开机会自动启动。
补充说明
关于 sniff
配置中通过 route 规则 "action": "sniff" 对 tun-in 入站流量启用协议嗅探,从 TLS SNI 或 HTTP Host 中提取真实域名,用于后续路由规则匹配。"timeout": "1s" 指定嗅探超时时间,超时后放弃嗅探、继续路由。
sniff 相关配置在 sing-box 1.11.0 版本发生了重大变更(将在 1.13.0 正式移除旧字段,详见 Migration):
inbound.sniff已废弃,迁移到 route 规则的"action": "sniff"中inbound.sniff_override_destination已废弃且被移除,没有等效替代。开发者认为"用嗅探到的域名替换目标 IP"是从 V2Ray 继承的错误设计(参考)。新版 sniff 只提取域名用于路由匹配,不会替换目标 IP。这正好符合我们的需求——内网域名通过 local-dns 解析到内网 IP 后,目标地址始终保持为该内网 IP
关于 VPN 路由规则使用 192.168.0.0/16
在配置中,家庭网络的实际网段是 192.168.188.0/24,但 VPN 路由规则使用了更大的 192.168.0.0/16:
"ip_cidr": [
"10.66.0.0/16",
"192.168.0.0/16"
],
"outbound": "home"
这样设计的目的是利用路由前缀树(最长前缀匹配)的优先级原理:
- 当在外面时:访问 192.168.188.x 会匹配 192.168.0.0/16 规则,流量走 VPN 回家
- 当回家接入 WiFi 后:系统会添加 192.168.188.0/24 的本地路由
- 192.168.188.0/24 比 192.168.0.0/16(24 VS 16)更具体,优先级更高
- 结果:本地路由自动生效,VPN 路由规则被覆盖,内网访问不再绕行 VPN
这避免了在家时访问内网服务仍然绕行 VPN 的问题。
关于移动热点场景下的 DNS 配置
当 Mac 通过 iPhone 热点上网时,系统会自动使用 iPhone 分配的 DNS。这会导致一个问题:
- 查询
nas.jinmiaoluo.com - 运营商 DNS 不知道内网记录,返回公网 IP(或 NXDOMAIN)
- 内网 DNS 服务器(192.168.188.140)的记录无法生效
虽然 sing-box 配置了 hijack-dns 规则来劫持 DNS 流量,但 macOS 在某些场景下可能绕过 TUN 接口直接查询系统 DNS。
解决方案:手动将系统 DNS 指向 TUN 接口地址,强制所有 DNS 查询经过 sing-box 处理。
操作路径:系统设置 > Wi-Fi > 点击已连接网络旁的 详细信息 > DNS > 添加 172.19.0.1
配置后,DNS 查询流程变为:
- 应用发起 DNS 查询,目标地址 172.19.0.1
- 流量进入 TUN 接口,被
hijack-dns规则拦截 - sing-box DNS 模块按规则分流:
.jinmiaoluo.com走内网 DNS,其他按规则处理 - 内网 DNS 请求通过 WireGuard VPN 发出,返回正确的内网 IP
关于拦截 DoT、QUIC 和 STUN
配置中有一条路由规则拦截了三类流量:
{
"type": "logical",
"mode": "or",
"rules": [
{ "port": 853 },
{ "network": "udp", "port": 443 },
{ "protocol": "stun" }
],
"action": "reject"
}
为什么要拦截这三类流量:
- 端口 853(DNS over TLS):部分应用(如 Firefox)内置了 DoT/DoH 客户端,会绕过系统 DNS 直接向公共 DNS 服务器发起加密查询。这些查询不经过 sing-box 的 DNS 劫持规则,导致内网域名解析失败、FakeIP 机制失效。拦截 853 端口后,应用会回退到普通 DNS(端口 53),被
hijack-dns规则正常捕获 - UDP 443(QUIC):QUIC 协议自带拥塞控制,当流量经过代理链路(VLESS over TLS)时,QUIC 的拥塞控制与代理链路的 TCP 拥塞控制会互相干扰,导致吞吐量下降。拦截 QUIC 后浏览器会自动回退到 HTTP/2 over TCP,代理链路只有一层拥塞控制,性能更好
- STUN 协议(WebRTC):浏览器通过 STUN 协议进行 NAT 穿透,用于发现本机公网 IP。如果 STUN 请求绕过代理直连,会暴露真实 IP 地址。拦截 STUN 可以防止 WebRTC IP 泄露
总结
这是目前我在 macOS(26.2) 上能找到的比较完美的方案。同时解决下面的问题。
- 全局代理:所有流量经过 TUN 接口,无需为每个应用单独配置代理
- 多 VPN 网络共存:同时连接家庭和公司的 WireGuard VPN,按目标 IP 自动分流,并解决了路由冲突的问题
- 完全自动化:配置完成后通过
brew services开机自启,日常使用无需任何手动操作
2026 年 1 月 25 日深夜,于北京路 1200bookshop。