几步搞定headscale + tailscale局域网异地组网互通

本文将介绍两种常见的异地组网场景配置方法。假设家庭局域网为 192.168.1.0/24。
场景一:单向访问局域网(出门在外访问家里设备)
通过在家中配置一台转发设备,让外网设备能够访问家里的局域网。
1. 转发设备(子网路由器,家里那台)
开启内核转发
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf验证:sysctl net.ipv4.ip_forward 返回 1。
启动 Tailscale
tailscale up --login-server=https://your-headscale --accept-routes --advertise-routes=192.168.1.0/24两个参数可以共存,作用互不干扰:
如果这台只做单向出口(只提供不需要访问别的局域网),可以不加
--accept-routes;加上也无害。
防火墙:如果用了 ufw,把 /etc/default/ufw 里 DEFAULT_FORWARD_POLICY 改成 ACCEPT。
2. Headscale 服务端
批准路由
headscale nodes approve-routes -i <节点ID> -r 192.168.1.0/24注意事项:
-
•
-r是全量覆盖,不是追加。多条要一次写全:-r 192.168.1.0/24,10.0.0.0/24 -
• 不带
-r执行 = 清空所有已批准路由 -
•
-r ""显式清空 -
•
-r的网段必须和节点广播的字面完全一致,不匹配的条目不报错但不生效
确认结果
headscale nodes list-routesApproved 和 Serving (Primary) 两列都出现该网段才算成功。只有 Approved 说明节点没在广播。
3. 其他局域网设备(访问端)
开启接受路由
tailscale set --accept-routes-
• Linux 默认关闭,必须显式开
-
• macOS / Windows / iOS / Android 默认开启,对应 GUI 选项为 Use Tailscale subnets(中文界面「使用 Tailscale 子网」)
-
•
tailscale set只改这一项,不会覆盖其他已有设置
确认收到路由
# 客户端视角:哪个 peer 提供了哪些网段
tailscale status --json | jq -r '.Peer[] | select(.PrimaryRoutes) | "\(.HostName): \(.PrimaryRoutes | join(","))"'
# 系统视角:路由是否装进内核
ip route | grep tailscale # macOS: netstat -rn | grep 192.168
tailscale status普通输出不显示子网网段,只列节点和 100.x 地址,看不到是正常的。
排查对照
现象 原因 Approved 有、 Serving 空 节点没在广播,或网段字面不一致 服务端 Serving 有、客户端 PrimaryRoutes 空 客户端没重连,或 ACL 拦截 客户端 PrimaryRoutes 有、 ip route 无 --accept-routes 没开,或本地网段撞车 路由都正常但 ping 不通 转发未开、防火墙 FORWARD 链、返程路由
网段撞车:客户端所在局域网若与远程网段相同(如两边都是 192.168.1.0/24),本地直连路由优先,远程永远访问不到。只能改掉其中一边的网段,改完重新广播并批准。
返程路由:局域网内其他设备回包给 100.64.0.0/10 会走默认网关而丢失。转发设备的 --snat-subnet-routes=true(默认值)会做 SNAT 规避此问题,代价是内网侧看不到真实来源 IP。
场景二:双向局域网互通(site-to-site)
目标:两个局域网内的设备都不装 tailscale,也能互相访问。做法是把 tailscale 装在两边的软路由(网关)上,互为对方的子网路由器。
示例:局域网 1 为 192.168.10.0/24,局域网 2 为 192.168.20.0/24。
下面两条命令中的
--snat-subnet-routes和--stateful-filtering仅 Linux 可用,详见本节末尾的参数说明。
软路由 A(局域网 1)
tailscale up --login-server=https://your-headscale \
--advertise-routes=192.168.10.0/24 \
--accept-routes \
--snat-subnet-routes=false \
--stateful-filtering=false软路由 B(局域网 2)
tailscale up --login-server=https://your-headscale \
--advertise-routes=192.168.20.0/24 \
--accept-routes \
--snat-subnet-routes=false \
--stateful-filtering=falseheadscale 分别批准
headscale nodes approve-routes -i <A的ID> -r 192.168.10.0/24
headscale nodes approve-routes -i <B的ID> -r 192.168.20.0/24为什么内网设备不用装 tailscale
192.168.10.10 访问 192.168.20.20 时,包发给默认网关(软路由 A),A 的路由表里有 192.168.20.0/24 dev tailscale0,直接转发。回程同理。因为 tailscale 就跑在网关本身,不需要在客户端或路由器上额外加静态路由——这是装在软路由上最大的好处。
两个额外参数
⚠️ 这两个都是 Linux 专属参数。在 macOS / Windows 上跑
tailscale up -h看不到它们,属正常现象(同样看不到的还有--netfilter-mode)。确认时务必在软路由本机上执行 help,不要在 mac 上查。两者在
tailscale up和tailscale set上都支持,官方 site-to-site 文档用的就是tailscale up写法。
--snat-subnet-routes=false
-
• 默认开启 SNAT,会把源地址改写成对端路由器的内网 IP。单向访问时它能掩盖返程路由问题,但双向互通场景下会让日志、ACL、基于源 IP 的访问控制全部失真。关掉后保留真实源地址。
-
• 关掉后必须解决返程路由:内网设备回包给对端网段时要能找到路。在软路由做网关的场景下天然满足(网关自己就有路由),但如果 tailscale 装在非网关的机器上,则需要在路由器上加静态路由。
-
• 已知问题:同一节点既做子网路由器又做 exit node 时,设置此项可能导致上游丢包。官方建议把两个角色拆到不同节点。软路由若同时开了
--advertise-exit-node要注意。 -
• 官方还建议配合 iptables 做 MSS clamping(
--clamp-mss-to-pmtu),避免大包在隧道里分片失败。
--stateful-filtering=false
-
• 有状态过滤会丢弃「不属于已有连接」的入站包,导致只能单向发起连接。
-
• 1.66 引入时默认开启,造成大量 site-to-site 断连;当前版本已改回默认关闭,所以多数情况可以省略这个参数。
-
• 显式写上仍然稳妥(防止某些发行版打包时改了默认值),但如果你的版本报 flag 不存在,说明版本过老,直接去掉即可。
-
• 反过来说:关掉它意味着子网路由器会接受任意入站包,配合 SNAT 关闭时存在被诱导发起大量 wg 握手的 DoS 风险。内网环境可接受,暴露在不可信网络时需自行补防火墙规则。
补充说明
HA 场景注意:如果两台软路由广播的是同一个网段(主备冗余),不要给它们都加 --accept-routes——备机会接受主机广播的路由,导致本该直连的流量绕道主机。本文档的两网段互通场景不受影响。
其余要求:两台软路由都要开内核转发(同第 1 节);headscale 若配了自定义 ACL,需放行两网段互访,默认全通则不用管。
验证顺序:先从软路由 A 上 ping 192.168.20.20——通了说明隧道和路由没问题,再从 192.168.10.10 ping 一次;后者不通而前者通,问题就在转发或防火墙 FORWARD 链。