前情提要

我的需求是从外面方便地访问家里的另一台电脑(能 ssh 上就行),并且我不喜欢多装太多服务在工作电脑上。
家里的电脑是一台 kali,是我们预备连接的 server。外面这台我经常带着跑的是 windows 系统,是 client。为了简单起见,我后面直接叫它们 kali 和 windows。

嘉宾介绍

首先是一个符合国情的需求:两台电脑都要有梯子。我在两边都装了 Clash Verge Rev,并且开了 TUN 模式,按照规则转发。梯子常年开着,这意味着两边的虚拟网卡都被 Clash 接管。

kali 的环境

租房处提供的路由器,普通的 DHCP。经过测试,kali 在这里没有独立的 IPv4 地址,是通过 NAT 出去的,一家人穿一条裤子出门。但是!好消息是它有独立的 IPv6 地址。这意味着我有机会不经过 NAT 转换,直接点对点连上这台电脑。

windows 的环境

这个就比较复杂了。分类讨论下来场景有三类:

  1. 在家的时候,和 kali 在一个局域网下,同一局域网下很容易 ssh 上。
  2. 在学校的时候,连接的是学校内网。
  3. 为了不在学校而又能模拟学校内网连接的情况,我在家进行测试的时候会让 windows 连手机流量热点(中国联通),并打开 EasyConnect VPN(SangFor)。

排查 - IPv6

问题描述

  • 在学校工位,通过 IPv6 地址,连不上 kali。
  • 在家使用手机流量热点,并打开学校 VPN,同样连不上 kali。
    以上描述的都是 ssh 的行为(实际上 ping 也一致)。

Test-Connection 测试

下面开始测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
PS C:\windows\system32> # 纯联通连接
PS C:\windows\system32> Test-NetConnection -ComputerName "2409:8a1e:7a56:e440:dbb:887:564d:7218" -Port 443
警告: TCP connect to (2409:8a1e:7a56:e440:dbb:887:564d:7218 : 443) failed
警告: Ping to 2409:8a1e:7a56:e440:dbb:887:564d:7218 failed with status: TimedOut
ComputerName : 2409:8a1e:7a56:e440:dbb:887:564d:7218
RemoteAddress : 2409:8a1e:7a56:e440:dbb:887:564d:7218
RemotePort : 443
InterfaceAlias : WLAN
SourceAddress : 2408:840d:7930:1055:8c2b:4624:8acc:f56d
PingSucceeded : False
PingReplyDetails (RTT) : 0 ms
TcpTestSucceeded : False

PS C:\windows\system32> # 联通+Clash
PS C:\windows\system32> Test-NetConnection -ComputerName "2409:8a1e:7a56:e440:dbb:887:564d:7218" -Port 443
ComputerName : 2409:8a1e:7a56:e440:dbb:887:564d:7218
RemoteAddress : 2409:8a1e:7a56:e440:dbb:887:564d:7218
RemotePort : 443
InterfaceAlias : Meta
SourceAddress : fdfe:dcba:9876::1
TcpTestSucceeded : True

PS C:\windows\system32> # 联通+Clash+校园网VPN
PS C:\windows\system32> Test-NetConnection -ComputerName "2409:8a1e:7a56:e440:dbb:887:564d:7218" -Port 443
ComputerName : 2409:8a1e:7a56:e440:dbb:887:564d:7218
RemoteAddress : 2409:8a1e:7a56:e440:dbb:887:564d:7218
RemotePort : 443
InterfaceAlias : Meta
SourceAddress : fdfe:dcba:9876::1
TcpTestSucceeded : True

总结下来是这样的:

网络环境 联通 联通+Clash 联通+Clash+SangFor
TestConnection ×

这并没有解决我 ssh 的问题,按道理讲如果是通的,那么打开了 Clash 我就该 ssh 上。而且,直连 IPv6 本来就应该可以连上,为什么现在居然连不上呢??
所以接下来我们一步一步排查。

联通直连 IPv6

首先要尝试回答的问题是,为什么不开 Clash 的时候连不上这个 IPv6 地址?
根据 gemini 的建议,使用 tracert 跟踪只在联通网络下连接 kali 的路径,检查是哪里断开了。在这个时候我才发现,原来 kali 所在的网络(家庭路由器)是移动的,而我的手机流量是联通的。
可能的情况有四种:

  1. 第 1-2 跳就断开了,说明是本地路由器的拦截或错误配置(发送端)
  2. 第 3-5 跳断开,说明联通内部路由表丢弃了发往移动 IP 段的包,是运营商路由故障(联通内部)
  3. IP 地址前缀从 2408(联通) 变为 2409(移动)的时候断开了,说明跨运营商互联有问题(联通->移动)
  4. 追踪到了目标附近,最后一跳断开,说明目标服务器屏蔽了访问(接收端)。
    我的试验结果是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PS C:\windows\system32> tracert -6 2409:8a1e:7a56:e440:dbb:887:564d:7218

通过最多 30 个跃点跟踪到 2409:8a1e:7a56:e440:dbb:887:564d:7218 的路由

1 8 ms 9 ms 7 ms 2408:840d:7500:3dfa::bb
2 * * * 请求超时。
3 49 ms 73 ms * fc00:1000::61
4 * * * 请求超时。
5 42 ms * * 2408:8000:9000:20e6::52
6 * 23 ms 32 ms 2408:8000:9000:20e6::b4
7 * * * 请求超时。
8 * * 89 ms 2409:8080:0:3:2e2:281::
9 * * * 请求超时。
10 29 ms 28 ms 30 ms 2409:8080:0:2:206:275:0:1
11 41 ms 23 ms 20 ms 2409:801e:f0:1::3c5
12 23 ms 21 ms 31 ms 2409:8a1e:7a05:6de2:56b2:9dff:fe15:b6b8
13 * * * 请求超时。
14 * * * 请求超时。
15 * * * 请求超时。
16 * * * 请求超时。
17 * * * 请求超时。
18 * * * 请求超时。

可见是最后一跳出问题,也即服务端拦截

那么,究竟是移动的路由器拦截了,还是 kali 自己的防火墙拦截了呢?
再次进行测试:在 kali 上使用 tcpdump 监听 443 端口的包。此时,有两种情况:

  1. 如果观察到收到了包但是拒绝了,那就是 kali 的防火墙拦截
  2. 如果屏幕一片死寂,那就是路由器拦截
    实际情况是二。

联通+Clash 连 IPv6

观察到之前使用 Clash 连接的时候是成功的,那么我想要验证它成功的方式。
还是沿用刚才的检测法,在 Clash 进行 Test-NetConnection 的时候,在 kali 这边用 tcpdump 看有没有包。

好耶,成功了!kali 呢?
——没有包!

此刻才基本确定,并非 Clash 有神力连上了我的 kali,而是它报!喜!不!报!忧!我们可以连接一个完全不存在(没开放)的端口进行测试,结果如下:

孩子呢,你一天到晚不好好学习,就知道哄你个大大哄你个妈妈。

这完全解释了为啥 ssh 连不上,因为事实上就是没连上。Clash 开启了 TUN 模式之后,在 Windows 发出 TCP SYN 包之后,Clash 直接在本地给 Windows 回了一个 SYN-ACK,故 Windows 视角下,误认为连接已经成功了。但实际上,此时真实的流量可能正在代理服务器上重试,或者因为规则不对直接被丢弃了,压根没出路由器。

排查 - wireguard

ok,那现在咋办呢?
我们尝试使用 wireguard,它会尝试创建一个 VPN。原理是这样的:kali和windows身份发生了翻转,kali开始向windows发出请求,由于是kali先发出请求的,联通路由器认为可以放行。接下来,windows 顺着这个连接的回包就将被认为是可信的。
只要这个隧道维持着(通过 PersistentKeepalive 定期发个小包),这扇门就为 windows 开着。
wireguard 使用 UDP 包装流量;如果采用 TCP,可能导致“TCP 重传风暴”,也就是丢包的时候内部 TCP 疯狂重传,外部 TCP 也疯狂重传,两层协议的拥塞控制算法叠加起来,网络带宽不堪重负。

再次测试连通性。windows 端也安装了一个 wireguard,首先确保其连接在内网基础上可通,证明 wg 并没有配错。然后撤掉内网,改成联通网络上测试。


windows 一直在尝试发握手请求。中间,wireguard 甚至显示连接已经建立了。但事实上,kali 依旧不为所动,一个包也没有。
这一通操作下来,发现 VPN 也并不能绕过路由器防火墙。wireguard 连 kali 的想法算是泡汤了,不在 windows 上多装一个软件的想法也泡汤了(不然可以直接用 clash verge rev 对 wireguard 的协议连)。

至此打道回府,老老实实装一个 tailscale 了事了。

tailscale 配置

tailscale 实际上也是基于 wireguard 实现的,但是它的打洞能力非常强,可以在 kali 发了出站包之后马上让 windows 的撞进来,从而建立连接。就算 P2P 打洞不成功,它能强制流量从最近的中转服务器那里绕一圈,就算是以延迟高一点为代价,也终归能连上。

注意在配置之前关掉 wireguard,关掉 TUN(否则网卡会打架),然后双方验证一下,就能连上了。我实际测了一下,叠一个学校 VPN 也没问题。

补配一下 Clash verge rev 规则。windows 这边我用的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function main(config) {
// 1. 确保 DNS 开启并设置 Fake-IP 过滤
// 防止 Clash 尝试给 Tailscale 分配虚假 IP 导致打洞失败
const tailscaleNet = "100.64.0.0/10";
if (!config.dns) config.dns = { enable: true };
config.dns["fake-ip-filter"] = [
...(config.dns["fake-ip-filter"] || []),
tailscaleNet,
"*.tailscale.net",
"kali", // 你的 MagicDNS 主机名
];

// 2. 强制 TUN 模式跳过 Tailscale 网段
// 这是物理层面的避让,防止流量进入 Clash 虚拟网卡
if (config.tun) {
config.tun["skip-proxy"] = [
...(config.tun["skip-proxy"] || []),
tailscaleNet,
];
}

// 3. 在 Rules 顶部插入直连规则
// 这是逻辑层面的避让,万一 TUN 没拦截住,规则层也会强制直连
const tailscaleRule = `IP-CIDR,${tailscaleNet},DIRECT,no-resolve`;
if (!config.rules) config.rules = [];
config.rules.unshift(tailscaleRule);

return config;
}

kali 这边是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function main(config) {
const tailscaleNet = "100.64.0.0/10";
const tailscaleDNS = "*.tailscale.net";

// 1. 注入 DNS 避让:防止 Clash 污染 Tailscale 的私有域名解析
if (!config.dns) config.dns = {};
config.dns["fake-ip-filter"] = [
...(config.dns["fake-ip-filter"] || []),
tailscaleNet,
tailscaleDNS,
"kali", // 你的 MagicDNS 主机名
"windows", // 如果你给 Windows 也起了名字
];

// 2. 注入 TUN 避让:这是物理层面的“互不干扰”
// 告诉 Clash 的 TUN 引擎:看到这个网段,直接绕过去,不要吸进来
if (!config.tun) config.tun = {};
config.tun["skip-proxy"] = [
...(config.tun["skip-proxy"] || []),
tailscaleNet,
];

// 3. 注入 路由逻辑:在规则链最顶端插入 DIRECT
// 确保万一流量漏到了规则层,也会被强制直连
const tailscaleRule = `IP-CIDR,${tailscaleNet},DIRECT,no-resolve`;
if (!config.rules) config.rules = [];

// 使用 unshift 确保该规则排在所有代理规则(如 MATCH)的前面
config.rules.unshift(tailscaleRule);

return config;
}

后话

我的排查就这么虎头蛇尾地结束了,实际踩的坑要多得多得多,比如冤枉学校 VPN 没有 IPv6 或者封禁端口或者封禁 UDP(可能确实封禁了吧,但是还没到这一关就死了啊),然后费劲换端口啊啥的。
事实证明不能盲信 LLM,还得自己多动脑啊。


附录:wireguard 配置

事实上,与全文的逻辑顺序不同,真实的时间线是我最开始先配了 wireguard,再开始测试 IPv6 连接。
在 kali 侧,只需设置流量转发时从 10.66.66.1 出去的流量直接走 DIRECT 即可(相当于它们不经过 clash 转发,而是直接发给 wg 服务)。
作为和 kali 的 wireguard 的对应,在 windows 上面进行配置的时候除了多装一个 wg 之外,另一个可选项是直接配置 clash verge rev 的规则,因为它内置了对于 wireguard 协议的支持。那么 windows 作为 client,虚拟节点可以配成 10.66.66.2。
这边也贴一下 windows 侧 wireguard 配置:

clash verge rev 有个很坑的一点,就是在左边的 Merge 里面写 rule 的话,只能完全覆盖,不能追加。问大模型所得到的答案是完全牛头不对马嘴的,这个信息是在 clash verge rev 的更新日志中提供的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function main(config) {
  const wg_node = {
    name: "kali-direct", // 确保名字唯一且被引用
    type: "wireguard",
    server: "2409:8a1e:7a56:e440:dbb:887:564d:7218",
    port: 51820,
    ip: "10.66.66.2",
    "private-key": "<redacted>",
    "public-key": "z9DytF559yk2eEuYKXogolr/I7qD2wSwKAbEF0T4iEA=",
    udp: true,
    mtu: 1280,
    "allowed-ips": ["0.0.0.0/0"]
  };

  // 1. 注入节点
  if (!config.proxies) config.proxies = [];
  // 先过滤掉可能存在的同名节点,防止重复报错
  config.proxies = config.proxies.filter(p => p.name !== "kali-direct");
  config.proxies.unshift(wg_node);

  // 2. 注入规则 (必须放在 rules 最前面)
  if (!config.rules) config.rules = [];
  config.rules.unshift("IP-CIDR,10.66.66.0/24,kali-direct,no-resolve");


  // 3. 注入策略组 (确保你在 UI 上能选到它)
  if (config["proxy-groups"]) {
    config["proxy-groups"].forEach(group => {
      if (group.proxies && !group.proxies.includes("kali-direct")) {
        group.proxies.unshift("kali-direct");
      }
    });
  }
  return config;
}