背景
在当前的工作环境下,公司对网络合规和出口流量审计的要求日益严苛。办公网络不仅严查各类 VPN 协议,还会对所有通往境外的连接记录进行深度审计。为了在满足个人上网需求的同时,彻底避免将个人设备的代理链路暴露在办公网络出口上,我构建了一套基于“组网即代理”的方案。
核心思路是利用家里的宽带作为唯一的“科学上网”出口,而办公环境下的手机仅作为一个纯粹的内网接入点,通过自建的 Tailscale 网络连接回自家的电脑进行“打洞”。具体设计动机如下:
- 规避办公网审计:通过将真正的代理入口放在家里的 Windows 电脑上,手机在办公网环境下只产生与国内服务器(阿里云)或家宽公网 IP 的通信。所有对境外的访问流量均被封装在隧道内部,并最终在家宽出口解封,从而确保办公网出口没有任何境外 IP 的访问记录。
- 阿里云控制面(Headscale):为了实现稳定且不经过境外的控制平面,我在国内的阿里云 ECS 上部署了 Headscale(Tailscale 的开源替代方案)。需要注意,阿里云在这里仅作为**控制面(Control Plane)**负责节点发现和 NAT 穿透握手,并不负责数据中转。流量最终是通过 P2P 打洞直连到家里的,这样既保证了低延迟,也避免了阿里云因流量中转产生高昂的带宽费用。
- 完全物理隔离:本方案实现了个人访问链路与办公网络的物理级隔离。办公网出口看到的只是前往阿里云的合规 HTTPS 流量,真正的互联网访问逻辑完全托管在自有设备和自有宽带上。
整体思路
- 阿里云 ECS 上部署 Headscale,作为 Tailscale 控制面(仅负责握手,记得关闭或不使用 DERP 中转,以节省阿里云流量)。
- 家里的 Windows 电脑 加入这个 tailnet,作为代理网关,获得内网 IP
100.64.0.2。 - 家里电脑 本地运行 SOCKS5 服务(如 Clash/v2ray),并监听
100.64.0.2:10808端口。 - Android 手机 使用 sing-box for Android,通过内置的 Tailscale endpoint 登录 Headscale。
- 手机流量路由:Android 的 TUN 流量通过 Tailscale 隧道转发到家里的
100.64.0.2:10808。
技术架构示意图:
flowchart LR
subgraph Phone["Android 手机"]
App["App 流量"]
Tun["sing-box TUN"]
TsEndpoint["Tailscale endpoint
ts-ep"]
end
subgraph Cloud["阿里云 ECS"]
Headscale["Headscale 控制面
:8443"]
end
subgraph Home["家里宽带"]
Windows["Windows 网关
100.64.0.2"]
Socks["SOCKS5 服务
100.64.0.2:10808"]
HomeWan["家里宽带出口"]
end
App --> Tun --> TsEndpoint
TsEndpoint -.->|登录 / 节点发现 / NAT 打洞| Headscale
TsEndpoint ==>|加密 tailnet 连接| Socks
Windows --- Socks
Socks --> HomeWan
这样手机侧的链路是:
1 | Android App 流量 |
注意:这只是个人设备访问个人授权网络的记录。实际使用时要严格遵守所在单位、学校和网络服务提供方的安全合规准则。
版本要求
Android 端需要使用支持 sing-box 1.14 新字段的 SFA/sing-box 版本。实测 sing-box for Android 1.14.0-alpha.21 以上可用。
低版本如果导入下面的配置,可能会报类似错误:
1 | unknown field accept_search_domain |
原因是 accept_search_domain、control_http_client、dns_mode、preferred_by 等字段属于较新的 sing-box 配置格式。
Headscale 服务端配置
服务端部署在阿里云 ECS,公网地址、域名和密钥在这里都做了脱敏。
采集到的版本:
1 | headscale version v0.28.0 |
配置文件路径:
1 | /etc/headscale/config.yaml |
核心配置如下:
1 | server_url: https://<HEADSCALE_DOMAIN>:8443 |
同时创建 /etc/headscale/derp-stun-only.yaml,只下发 STUN,不提供可用 DERP 中继。这里可以配置多个 STUN 节点,让客户端在多出口网络里探测到更多 NAT 映射候选:
1 | regions: |
这里的效果是:Headscale 继续提供 STUN 给 tailscale netcheck 和打洞探测使用,但 DERP 中继端口不可用,打洞失败时不会通过服务器中转流量。多 STUN 的意义是增加公网映射候选,尤其适合学校、公司这类可能按目标 IP 分配不同出口的网络;如果某个 STUN 服务器和真实 peer 目标走到同一个出口,直连打洞成功率会更高。
systemd 服务使用发行版默认的 headscale serve:
1 | [Service] |
节点列表脱敏后大概是这样:
1 | 100.64.0.1 headscale-server linux |
其中 100.64.0.2 是家里的 Windows 电脑,也是 Android 最终要访问的 SOCKS5 节点。
家里 Windows 电脑的 Tailscale 配置
Windows 端不需要额外写复杂配置,核心就是在 Headscale 服务端生成一个预授权 key,然后在 Windows 上用这个 key 加入自建控制面。
在 Headscale 服务端生成 Windows 节点使用的 key:
1 | headscale preauthkeys create --user 1 --expiration 24h |
如果希望这个 key 可以给多台设备重复使用,可以加 --reusable:
1 | headscale preauthkeys create --user 1 --expiration 24h --reusable |
然后在家里的 Windows 电脑上执行:
1 | tailscale up --login-server https://<HEADSCALE_DOMAIN>:8443 --auth-key <WINDOWS_AUTH_KEY> --hostname home-windows-pc --accept-dns=true --accept-routes=true |
这台电脑上需要有一个 SOCKS5 服务监听 10808。如果只监听 127.0.0.1:10808,tailnet 里的手机访问不到;需要确保它监听在 0.0.0.0:10808,或者至少监听到 Tailscale 网卡的 100.64.0.2:10808,同时 Windows 防火墙允许 tailnet 访问这个端口。
Android sing-box 配置
Android 端不要同时开官方 Tailscale App 的 VPN。Android 通常只能稳定运行一个 VPN,SFA 的 TUN 已经占用 VPN 入口,所以这里让 sing-box 自己内置一个 Tailscale endpoint。
Android 端同样需要先在 Headscale 服务端生成一个 auth key:
1 | headscale preauthkeys create --user 1 --expiration 24h |
如果只是给手机导入一次配置,建议使用一次性 key,不加 --reusable。生成出来的 key 填到下面 sing-box 配置的 auth_key 字段里。
完整配置如下,auth_key 已脱敏。这一份是基础版,默认最终出站都是家里电脑的 SOCKS5:
1 | { |
国内外分流版本
上面的基础版会把手机侧流量默认全部送到家里电脑的 100.64.0.2:10808。如果希望国内站点直连、国外站点仍然走家里 SOCKS5,可以使用下面这个分流版。它使用 geosite-cn、geoip-cn 做国内直连,使用 geosite-geolocation-!cn 做国外代理,国内 DNS 走 223.5.5.5,国外 DNS 走 Cloudflare DoH 并通过家里 SOCKS5 出口。没有命中规则的流量默认直连,避免微信这类国内 App 因规则缺失误走隧道。
这个版本会在首次启动时下载远程 .srs 规则集,后续由 cache_file 缓存。注意 cn-dns 这个 UDP DNS 服务器不要写 "detour": "direct",新版 sing-box 会报 detour to an empty direct outbound makes no sense,因为 UDP DNS 默认就是直连。这里也不再全局阻断 UDP 443;阻断 QUIC 虽然能让部分连接回退到 TCP,但放在国内规则前面容易误伤微信、小程序和国内 App。
分流版完整配置如下,auth_key 和 Headscale 域名已脱敏:
1 | { |
关键点是这个出站:
1 | { |
detour: ts-ep 表示连接 SOCKS5 服务本身时先走 Tailscale endpoint。没有这个字段,Android 的普通网络无法直接访问 100.64.0.2。
分流规则的关键点是:微信和 QQ 先按包名强制 direct,避免国内 IM 流量误走家里 SOCKS5;geosite-geolocation-!cn 明确送到 proxy,用于国外站点;final 保持 direct,让未知流量默认直连。不要把 UDP 443 阻断规则放在国内规则前面,否则微信、小程序或国内 App 的 QUIC/UDP 连接可能先被拦截,表现为发消息慢、加载卡顿。
启动顺序
- Headscale 服务端先启动,确认
server_url能访问。 - 家里 Windows 电脑登录 Headscale,确认拿到
100.64.0.2。 - 家里电脑启动 SOCKS5 服务,确认
100.64.0.2:10808可访问。 - Android 导入 sing-box 配置,启动 SFA。
- 在 Headscale 上查看
sfa-android节点是否在线。
常用检查命令:
1 | headscale nodes list |
Windows 上检查:
1 | tailscale status |
NAT 类型也要检查一下。Tailscale 打洞最怕的是两端都是对称型 NAT;办公网这边是对称型 NAT 问题不大,只要家宽这一端不是对称型 NAT,通常仍然可以打出直连。如果两端都是对称型 NAT,P2P 直连大概率失败,最终会退回 DERP 中转。
在家里 Windows 电脑上跑:
1 | tailscale netcheck |
在办公网络侧也找一台同网段设备跑同样的命令:
1 | tailscale netcheck |
重点看输出里的 MappingVariesByDestIP。如果是 true,基本可以认为这一侧是对称型 NAT 或 hard NAT;如果是 false,打洞条件会好很多。本方案里最关键的是家宽侧尽量保持 MappingVariesByDestIP: false。
sing-box 配置可以先用本地源码检查:
1 | go run -tags "with_gvisor,with_tailscale" ./cmd/sing-box check -c .\sing-box-android-tailscale.json |
排错
如果 Android 报 unknown field accept_search_domain,升级 SFA/sing-box 到 1.14.0-alpha.21 以上。
如果 Android 能登录 Headscale 但不能访问 100.64.0.2:10808,先检查家里电脑上的 SOCKS5 是否监听在 Tailscale 可访问的地址上,再检查 Windows 防火墙。
如果 Headscale 控制面域名解析失败,确保 Android 配置里的 control_http_client.domain_resolver 指向 local,避免控制面解析也走还没建立起来的代理链路。
如果 DNS 查询异常,确认 dns_mode 是 hijack,并且 route.rules 里有 port: 53 的 hijack-dns 规则。
脱敏清单
这篇文章里隐藏了这些内容:
- Headscale 服务器公网 IP。
- Headscale 域名。
- SSH 密码。
- Tailscale auth key。
- 节点公钥、机器密钥和私钥路径。
- 证书文件真实路径。
真实环境里不要把 auth key、SSH 密码、私钥、节点 key 写进博客仓库。首次注册成功后,也建议把一次性 auth key 作废。
说些什么吧!