把学校机房的 5090 接回直连
陈淮锐 · 2026 年 9 月 18 日整理 · Tailscale / IPv6 / NAT6
机房里有一台带 5090 的机器,是我跑实验用的。人在宿舍,机器在机房,中间隔着校园网、两层 NAT, 以及一个我完全够不着的上游。
一开始想得很简单:装好 Tailscale,回宿舍用 SSH 连上,就能干活。 后面分别遇到了桌面会话不一致、NAT 映射受限和 IPv6 前缀未下发的问题。按当时的记录,最后恢复了直连;下面保留影响判断的现象与配置变化。
阶段 1 - 远程桌面进入了不同会话
服务器上跑着向日葵,我平时用它看 GNOME 桌面,敲代码走 SSH。有一阵子想省事, 直接开 Windows 自带的远程桌面(mstsc / RDP)连过去,然后就出问题了。
症状很迷惑:RDP 登进去看到的桌面,和向日葵里看到的桌面根本不是同一个。 两边各开各的窗口,互相看不见对方。在 RDP 里开的程序,切回向日葵完全找不到;反过来也一样。 严重的时候 RDP 下 GNOME 干脆直接黑屏,只剩个鼠标能动。
当时第一反应是「xrdp 有 bug」,换了好几个配置项都没用。后来才想明白, 在这台机器的配置下,两种入口使用了不同的 Linux 桌面会话:
- 向日葵连的是物理控制台那个会话——就像人坐在显示器前一样, 能看到本地显示器正在展示的桌面。
- RDP(xrdp)登录时会新开一个会话。在 GNOME 下它并不会附到物理控制台上, 而是另外起了一个「第二个图形界面」。
在当时的 xrdp 配置下,两种方式进入的是不同桌面会话。黑屏可能与会话或显示后端有关,单凭黑屏无法确定具体原因,也不能据此认定两种远程桌面方式一定不能并存。
(当时还以为是显卡驱动的问题,差点去重装 NVIDIA 驱动,还好没动手。)
放弃 RDP,把 xrdp 停掉。桌面这一层只留向日葵(看物理控制台), 其余所有事情一律走 SSH。跑训练、改配置和看日志都可以从这个入口完成。
后面查路由表、抓 DHCPv6 包、调路由器 API,全是在 SSH 里做的。 回头看,桌面这一层早点定下来确实省事,否则网络还没查明白,又要先处理黑屏。
阶段 2 - Tailscale 一直经过中继
桌面总算理清了,接着看连接。那阵子跑 tailscale ping,反复看到的都是这副样子:
pong from a12354768 (100.x.x.x) via DERP(sfo) in 359ms
pong from a12354768 (100.x.x.x) via DERP(sfo) in 350ms
pong from a12354768 (100.x.x.x) via DERP(sfo) in 351ms
direct connection not established
每次回包都从中继绕一圈,最后一行才是重点:直连压根没建起来。
三百多毫秒敲 SSH,打字都能感觉到等待。Tailscale 会尝试建立 UDP 直连,成功后走 P2P, 没建立起来时可以靠 DERP 中继维持连接。现在一直停在中继,得往下查直连卡在哪儿。
失败时
- 宿舍 Windows
- DERP(sfo) 中继
- 机房 5090
现在
- 宿舍 Windows
- 5090(直连)
netcheck 显示映射随目标变化
tailscale netcheck 会打一组 STUN 探测,报告本机对外的网络特征。5090 这边当时是这样的:
* UDP: true
* IPv4: yes, 183.63.97.253:27197
* IPv6: no, but OS has support
* MappingVariesByDestIP: true
* PortMapping:
* Nearest DERP: San Francisco
MappingVariesByDestIP: true 是关键。它表示出口 NAT 分配给同一台内网设备的公网端口,
会随目标地址的不同而变化。换句话说,对面看到的端口号不固定——这正是 UDP 打洞最难对付的一类 NAT。
这种变化会增加端点预测和打洞的难度,是否能建立直连还要结合两端网络与实际探测结果判断。
再看 5090 自己的地址:192.168.31.151,而出口路由器的 WAN 侧是 10.215.x.x,
一段私有地址。这确认了一个需要继续排查的条件:5090 前面不止一层 NAT。
示意图:5090 前面有两层 NAT。UPnP 只能作用于我够得着的那一层。
(10.x 的 WAN 地址不是猜的,是从路由器自己的 NAT-PMP 回包里漏出来的。)
先试了最简单的一招:开 UPnP
出口路由器是台小米万兆路由器(RC01)。我远程登进它的后台,把 UPnP 打开了。 Tailscale 立刻就申请到了映射,路由器后台能直接看到:
协议 内部端口 外部端口 客户端
UDP 41641 41642 192.168.31.151
5090 的 netcheck 也确认拿到了 PortMapping: UPnP, NAT-PMP, PCP。
然后重新打洞——还是 DERP。
UPnP 只在小米路由器这一层开了个洞。而它自己也在 10.215.x.x 后面,
它向上游申请端口映射时,拿到的仍然是一个内网地址。
也就是说,这个洞只打到了第二层 NAT 的内侧,外面那层压根看不到。 仅修改这台路由器,并没有解决上游地址转换的限制。
阶段 3 - 通过管理 API 配置出口路由器
要往下走就得改路由器配置,而人不在现场,所以只能从 5090 上远程操作它。
这台路由器是我自己网络的出口设备,管理密码本来就有。 麻烦在网页后台:远程、跨代理打开,经常加载不全。于是改成用脚本调用它的 API, 还是用原有管理凭据登录,只是省掉了反复等页面的过程。
先看看网页是怎么登录的
小米路由器的 LuCI 后台有一套自己的登录校验。抓页面 JS 能看出来:密码不是明文传的, 而是客户端先生成一个 nonce,再拿它跟密码一起做两轮 SHA-256。
import hashlib, json, random, time, urllib.request, urllib.parse
BASE = "http://192.168.31.1"
KEY = "<从页面 JS 里抠出来的 32 位 hex>"
DEVICE_ID = "<本机网卡 MAC>"
def sha256(s):
return hashlib.sha256(s.encode()).hexdigest()
def login(pwd):
# nonce 由客户端自生成: 0_<MAC>_<unix时间>_<四位随机>
nonce = "0_%s_%d_%d" % (DEVICE_ID, int(time.time()), random.randint(0, 9999))
# 两轮 SHA-256: 先 hash(pwd + KEY), 再连上 nonce 一起 hash
password = sha256(nonce + sha256(pwd + KEY))
data = urllib.parse.urlencode({
"username": "admin",
"password": password,
"logtype": 2,
"nonce": nonce,
}).encode()
r = urllib.request.urlopen(urllib.request.Request(
BASE + "/cgi-bin/luci/api/xqsystem/login",
data=data, method="POST"))
return json.loads(r.read())["token"]
几个当时踩过的点:
-
nonce 是客户端自己生成的,不是服务器下发的。格式
0_<clientMAC>_<unix时间>_<四位随机>, 其中 clientMAC 由路由器注入登录页——从 5090 访问时就是 5090 网卡的 MAC。 - KEY 是一个完整的 32 位 hex。页面里那个字段很容易只看到一半, 抄漏了后面几位就会一直登录失败。
- 避免重复使用 nonce。当时连续请求遇到了
Invalid nonce,调试时需要检查生成方式和请求时序。这不是密码绕过方法,调用仍需要已有的管理凭据。
token 应放在请求路径中
登录成功拿到 token 之后,我照着直觉拼 ?token=<token> 去调别的接口,
结果全是 {"code":401,"msg":"Invalid token"}。
折腾了一阵才发现,真正的页面用的是路径参数而不是查询参数:
/cgi-bin/luci/;stok=<token>/api/xqnetwork/xxx
那个 ;stok= 是嵌在路径里的分号语法,不是 ?。知道这一点之后,
剩下的接口就都能通了——包括后面要用的 IPv6 配置接口。
阶段 4 - 换 IPv6 试试
回到正题。既然 IPv4 那层的 CGNAT 我够不着,那就换条路——让 Tailscale 走 IPv6。 能不能通,取决于出口路由器能不能从上游拿到 IPv6。而这台 RC01 的固件是支持 IPv6 的, 页面里给了好几种模式:
- Native —— 原生,直接从上游拿地址
- DHCPv6 —— 通过 DHCPv6 获取
- PPPoEv6 —— IPv6 也走 PPPoE 拨号
- 静态 IPv6、Passthrough(透传)、NAT6
这台机器的上行是校园网,IPv4 走 PPPoE。于是挨个试:
Native → ipv6_mode: native up: false
PPPoEv6 → ipv6_mode: pppoev6 up: false
Passthrough → ipv6_mode: pi_relay up: false
DHCPv6 → ipv6_mode: dhcpv6 up: true ip6addr: []
前三个直接起不来。DHCPv6 看起来有戏——up: true 了,但那个空的
ip6addr: [] 说明它其实没拿到任何地址,只是链路活着而已。
(顺带一提,切换模式的瞬间路由器会短暂失去响应,poll 的时候会
No route to host 几下,别慌,等它自己缓过来就行。)
最迷惑的一段:日志里明明有公网前缀
我去把路由器的系统日志下载下来翻了一遍(用它自己的日志导出接口拿压缩包,在 5090 上解开看)。 里头确实出现过公网前缀——上游是发了东西下来的:
RA Evaluate options start
PI length:64, valid:2592000, preferred:604800,
target: 2001:da8:****:****::
PI update PREFIX:2001:da8:****:****::
RA Evaluate options end
但在路由表里,这个前缀只挂在 WAN 口上,LAN 什么都没有:
2001:da8:****:****::/64 dev eth4 proto static
fd00:6969:6969::1 dev br-lan proto kernel
而 fd00::/64 属于本地地址范围,不能直接在公网路由。同一份日志还给出了前缀下发的线索:
A default route is present but there is no public
prefix on lan thus we don't announce a default route!
翻译过来就是:上游确实发了一个 /64 的前缀信息,但它只落在 WAN 口上,
没有被分配下来给 LAN 用。路由器拿不到能往下发的委托前缀(DHCPv6-PD),
所以 LAN 只能发私有前缀,下面的机器自然也拿不到公网 IPv6。
示意图:问题不在选哪个模式,而在前缀给了 WAN、没给 LAN。
(为了确认这一点,还在 5090 上抓了一次包,看到路由器发出的 RA 里确实只有 managed / other stateful 标志,没有 PIO。这支持当次通告未向 LAN 提供相应前缀的判断。)
真正跑起来的是 DHCPv6 + NAT6 这个组合:路由器自己在 WAN 侧拿到了公网 IPv6,
LAN 内部仍然用 fd00::/64,由路由器做 IPv6 的地址转换把内网流量送出去。
打开之后,WAN 上出现了一串 2001:da8:****:**** 的地址,
5090 这边的 netcheck 立刻变成 IPv6: yes,
并且探测到了自己对外呈现的 IPv6 端点。
路由器后台同时能看到此前 UPnP 配置产生的 Tailscale NAT-PMP 映射。 5090 也探测到了 IPv6 出网能力;这些状态仍需与实际连接路径分开确认:
* IPv6: yes, [2001:da8:****:****:****:****:****:****]:54215
* PortMapping: UPnP, NAT-PMP, PCP
* Nearest DERP: Hong Kong
最近 DERP 从旧金山变成香港,说明首选中继节点发生了变化,但不能单独证明实际流量已经直连。按我当时的记录,连接最终转为 direct;本文未附完整的成功输出,复现时应以 tailscale ping 或 tailscale status 显示的实际端点与连接路径为准。
(这台 IPv6 也是远程开的——人始终没走到路由器跟前,全程是从 5090 上跑脚本改的。)
代价与遗留问题
按当时的记录,最后连上了。不过配置里仍然留着几个妥协,不能看到 direct 就把它们忘了:
-
NAT6 意味着 LAN 设备没有独立的公网 IPv6。
内网还是
fd00::/64,地址转换由路由器做。这是「能出网」,不是「每台设备一个公网地址」。 - 入站访问仍然受限。LAN 没有独立的公网 IPv6,外部主动访问仍得考虑地址转换和防火墙, 不能把这次 Tailscale 直连等同于所有服务都能直接被访问。如果能拿到 DHCPv6-PD 前缀下发给 LAN,会更符合我原本想要的结构。
-
当时拿到的只有 WAN 侧地址。
记录里上游给了 WAN 侧的
/64,没拿到委托前缀。再往下是上游不支持、需要额外配置,还是有别的条件,得找网络管理员确认,不能只靠切换路由器模式猜。
若要让 LAN 获得可用的委托前缀,下一步需要向网络管理员确认这条线路是否支持 DHCPv6-PD、是否需要绑定设备或特定 VLAN。 这些上游条件无法仅靠反复切换路由器模式确定。
下次排查时先看哪些状态
- 先确认远程桌面的会话模式。本次向日葵连接物理控制台,xrdp 使用独立会话。我最终保留一种图形入口,其余操作交给 SSH。
-
MappingVariesByDestIP值得先看一眼。true表示探测到的映射随目标变化,是打洞困难的一条线索。但别只看这一行就宣布没救,还得结合两端网络和实际连接结果。 -
开 UPnP 只解决你够得着的那一层。
上游还有 CGNAT 时,本地映射没法控制那一层。
PortMapping有值不等于能直连,最终还是要看打洞结果。 - IPv4 卡住了,可以查查 IPv6。 前提是两端有可用的 IPv6 路径、相关流量也能通过。对这次排查来说,换这条路比继续只盯着 IPv4 端口映射更有用。
- IPv6 的坑经常在「WAN 有、LAN 没有」。 日志里看到公网前缀不等于通了,得确认它不是只挂在 WAN 口上、真的被发到了 LAN。
- 网页后台不好用时,可以看看已有的管理 API。 这次用脚本能记录参数、比较结果,也能在 SSH 里完成操作。管理凭据和修改前的配置要留好,能调用接口不等于随便改都能恢复。
这次排查最终收敛到几个具体对象:远程桌面进入的会话、地址经过的 NAT,以及 IPv6 前缀的分配位置。 把它们分别查清楚,才能判断下一步该改本机、路由器,还是向上游确认。
文中的三张配图为示意图,按本次排查的真实结构绘制,非截图。 所有公网前缀、MAC 地址、Tailscale 地址及路由凭据均已脱敏。 「远程登进路由器」一节针对的是本人自有并持有管理凭据的网络设备,做法是用脚本调用其 Web API。
建议不要公开自己正在使用的网络设备的配置与凭据,以防被滥用。
—— 记于机房那台 5090 又跑满了一整晚之后