Android 的单 VPN 规则并不阻断托管 SSH 服务器
单 VPN 限制约束的是 VPN 服务,而不是套接字。手机上的 SSH 服务器失败,原因在监听者、对端可达性回程路径。
这种信念流传甚广,几乎被当作事实:Android 只允许一个 VPN,所以运行在手机上的 SSH 服务器无法通过那条隧道工作。前半句没错,结论却不成立。平台限制约束的是多少个应用能占用 VPN 服务槽位,而不是能存在多少个套接字,也不是谁可以连到手机。带有监听套接字的 SSH 守护进程只是一个使用普通端口的普通应用,而隧道只是它的流量可以走的路径之一。
单 VPN 规则真正约束的是什么
VpnService 是单槽位资源。当一个应用建立隧道后,它就成为了设备的 VPN,第二个想建立隧道的应用会被拒绝,直到第一个消失。这是一条关于隧道归属的规则,它解释了为什么两个 VPN 客户端不能同时运行。
它对监听套接字只字未提。SSH 服务器不调用 VpnService,不拦截数据包,也不争抢那个槽位。它绑定一个端口并接受连接,这与完全没有安装 VPN 时它所做的完全相同。所以这个说法把对隧道提供者的限制和对网络连接的限制混为一谈,而两者毫无关系。
监听者是目的端,而不是隧道的一部分
隧道一旦存在,手机在上面就有地址,对端设备也有通往该地址的路由。是否有东西应答由监听者决定。绑定到 loopback 的守护进程只能从手机本身访问;绑定到旧隧道地址的守护进程在重连之后可能仍然保留原来的绑定,而它绑定的地址其实已经改变。
绑定规则就是普通的 SSH 配置:监听地址和端口,以及谁可以通过认证。在 Android 上这些设置位于应用自身的配置里,而应用的进程所有者可能不被允许使用特权端口,所以桌面上以 root 身份绑定 22 端口的习惯并不能原样搬过来。
ListenAddress 10.90.0.4
Port 2222
AllowUsers alice接下来是路径本身的许可。一个乐于承载出站网页流量的消费级 VPN 完全不必允许入站连接到它的客户;很多服务确实不允许,因为那会让一个客户暴露在另一个客户面前。如果对端从任何地方都到不了手机的隧道地址,问题出在服务的策略上,而不在手机。
回程路径才是通常出问题的地方
假设对端到达了手机的隧道地址,SYN 也到了。现在响应必须沿同一条路径出去,而这是手机内部的路由决策。如果响应因为 Wi-Fi 是默认网络而走了 Wi-Fi,对端看到的就是一个永远无法完成的连接,这看起来和防火墙一模一样。
Android 让这件事比桌面更复杂,因为默认网络是按进程选择的,而且可以被 VPN 应用策略改变。VPN 客户端只能为一个允许应用列表做路由,所以列表之外的服务器应用在回复时仍然使用物理网络,而对端却在隧道里等这些回复。
应用还可以把自己的套接字绑定到指定网络,这会覆盖默认值。当你读到某服务能从某个地址访问、从另一个地址访问不到的报告时,通常就是这个机制在起作用。
绕过 VPN 并不能修复入站服务
流行的做法,把 SSH 应用排除在 VPN 之外,让它不再被重定向,其实是对相反问题的修正。当出站 SSH 客户端的传输因隧道而反复中断时,它有用。对托管服务器而言,它可能把回复移出对端正使用的隧道,从而把部分故障变成完全故障。
在路由器上开放公共端口也是同理。如果要求是一台已授权设备通过 VPN 到达手机,那么把 SSH 暴露到互联网是另一种威胁模型,而不是修复。
先确定服务的地址:手机的 LAN 地址、隧道地址,还是刻意转发的端点。每一种都有自己的可达性与暴露约定,把它们混在一起,正是让小小的访问问题变成永久漏洞的方式。
一次只诊断一个方向
在改动配置之前先记下四个事实:对端连接的地址、手机上的监听状态、拥有它的应用与用户上下文,以及每个方向上观察到的路径。一份只说 VPN 会断开 SSH 的报告,这四个事实一个都没有,也无法区分客户端传输问题和服务器不可达。
adb shell ip -br addr
adb shell ip route get 10.90.0.9
nc -vz 10.90.0.4 2222从对端而不是手机执行端口检查,并阅读 SSH 客户端的详细输出,看它是在 TCP 握手之前还是之后失败。从未连上的套接字和被拒绝的登录属于不同阶段,责任方也不同。
调试时保持主机密钥校验和账户限制不变。正是限制监听者和允许的用户,才让一台可达的服务器变得可接受;为了连通而放松这些,是用一个问题换一个更糟的问题。OpenSSH 服务器参考涵盖了监听与访问控制,如果你的 Android 实现遵循同样的配置名。