tailscale 为什么不能像 OpenVPN 那样用 via 路由访问对端局域网
九月 04, 2026 更新: 十月 07, 2026 [网络与安全] #tailscale #Linux #routing #wireguard #tun背景:想在 tailscale 网络里"借用"对端访问其局域网
本机(tailscale 100.64.0.1)想临时访问对端节点(100.64.0.2)所在站点的局域网设备 192.168.2.1。 直觉做法是加一条经对端转发的路由,就像 OpenVPN 里那样:
sudo ip r replace 192.168.2.1/32 via 100.64.0.2
结果:
Error: Nexthop has invalid gateway.
换成写进 tailscale 自己的路由表也报一样的错。而另一台 OpenVPN 机器上同样的姿势却完全正常:
# OpenVPN 机器上实测可行
ip r add 192.168.2.1 via 10.8.0.5 # 成功,且能正常访问 192.168.2.1
同样的命令形态,一个报错一个成功,差在哪?本文完整记录排查过程与背后的架构差异。
排查过程
本机 tailscale 的路由布局
$ ip route show
default via 192.168.4.1 dev bridge0 proto static metric 425
192.168.4.0/22 dev bridge0 proto kernel scope link src 192.168.5.244 metric 425
... # main 表里没有任何 100.64.0.0/10 相关路由
$ ip route show table 52
100.64.0.2 dev tailscale0
100.64.0.3 dev tailscale0
...
100.100.100.100 dev tailscale0
$ ip rule show
5270: from all lookup 52
...
关键事实:所有 tailscale peer 的 /32 路由都在独立的 table 52 里,main 表一条都没有;
ip rule 5270: from all lookup 52 让 peer IP 的查表先落到 52。
$ ip route get 100.64.0.2
100.64.0.2 dev tailscale0 table 52 src 100.64.0.1 uid 1000
但 ip route get 能查到,不代表 add 能通过——实测 main 表、table 52 两个目标表都报同样的错
(2026-09-04 于 7.0.0-28 内核复核,同结果)。
一个被推翻的假设:网关校验不是"查目标表"
最初以为是"网关必须在目标表内可达",于是试 table 52(网关 100.64.0.2 明明就在 52 里):
$ sudo ip r add 192.168.2.1/32 via 100.64.0.2 table 52
Error: Nexthop has invalid gateway. # 依然失败
勘误:早先"只查 main 表、不跟 ip rule"的归因是错的
初稿曾把报错归因于"内核校验网关时 fib_lookup() 固定只查 main 表(RT_TABLE_MAIN)、
路由添加时的 nexthop 校验不查规则链"。该解释被下面的实测推翻——真正的判官是
scope(on-link)过滤,与查哪张表无关:
实测 A(证伪"只查 main"):往 table 52 放一条专门当网关的路由、scope 写成 link
(main 表里没有它,只有经 ip rule 5270 能到):
$ sudo ip r add 100.64.0.254/32 dev tailscale0 scope link table 52
$ sudo ip r add 10.99.99.99/32 via 100.64.0.254 # 加进 main,成功!
10.99.99.99 via 100.64.0.254 dev tailscale0
网关只存在于 table 52、目标却是 main 表,却能加上——说明添加路由时校验 gateway 会走完整 ip rule 链(fib_lookup → fib_rules_lookup → table 52),并非只查 main。
实测 B(定位到 scope):关键线索在路由属性——tailscaled 写进 table 52 的 peer 路由 全部是 scope global:
$ ip -d route show table 52 | head -2
unicast 100.64.0.2 dev tailscale0 proto boot scope global
unicast 100.64.0.3 dev tailscale0 proto boot scope global
内核在添加路由时(fib_check_nh_v4_gw(),net/ipv4/fib_semantics.c)解析 gateway,
查询 scope 被钳制为 ≥ RT_SCOPE_LINK——语义是**"网关必须由一条 on-link(直连、
scope ≥ link)的路由覆盖"**;查表时 scope 不足的路由会被丢弃(fib_trie.c:
fib_scope < flowi4_scope 即跳过,global=0 < link=253)。于是 100.64.0.2 无论
main、table 52、带不带 dev,全部 invalid gateway;只有显式 dev + onlink
能跳过这套校验:
$ sudo ip r add 10.99.99.99/32 via 100.64.0.2 dev tailscale0 onlink table 52 # 成功
(反向对照:把实测 A 那条 100.64.0.254 的 scope 改成 global,via 100.64.0.254
立刻回到 invalid gateway——同一条路由只差 scope。)
那 ip route get 为什么能查到?它走独立的查询路径(inet_rtm_getroute),查询 scope 是
universe、不过滤 global 路由——所以"查得到"与"能当网关"是两个不同的问题。
就算校验过了,也到不了对端
把报错消掉很容易(不带 via 的 dev 路由不触发网关校验,带 via 的加 dev + onlink 也行,见上节实测):
sudo ip r add 192.168.2.1/32 dev tailscale0 # 能装上
ping -c2 192.168.2.1 # 预期全丢:隧道内无对端认领该目标
这就触及真正的架构差异了。
根因:两级转发模型 vs OpenVPN 的单链路模型
包进隧道后,谁决定发给谁?
tailscale0 是普通 TUN(本机实测 type tun,wireguard-go 用户态模式)。
内核路由表只负责"把包交给 tailscale0"这一级——TUN 设备不检查目标 IP 是否被允许,
任何路由进来的包都原样写进 tun fd。真正判决在第二级:
tailscaled 用户态进程从 tun 读走包后,wireguard-go 拿 dst 去匹配各 peer 的 AllowedIPs:
- 本机是 100.64.0.1,对端们是 100.64.0.2/.3/...——每个 peer 的 AllowedIPs 只有它自己的 tailscale IP(+ MagicDNS 100.100.100.100);
- 192.168.2.1 不属于任何 peer → 无匹配 → 包在用户态进程内直接丢弃;
via 100.64.0.2只是内核的"下一跳"概念,对 TUN 设备来说包原样进隧道, "发给 peer .2"这个意图根本传不到 wireguard 层。
所以这不是路由表技巧能绕过的:手工路由改的是第一级,判决在第二级,而第二级只认宣告。
OpenVPN 为什么能通
OpenVPN 的 tun0 是单对端 ptp 链路:包进隧道后没有"选谁"这回事——固定发往唯一的 server。 而 server 是显式开启转发的真实路由器(内核 ip_forward + 它自己的 LAN 路由), 所以任何你路由过去的目标,只要 server 能到达就通。
# OpenVPN 机器实测路由
10.8.0.0/24 via 10.8.0.5 dev tun0 # config 里 route 指令,程序经 netlink 写入
10.8.0.5 dev tun0 proto kernel scope link src 10.8.0.6 # 内核自动生成
顺带解答一个疑问:ip route 为什么能看到 10.8.0.5 这个"peer"?它不可能读 openvpn 配置。
看 proto kernel 标记——这条路由是内核在配置 ptp 地址时自动生成的:
openvpn 配地址用的是 10.8.0.6 peer 10.8.0.5(点对点形式),对端地址成为接口地址属性,
内核自动为链路对端建 /32 路由(与给网桥配 IP 自动生成链路路由同理)。
对比 tailscale0 的地址 100.64.0.1/32——纯 /32、没有 peer 字段(多对端不存在单一链路对端),
所以内核不会生成任何 peer 路由,对端们是 tailscaled 手工写进 table 52 的。
这同时也解释了开头那个"一个报错一个成功"的表层差异:OpenVPN 机上 via 10.8.0.5 能装上,
除了隧道路由在 main 表里,更关键的是内核自动生成的 peer 路由是 scope link
(proto kernel scope link)——正好满足上文"网关必须由 on-link 路由覆盖"的校验;
而 tailscaled 手写的 peer 路由是 proto boot scope global,在同样的校验处就被拦下。
报错与否取决于覆盖网关那条路由的 scope,与规则链、目标表都无关。
两条"peer"路由的来历:scope 与 proto 为什么不同
顺着上面两个输出再问一层:同样是"对端可达",为什么 OpenVPN 的是 proto kernel scope link、
tailscale 的是 proto boot scope global?因为两者根本不是同一种东西——生成方、生成时机都不同。
OpenVPN 的 peer 路由是"配地址"的副作用,由内核自己生成。 openvpn 配 10.8.0.6 peer 10.8.0.5
时,对端 IP 作为接口地址属性(IFA_ADDRESS)随地址一起下发;内核 fib_add_ifaddr()
(net/ipv4/fib_frontend.c)除了把本端 10.8.0.6 收进 local 表,还会为对端地址自动建一条
/32 路由,且 scope/proto 是写死的(fib_magic()):proto = RTPROT_KERNEL(标记"内核创建")、
scope = RT_SCOPE_LINK——ptp 对端在语义上就是直连对端(on-link、不经过任何中间网关),
天然满足上文网关校验对 scope 的要求。这也是 OpenVPN 的 via 10.8.0.5 能装上的直接原因。
tailscale 的"peer 路由"是 tailscaled 经 netlink 手工写入的普通 dev 路由(wgengine/router/
osrouter/router_linux.go 的 addRoute()):只填 LinkIndex(tailscale0)、Dst、Table=52 三个字段,
不带 via、也不指定 scope/proto——于是落在其 netlink 库的默认值上:scope=universe、proto=boot
(nl.NewRtMsg() 的缺省)。所以 proto boot scope global 不是设计选择,是"没设置 + 库默认":
tailscaled 自己从不用 via 指 peer(包进隧道后的归属由 wg 引擎按 AllowedIPs 判决,路由只负责
把包送进 tun),scope 写多少对它毫无影响。
侧面印证:tailscaled 在 netlink 不可用的兜底路径会退化执行 ip route add <网段> dev tailscale0
(iproute2),而 iproute2 对 dev-only 路由默认给 scope link——同一条路由若由这条路径写入,
via peer IP 就能装上(对照上节实测 A:scope link 的网关路由只放 table 52,main 表加 via 照样成功)。
两个"peer"路由的性质也不同:OpenVPN 那条描述"链路另一端是谁"(内核地址模型的一部分); tailscale 那条描述"哪些目标 IP 属于隧道"——本质是 AllowedIPs 的内核投影,随 netmap 增删、 与点对点链路毫无关系。
核心差异一图流
| OpenVPN(tun 模式) | Tailscale(本机) | |
|---|---|---|
| 隧道形态 | 每客户端 1 条到 server 的链路 | 1 个 TUN 设备对 N 个 peer |
| 包进隧道后的判决 | 无判决:固定发唯一 server | 用户态 wg-go 按 dst 匹配 AllowedIPs |
| 未宣告的目标 | 无此概念(server 来者不拒) | 直接丢弃 |
| 隧道→LAN 转发 | server 内核 ip_forward | 仅 subnet router 节点(内核 ip_forward + nftables) |
| peer 表在哪 | openvpn daemon 用户态(config 单点) | tailscaled 用户态(netmap 动态下发) |
| "peer"路由的来历 | 配地址时内核自动生成(proto kernel scope link) | AllowedIPs 的内核镜像,tailscaled netlink 写入(proto boot scope global) |
| "用 via 路由借道" | 天然成立(server 就是路由器) | 不成立(peer 默认是叶子,不承担转发) |
一句话:转发执行者两边都是内核,判决者都是用户态程序;区别是 OpenVPN 用配置把"谁当路由器" 写死成唯一的 server(无条件信任),tailscale 用宣告机制动态决定谁当路由器(默认谁都不当,最小暴露)。
解决方案
不改 tailscale 配置:以对端做跳板
peer 的 tailscale IP(100.64.0.2)现在就能通(table 52 有路由),所以:
# TCP 服务透传
ssh -L 8080:192.168.2.1:80 user@100.64.0.2
# 通用 SOCKS 代理(任意协议)
ssh -D 1080 user@100.64.0.2
# 直接登录对端操作
ssh user@100.64.0.2
要 IP 层透明直达:subnet router(唯一的正规解)
tailscale 版"让对端扮演 OpenVPN server"= 在对端宣告子网路由,本机接受:
# 对端(能路由到 192.168.2.0/24 的节点)
tailscale set --advertise-routes=192.168.2.0/24
# 本机
tailscale set --accept-routes
tailscale set 全程不掉线,撤销只需把值清空。生效过程不只是用户态配置:
- 用户态:两端 prefs 持久化;
- 控制面:对端宣告上传协调服务器,审核后把新 netmap 推给所有 peer;
- 本机内核改动:192.168.2.0/24 加入对端 AllowedIPs(本机是 wg-go 用户态持有; 内核 wireguard 模式则写入内核 wg 对象),同时 tailscaled 经 netlink 新增一条内核路由;
- 对端内核改动:nftables 加 forward/SNAT 规则,开启 ip_forward sysctl——这就是 "OpenVPN server 转发角色"的等价物。
实战坑:accept-routes 全量接受——本网段被自身通告劫持
上面"正规解"要求本机开 accept-routes,但它有一个设计限制:接受是全量的,没有按前缀过滤。 假设本机所在网段(192.168.4.0/22)里有两台机器同时向 tailnet 宣告了本网段(常见的冗余 subnet router 配置:两台都 advertise 192.168.4.0/22,供远端 192.168.2.0/24 站点访问), 本机一开 accept-routes 就会把"自己所在的网段"也收下来,后果:
- 本机布局里 tailscale 路由在 table 52、rule 5270 先于 main 表 → 本地流量查表先命中 隧道路由 → 被劫进 tailscale0,经隧道 hairpin 到另一台广告节点再绕回本网段;
- 该节点一挂,本网段访问直接断;即使在线路径也畸形(绕一圈,默认 SNAT 下源地址还变成 广告节点)。
现状核验:tailscale debug prefs 的 AdvertiseRoutes、tailscale status --json 的
Self.AllowedIPs(本机自己也在宣告时可见本网段)。三种解法(按推荐序):
A. 选择性手工路由——不开 accept-routes(实测:不成立)
只加目标网段、不开 accept 的思路看起来干净:
sudo ip r add 192.168.2.1/32 dev tailscale0
但实测包进隧道即被本机 wg 引擎丢弃(广告节点侧 tcpdump 全空)。原因是一个容易踩的视角陷阱:
tailscale status --json 里对端 AllowedIPs 显示包含通告(如 "192.168.2.1/32"),
那是 netmap 视角;而 accept-routes=false 时,wireguard 引擎实际使用的 allowedIPs 会把
对端子网通告过滤掉,只留各 peer 的 tailscale IP。用 status json 判断"wg 是否放行"会误判。
所以想走 subnet router,accept-routes 这一侧绕不开。
B. accept-routes + 高优先级 ip rule 把本网段拨回 main(实测:有效,推荐)
保留 accept 的自动全量能力,用内核规则修正冲突(rule 插在 tailscale 的 5270 之前):
sudo ip rule add pref 100 to 192.168.4.0/22 lookup main # 先加保护,再开 accept
sudo tailscale set --accept-routes
rule 100 先于 rule 5270 命中:本网段查表直接落到 main 表的本地直连,不进隧道;
远端 192.168.2.1/32 不在规则内,照走 tailscale(accept 后自动装入 table 52)。
实测结果:table 52 出现 192.168.2.1 dev tailscale0,远端 ping 通(~11ms),
本地网关 ping 仍直连(0.5ms)。等于把"全量接受"改成"按需屏蔽"——以后新增的其他远端
通告仍会自动收,需要时再补规则精确屏蔽。该规则需自行持久化,tailscaled 不会清理它管理
的规则号段之外的条目:
# /etc/systemd/system/tailscale-protect-lan.service
[Unit]
Description=Protect local LAN subnet from tailscale route shadowing
After=network-online.target tailscaled.service
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'ip rule add pref 100 to 192.168.4.0/22 lookup main 2>/dev/null || true'
ExecStop=/bin/sh -c 'ip rule del pref 100 to 192.168.4.0/22 lookup main 2>/dev/null || true'
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now tailscale-protect-lan.service
C. 先测新版 tailscaled 是否自带冲突跳过(5 秒实验,可回滚)
tailscale set --accept-routes
ip route show table 52 # 192.168.4.0/22 是否被装上?
journalctl -u tailscaled --since "1 min ago" | grep -i conflict # 有 skip 日志 = 被保护
ping -c1 192.168.4.1 # 本网段网关仍直连即安全
tailscale set --accept-routes=false # 不对就立即回滚
新版若对与本地直连冲突的 accepted 路由自动跳过(日志可见 skip),直接 accept 即可无事。
2026-10-07 复核(tailscale 1.102.4):不会跳过。 重开 accept-routes,table 52 立即装回本地网段路由、无任何 skip 日志——ip rule 豁免在当前版本仍是必配项。
排障实录:从"宣告成功"到"真正连通"的完整链路
本机与对端网络(headscale v0.29.3 + tailscale 1.102)实测踩坑全过程,按检查顺序:
-
广告节点宣告:
tailscale set --advertise-routes=192.168.2.1/32(对端能直达该目标为前提)。 -
headscale 批准:v0.29 CLI 重构后是
headscale nodes list-routes/headscale nodes approve-routes <node-id> <prefix>,不再有顶层routes命令。批准前Self.AllowedIPs不会出现该路由。勘误(2026-09-12):
approve-routes不接受位置参数,正确写法是 flag 形式headscale nodes approve-routes -i <node-id> -r <prefix>(0.29.3--help实测:只有-i/--identifier与-r/--routes)。list-routes本身不带参数,写法无误。 -
广告节点进入 serving 状态(本次最大暗坑):headscale 侧已显示 Approved + Serving、 其他节点的 netmap 也已收到通告,但广告节点自己的 netmap 迟迟不更新——
force-netmap-update无效,Self.AllowedIPs始终缺路由、PrimaryRoutes为空,必须重启 tailscaled 才进入 serving 状态并应用 netfilter 规则。 -
广告节点 netfilter 后端判定:tailscaled 日志显示
netfilter running in iptables mode—— 规则在 iptables 而非 nftables!nft list ruleset为空不代表没规则,误判会带偏排查。 正确查法:iptables -S | grep ts-与iptables -t nat -S,可见 ts-forward/ts-input/ ts-postrouting 链 + mark 0x40000 MASQUERADE,规则本身逻辑正确。 -
访问端包是否真的进隧道(定位到本机的关键证据):在广告节点
tcpdump -ni any icmp抓包全空 → 包根本没到广告节点,被访问端本地丢弃。结合第 3 节视角陷阱定位到: accept-routes=false 时 wg 引擎过滤对端通告,本机装 dev 路由也白搭。 -
解法落地:先加
ip rule pref 100 to 192.168.4.0/22 lookup main保护本网段, 再tailscale set --accept-routes→ table 52 自动出现192.168.2.1 dev tailscale0, 远端与本地 ping 同时验证通过。
关键教训:"宣告成功"与"真正连通"之间隔着 5 个独立环节(宣告→批准→节点 serving→ 广告节点 netfilter→访问端 accept),任一环节静默失效都不通;其中 status json 的 AllowedIPs 是 netmap 视角、accept-routes 才是 wg 引擎的开关、netfilter 后端要按日志判断——三个陷阱 分别浪费一轮排查。
2026-10 复发实录:跨站点同网段——宣告网段里的每台 Linux 都会中招
架构换了个形态,坑还是同一个:家庭站 192.168.2.0/24(KVM 宿主机 + 桥接 VM,网段被 tailnet 里的节点宣告供公司站远程访问),VM 为了走公司侧 192.168.4.0/22 的隧道开了 accept-routes,结果把自己所在的 192.168.2.0/24 也收进了 table 52——局域网流量 被劫进隧道 hairpin 到 subnet router 再绕回,宿主机 SSH/ping VM 全部超时。
症状极具迷惑性,识别特征:
- 宿主 → VM:ping/SSH 超时;但 ARP 正常、IPv6 直连正常、tailscale IP 正常;
- VM → 宿主:ping 通(去程绕隧道、回程走 LAN 的非对称路径),延迟 1.8ms (直连应 0.3ms)、ping 网关 TTL=63(直连应 64)——都是绕行的铁证;
- 双侧 tcpdump:echo request 确实到达 VM 网卡,但无任何回包——回包查表先命中
table 52 的
192.168.2.0/24 dev tailscale0,被送进隧道发往 subnet router, 永远回不到同网段的发起者。
"/23 短前缀宣告"为什么救不了 Linux:直觉方案是把宣告改成 192.168.2.0/23, 指望"本地 /24 更长前缀获胜"。这在 Windows/macOS 上成立(子网路由进系统常规表, 遵守最长前缀),tailscale 官方文档也正是这么建议的——但同一页随即说明 Linux 用 policy routing 引入了额外一层优先级,需配套执行:
sudo ip rule add to 192.168.2.0/24 priority 2500 lookup main
原因:策略路由是逐表顺序查找、单表内才做最长前缀——查 192.168.2.x 时 rule 5270 先命中 table 52 的 /23 即走隧道,main 表的 /24 直连根本轮不到被查。tailscale 开发者在 issue #1227 的原话: "the 'more-specific route' trick won't work on Linux. On that platform Tailscale uses policy routing and puts its route into table 52."(2026-10-07 于 1.102.4 复核: 重开 accept-routes,table 52 立即装回本地网段路由,无冲突跳过日志。)另外 /23 会 把 192.168.3.0/24 一并拉进隧道:家里没有该网段时,公司侧访问 3.x 直接黑洞。
ip rule 的持久化(运行时状态重启即丢,tailscaled 只管理自己 5210–5270 的 pref 段、不会恢复外部规则,必须自行落地)。首选 systemd oneshot(与方案 B 同构,pref 2500 取自官方文档示例,任何 < 5270 皆可):
# /etc/systemd/system/ts-lan-prefer.service
[Unit]
Description=Prefer direct LAN routing for 192.168.2.0/24 over tailscale policy routing
Documentation=https://tailscale.com/docs/reference/troubleshooting/network-configuration/lan-traffic-overlapping-subnets
After=tailscaled.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
# 幂等:ip rule add 不查重,重复执行会叠加,先 grep 判断再 add
ExecStart=/bin/sh -c "ip rule show | grep -q 'to 192.168.2.0/24 lookup main' || ip rule add to 192.168.2.0/24 priority 2500 lookup main"
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now ts-lan-prefer.service
两个实现细节:① rule 的匹配顺序由 pref 数字决定、与添加时间无关,该单元先于
tailscaled 执行也没问题;② NetworkManager 管理的机器可用原生方式
nmcli con mod <连接名> +ipv4.route-rules "priority 2500 to 192.168.2.0/24 table main"
(ipv4.route-rules 属性),随连接生命周期自动增删,无需自建单元。
最终效果(VM 实测):accept-routes=true 保持开启——公司网段照走隧道
(ip route get 192.168.4.1 → dev tailscale0 table 52),本网段豁免直连
(ip route get 192.168.2.100 → dev enp1s0)。管理员侧宣告无需改动:纯 Linux
tailnet 保留 /24 最简洁;有 Windows/macOS 设备时才值得改 /23,两种手段可叠加。
结论泛化:只要节点"住在某个被 tailnet 宣告的网段里",开 accept-routes 前必配本网段 的 ip rule 豁免——与上文"本站冗余宣告"是同一机制的两个触发面。
关键认知:tailscale 接受侧没有 OpenVPN 那样的"要不要这条路由"细粒度控制,但内核 ip rule 可以补齐这个缺口——方案 B 就是"选择性接受"的内核等价物。
附:TUN 还是 TAP,怎么判定
排查中顺手验证 tailscale0 是 TUN(三层)不是 TAP(二层):
$ ip -d link show dev tailscale0 | head -1
tailscale0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> ... tun type tun ... # type tun = TUN
$ cat /sys/class/net/tailscale0/type # 65534 = ARPHRD_NONE(TUN);TAP 会是 1
$ cat /sys/class/net/tailscale0/tun_flags # 0x5001:含 0x1(TUN_TUN_DEV) 无 0x2(TUN_TAP_DEV)
$ cat /sys/class/net/tailscale0/address # 空:TUN 无 MAC;TAP 会有
若用内核 wireguard(非 wg-go),ip -d link 会显示 type wireguard,丢弃判决会移交给
内核 wg 模块执行——但 AllowedIPs 的内容仍由 tailscaled 按 netmap 下发,规则始终是 tailscale 定的。
总结
- 报错根源(勘误:早先"
fib_lookup只查 main 表、不跟 ip rule"的归因是错的——实测 scope link 的网关路由只放 table 52、main 加 via 照样成功,说明校验走完整规则链):添加路由时内核fib_check_nh_v4_gw()要求网关由 scope ≥ link 的 on-link 路由覆盖,tailscaled 的 peer 路由全是proto boot scope global→ 被 scope 过滤判 invalid gateway。与写进 main 还是 table 52、带不带 dev 都无关,只有显式onlink能绕过;对照 OpenVPN:内核 ptp 路由是scope link,天然满足校验; - 但就算消掉报错也到不了:tailscale 多对端隧道按 AllowedIPs 在用户态判决目标归属,未宣告的子网一律丢弃,
via的意图传不到隧道内部; - OpenVPN 能行是因为单 ptp 链路无判决 + server 是显式内核转发路由器;
- 想让 tailscale 节点扮演 OpenVPN server 的角色,唯一机制是 subnet router(advertise-routes + accept-routes),会同时改动两端的内核状态;
- accept-routes 是全量接受:本网段若被其他节点宣告,一开 accept 就会收下自己的网段导致本地流量 hairpin;实测解法=先加高优先级 ip rule 把本网段拨回 main 再开 accept("选择性接受"的内核等价物,含 systemd 持久化);"不开 accept 手工路由"方案实测不成立——wg 引擎会过滤对端通告,且 status json 的 AllowedIPs 是 netmap 视角会误导判断;
- 2026-10 复发(跨站点同网段:家庭网段被宣告供公司访问,网段内的 Linux 节点开 accept-routes 即被劫持,宿主↔VM SSH 超时但 ARP/IPv6/tailscale 正常、回包进隧道):"/23 短前缀宣告让本地 /24 赢"仅适用于 Windows/macOS(常规路由表、最长前缀);Linux policy routing 逐表顺序查找、跨表无最长前缀竞争,官方文档对 Linux 的配套解即
ip rule pref 2500 lookup main+ systemd 持久化(tailscale 1.102.4 复核无自动冲突跳过); - 临时借用:ssh 跳板 /
-L/-D,无需动任何配置。