对称型 NAT 下的异地组网:Tailscale 与私有 DERP 中继实战Tailscale Mesh Networking Behind Symmetric NAT with a Private DERP Relay
在外网用手机连回家里或工作站的私有服务时,Tailscale 往往是最省心的选择。但一旦两端同时撞上对称型 NAT(Symmetric NAT)、运营商未下发 IPv6、且本机还常驻着代理软件,体验就会断崖式下跌:打洞彻底失败,流量被迫绕道海外公共中继(DERP),往返延迟飙升到 350ms ~ 2700ms。
这篇文章记录我是如何把这套异地组网链路重新拉回 98ms ~ 120ms 稳定可用状态的。核心只做两件事:在本地把 Tailscale 流量从代理软件里彻底剥离出来,以及在国内云服务器上用不到 5MB 内存跑一个带客户端鉴权与证书指纹绑定的私有 DERP 节点。
为什么打洞会失败?
在排查初期跑一次 tailscale netcheck,可以看到两个关键信号:
MappingVariesByDestIP: true—— 这意味着当前网络处于对称型 NAT 之下,每次向不同外部目标发起连接,NAT 网关都会分配不同的外部端口,常规 STUN 打洞几乎无法穿透。- 当 Windows 工作站与手机端同处于此类环境且没有公网 IPv6 时,流量 100% 会退化走 DERP 中继;而官方内置的东京、新加坡等节点在晚高峰期丢包与抖动非常明显。
要让中继快且稳,最直接的办法是在国内就近的云服务器上自建一个私有 derper。但在动手之前,必须先解决本地代理软件抢占流量的问题。
第一步:本地代理分流绕过与防火墙放行
如果电脑上常驻 Clash Verge,默认情况下 tailscaled.exe 发出的 STUN 探测包和 WireGuard 流量很容易被代理接管,导致连内网握手都绕一圈代理节点。
在 Clash Verge 的**全局扩展脚本(Script)**中,我把 Tailscale 的进程、直连端口(UDP 41641)和专属 CGNAT 网段(100.64.0.0/10)全部前置到了 DIRECT:
rules.push(
"PROCESS-NAME,tailscaled.exe,DIRECT",
"PROCESS-NAME,tailscale.exe,DIRECT",
"PROCESS-NAME,tailscale-ipn.exe,DIRECT",
"DST-PORT,41641,DIRECT",
"IP-CIDR,100.64.0.0/10,DIRECT,no-resolve",
"IP-CIDR6,fd7a:115c:a1e0::/48,DIRECT,no-resolve",
"DOMAIN-SUFFIX,tailscale.com,DIRECT",
"DOMAIN-SUFFIX,tailscale.io,DIRECT",
"DOMAIN-SUFFIX,ts.net,DIRECT"
);
如果开启了 TUN 虚拟网卡模式,光靠规则层的 DIRECT 还不够,还需要在「虚拟网卡设置」的路由排除网段(route-exclude-address)中显式填入 100.64.0.0/10 与 fd7a:115c:a1e0::/48,避免两块虚拟网卡在路由表层打架。
接着在管理员 PowerShell 中放行入站规则,允许虚拟局域网内的其他设备访问本机服务:
# 放行 WireGuard 默认直连端口
New-NetFirewallRule -DisplayName "Tailscale Direct UDP 41641" -Direction Inbound -Protocol UDP -LocalPort 41641 -Action Allow
# 允许 Tailscale 虚拟网段访问本机端口
New-NetFirewallRule -DisplayName "Allow Tailscale Inbound All" -Direction Inbound -RemoteAddress 100.64.0.0/10 -Action Allow
第二步:无域名自建私有 DERP 节点
以往部署私有 DERP 最麻烦的是申请域名和维护 Let’s Encrypt 证书。实际上,新版 Tailscale 已经原生支持 自签名 IP 证书 + SHA256 指纹绑定(CertName),完全不需要域名。
为了在 2GB 内存的轻量云服务器上保持干净,我选择直接在本地 Windows 用 Go 交叉编译出 Linux 静态二进制再上传:
$env:CGO_ENABLED="0"; $env:GOOS="linux"; $env:GOARCH="amd64"
go install tailscale.com/cmd/derper@latest
scp "$HOME\go\bin\linux_amd64\derper" server:/usr/local/bin/derper
在云服务器上生成一张包含公网 IP subjectAltName 的十年期自签名证书,并用 Systemd 托管 derper:
SERVER_IP="203.0.113.10" # 替换为云服务器公网 IP
mkdir -p /etc/derper/certs
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
-keyout /etc/derper/certs/${SERVER_IP}.key \
-out /etc/derper/certs/${SERVER_IP}.crt \
-subj /CN=${SERVER_IP} \
-addext subjectAltName=IP:${SERVER_IP}
对应的 /etc/systemd/system/derper.service 配置有三个安全要点:
- 高位非标端口:用
TCP 53445跑 DERP、UDP 53478跑 STUN,并设-http-port=-1彻底关闭 HTTP 监听。 - 强制客户端鉴权:开启
-verify-clients=true(云服务器自身也需登录同一个 Tailscale 账号),通过本机tailscaled.sock校验设备身份,防止节点被外人扫描白嫖带宽。
[Unit]
Description=Tailscale Private DERP Server
After=network.target tailscaled.service
Requires=tailscaled.service
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/derper -hostname=203.0.113.10 -a=:53445 -http-port=-1 -stun -stun-port=53478 -certmode=manual -certdir=/etc/derper/certs -verify-clients=true
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
第三步:在 ACL 中绑定 SHA256 证书指纹
启动 derper 后,从 systemctl status derper 日志里可以拿到一行形如 sha256-raw:... 的证书指纹。把它填进 Tailscale 控制台的 Access Controls (ACL) 即可生效:
"derpMap": {
"OmitDefaultRegions": false,
"Regions": {
"901": {
"RegionID": 901,
"RegionCode": "shanghai-private",
"RegionName": "Private Relay",
"Nodes": [
{
"Name": "901a",
"RegionID": 901,
"HostName": "203.0.113.10",
"IPv4": "203.0.113.10",
"DERPPort": 53445,
"STUNPort": 53478,
"CertName": "sha256-raw:85f8c827a8f9279025350fd7f4996e49923c96035f69e0400786fe0e999f39cb"
}
]
}
}
}
保存后在终端再次执行 tailscale netcheck 和 tailscale ping,可以看到默认中继已经切到了 Private Relay,跨网往返延迟稳定在 98ms 左右,且服务端 derper 常驻内存不到 5MB。
附:将仅监听回环地址的本地服务暴露给 Tailscale
如果本机某些 Web 服务出于安全考虑只绑定了 127.0.0.1:8000 而不想改配置监听全网卡(0.0.0.0),可以用 Windows 自带的 netsh portproxy 只把 Tailscale 虚拟网卡 IP 的对应端口转发到本地回环:
netsh interface portproxy add v4tov4 listenaddress=100.90.x.x listenport=8000 connectaddress=127.0.0.1 connectport=8000
这样公网和局域网依然无法触达该端口,只有通过 Tailscale 鉴权的自己的设备才能访问。
When connecting from a mobile network back to private services running on a home workstation, Tailscale is usually the most effortless option. However, once both endpoints find themselves behind Symmetric NAT without IPv6—and your workstation is simultaneously running a local proxy client—the experience falls off a cliff. Direct UDP hole-punching fails completely, forcing traffic through overseas public DERP relays with round-trip latency spiking to 350ms ~ 2700ms.
This post documents how I brought cross-network latency back down to a rock-solid 98ms ~ 120ms. It comes down to two things: cleanly isolating Tailscale traffic from the local proxy client, and running a domain-less private DERP relay with client verification and SHA256 certificate pinning in under 5MB of RAM.
Why Does Direct Hole-Punching Fail?
Running tailscale netcheck during initial diagnosis revealed two critical signals:
MappingVariesByDestIP: true— This indicates Symmetric NAT. Because the gateway assigns a different external port for every destination IP, standard STUN hole-punching is practically impossible.- When both the Windows workstation and mobile device are behind Symmetric NAT without public IPv6, 100% of traffic falls back to DERP relays. Official public relays (Tokyo, Singapore, etc.) suffer heavy jitter and packet loss during peak evening hours.
The most reliable fix is hosting a private derper instance on a nearby domestic cloud VM. Before doing that, however, we have to stop the local proxy from hijacking Tailscale packets.
Step 1: Proxy Bypass Rules and Firewall Policies
When Clash Verge is running on Windows, STUN probes and WireGuard packets emitted by tailscaled.exe are easily intercepted by the proxy core, causing even local mesh handshakes to detour through external proxy nodes.
In Clash Verge’s global Extend Script, I prepended DIRECT rules for Tailscale’s executables, its direct UDP port (41641), and its CGNAT subnet (100.64.0.0/10):
rules.push(
"PROCESS-NAME,tailscaled.exe,DIRECT",
"PROCESS-NAME,tailscale.exe,DIRECT",
"PROCESS-NAME,tailscale-ipn.exe,DIRECT",
"DST-PORT,41641,DIRECT",
"IP-CIDR,100.64.0.0/10,DIRECT,no-resolve",
"IP-CIDR6,fd7a:115c:a1e0::/48,DIRECT,no-resolve",
"DOMAIN-SUFFIX,tailscale.com,DIRECT",
"DOMAIN-SUFFIX,tailscale.io,DIRECT",
"DOMAIN-SUFFIX,ts.net,DIRECT"
);
If TUN mode is enabled, rule-level DIRECT alone isn’t enough—you must also add 100.64.0.0/10 and fd7a:115c:a1e0::/48 to route-exclude-address in the TUN settings so the two virtual NICs don’t fight over routing tables.
Next, open inbound rules in an elevated PowerShell prompt so other authenticated devices on the tailnet can reach local services:
# Allow WireGuard direct UDP port
New-NetFirewallRule -DisplayName "Tailscale Direct UDP 41641" -Direction Inbound -Protocol UDP -LocalPort 41641 -Action Allow
# Allow inbound connections from Tailscale CGNAT subnet
New-NetFirewallRule -DisplayName "Allow Tailscale Inbound All" -Direction Inbound -RemoteAddress 100.64.0.0/10 -Action Allow
Step 2: Deploying a Domain-less Private DERP Relay
Traditionally, the most annoying part of running a private DERP server was registering a domain and maintaining Let’s Encrypt certificates. Modern Tailscale versions natively support self-signed IP certificates pinned via SHA256 fingerprint (CertName)—no domain name required.
To keep my 2GB cloud VM lean without installing a Go toolchain on it, I cross-compiled a static Linux binary locally on Windows and uploaded it via scp:
$env:CGO_ENABLED="0"; $env:GOOS="linux"; $env:GOARCH="amd64"
go install tailscale.com/cmd/derper@latest
scp "$HOME\go\bin\linux_amd64\derper" server:/usr/local/bin/derper
On the cloud VM, generate a 10-year self-signed certificate containing the public IP in subjectAltName:
SERVER_IP="203.0.113.10" # Replace with your VM public IP
mkdir -p /etc/derper/certs
openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
-keyout /etc/derper/certs/${SERVER_IP}.key \
-out /etc/derper/certs/${SERVER_IP}.crt \
-subj /CN=${SERVER_IP} \
-addext subjectAltName=IP:${SERVER_IP}
The /etc/systemd/system/derper.service unit enforces two key security practices:
- Non-standard high ports: Run DERP over
TCP 53445and STUN overUDP 53478, with-http-port=-1to disable HTTP entirely. - Client identity verification: Enable
-verify-clients=true(with the cloud VM itself logged into the same tailnet) soderperqueries the localtailscaled.sockand rejects unauthorized scanners.
[Unit]
Description=Tailscale Private DERP Server
After=network.target tailscaled.service
Requires=tailscaled.service
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/derper -hostname=203.0.113.10 -a=:53445 -http-port=-1 -stun -stun-port=53478 -certmode=manual -certdir=/etc/derper/certs -verify-clients=true
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Step 3: Pinning the SHA256 Certificate Fingerprint in ACLs
Once derper starts, grab the sha256-raw:... fingerprint printed in systemctl status derper and add it to Access Controls (ACL) in the Tailscale admin console:
"derpMap": {
"OmitDefaultRegions": false,
"Regions": {
"901": {
"RegionID": 901,
"RegionCode": "shanghai-private",
"RegionName": "Private Relay",
"Nodes": [
{
"Name": "901a",
"RegionID": 901,
"HostName": "203.0.113.10",
"IPv4": "203.0.113.10",
"DERPPort": 53445,
"STUNPort": 53478,
"CertName": "sha256-raw:85f8c827a8f9279025350fd7f4996e49923c96035f69e0400786fe0e999f39cb"
}
]
}
}
}
After saving, running tailscale netcheck and tailscale ping confirms that the preferred relay has switched to Private Relay. End-to-end relay latency now stays around 98ms, while derper consumes less than 5MB of resident memory.
Bonus: Exposing Loopback-Only Local Services to Tailscale
If a local web service binds strictly to 127.0.0.1:8000 for security and you don’t want to expose it on 0.0.0.0, you can use Windows’ built-in netsh portproxy to forward traffic arriving on your Tailscale IP directly to loopback:
netsh interface portproxy add v4tov4 listenaddress=100.90.x.x listenport=8000 connectaddress=127.0.0.1 connectport=8000
This keeps the port invisible to the local LAN and public internet while making it seamlessly accessible to authenticated devices on your tailnet.