你用 SSH 登录,加上 -D 1080,命令执行成功。手机里现在有了 SOCKS 监听端口,可一半应用仍然走移动数据出去。这是正常结果,不是隧道坏了:ssh -D 只创建了一个代理端点,而 Android 应用只有在被某种机制推动时才会使用代理。把链路逐段走一遍,就能看出每部分工作究竟落在哪里。

从流量真正开始的地方查起

挑一个本应走隧道的应用,检查它自己有没有代理设置。带 SOCKS 输入框的浏览器是最简单的情况:把它指向 127.0.0.1:1080,如果设置里有远程 DNS 就打开,请求就能通过。聊天客户端、地图应用或没有该输入框的游戏根本无法使用这个监听端口,你在 SSH 命令里加什么都不会改变这一点。

在牵涉其他东西之前,先用一个直接说 SOCKS 的客户端,从手机自己的 shell 证明监听端口可用:

ssh -N -D 1080 [email protected]
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me

返回的地址应当是 SSH 服务器的地址,而不是手机的。如果是,代理这一半就完成了,剩下的工作是关于覆盖范围。如果不是,先修 SSH 这一侧:密钥认证、服务器能否到达目标,以及 SSH 服务器自身的出口。

补上 Android 需要的那一环

没有代理设置的应用需要某种东西来截获它们的报文并转换成代理连接。在未 root 的设备上,那就是 VPN 服务适配器:它接管虚拟接口,把选中的流量转发给本地 SOCKS 监听端口。链路就变成:应用、适配器、SOCKS 监听端口、SSH 连接、目标。

安装适配器,授予它 VPN 权限,并选择要截获的应用或路由。状态栏里的钥匙图标此时代表的是适配器,而不是另一个商业 VPN。Android 每个用户配置文件只允许一个活动 VPN 服务连接,所以再启动第二个 VPN 应用并不会在链路上叠加一层,而是争抢同一个角色。

这一步也最容易让成功的测试误导你。如果适配器只设置为截获你已经指向代理的那个浏览器,一切都能用,别的却完全没被覆盖。在断定设备已经进隧道之前,先读一遍截获列表。

让 SSH 传输留在截获范围之外

SSH 连接本身绝不能被重新送进它所维持的 SOCKS 隧道。如果适配器截获了 SSH 应用的流量,隧道就依赖一条正在等待隧道的连接,结果是一个一旦适配器启动就卡住或失败的循环。

集成实现会保护自己的传输套接字。使用分离的应用时,你需要一个受支持的例外:把 SSH 应用排除在截获之外,或按地址排除 SSH 端点。两者都有代价。排除整个终端应用会把与它无关的网络活动也移出隧道,而地址例外在服务器地址变化、或下次连接时域名解析结果不同时就会失效。

从外部验证例外:重新连接适配器,然后确认 SSH 会话仍然存活、经代理的请求仍然报告服务器地址。那种在适配器启动前正常、启动后卡死的会话,就是前面说的循环。

把 DNS 与 UDP 一路追到链路尽头

OpenSSH 的动态转发只承载 TCP 连接。它不是 UDP associate 或 ICMP 传输,适配器也无法靠接受 UDP 报文凭空造出 UDP 转发。任何坚持走 UDP 的东西,包括 QUIC 和 WebRTC 媒体,都会视应用而定失败或退回 TCP。

DNS 需要单独决策。在本地解析域名的应用会在连接之前就通过普通解析器发出查询,于是隧道承载了连接却没有承载这次查询。带远程 DNS 选项的代理感知客户端能避开这一点;被截获却没有该选项的应用不行。与其假定适配器改掉了它,不如去看究竟是哪个解析器回答的。

让 SOCKS 监听端口保持绑定在 loopback,首次使用时校验 SSH 服务器的主机密钥,并为最终网站保留正常的 HTTPS 校验。SSH 保护的是手机到服务器这一段,它并不加密服务器之后的行为。

检查两条生命线并做决定

适配器和 SSH 会话有各自独立的生命周期。如果 SSH 掉线而适配器继续截获,应用可能毫无明显错误地卡住,因为它们的报文仍然进入一个已经没有可用出口的虚拟接口。如果适配器停止,应用要么直连,要么被阻断,取决于设备的 VPN 策略。

有意识地测试两个方向:在适配器运行时杀掉 SSH 会话,然后在 SSH 保持在线时停掉适配器。手机在每种情况下表现出的行为,才是你真正在部署的策略,安装说明怎么写都无关紧要。

如果工作流程需要整机覆盖并要有可预测的失效方式,诚实的答案是真正的 IP VPN,或一个为此链路设计并持续维护的适配器。把一个 shell 会话搁在一个无关的截获应用旁边跑着,并不算受维护的设备隧道;那是两个各自结束时都没有约定的部件。

相关文章