Web 前端思考题:如何获取往返数据包的 TTL
注意,这里说的是「往返」,即去程和返程,也就是「服务端」收到「客户端数据包」的 TTL,和「客户端」收到「服务端数据包」的 TTL。
注意,这里说的是「接收」,毕竟发送时的 TTL 毫无意义,服务端固定已知,客户端通常是 64 或 128(和操作系统相关)。
这个问题的初衷,是想通过 JS 检测用户和服务器之间的往返路由数。虽然大多情况下往返链路相近,但也存在差异较大的情况。例如访问某些香港服务器,去程电信直连,而返程则会到日本绕一圈,链路差异非常大。
去程 TTL 很容易获取,直接读取 Web 服务器的 socket 即可(例如通过 getsockopt)。或者用 raw socket/libpcap 抓包也不难实现。
返程 TTL 就没那么容易获取了。
也许你会说,可通过服务器 traceroute 反查。这种方案虽然可行,但并不准确。如今用户几乎都在内网中,反查只能到用户的公网路由器,内网的路由数仍无法获取。而且反查效率很低,需要发不少数据包。
但用 JS 读取数据包 TTL 更不可行,毕竟浏览器功能十分有限。抓包这种操作,想都不用想;而 getsockopt 这类高权限 API,浏览器显然不可能提供。因此我们得另辟蹊径。
设想下,假如客户端能把某个「收到的数据包」封装在「另一种数据包」的内容里回传给服务器,那我们就可以在服务器上获取返程信息了。
事实上,这种情况是存在的!「ICMP 端口不可到达」协议专做这事。
那么,客户端在什么情况下会触发这种 ICMP?只要浏览器在收到服务器数据包之前断开连接即可。之后数据包达到时,由于操作系统找不到目标端口对应的连接,只能丢弃该包,同时回复一个「ICMP 端口不可到达」告知服务器。
该 ICMP 的内容部分,正是被丢弃包的网络层和传输层头,包含了我们想要的返程信息。
因此我们的核心思路:
JS 向服务器发包
服务器稍作延迟,回复任意内容
JS 在收到包之前释放连接
服务器抓取 ICMP (type=3, code=3) 类型的包
由于 TCP 连接是操作系统维护的,JS 难以精确控制释放时间,因此我们使用更简单的 UDP。通过 WebRTC 即可实现 UDP 连接的创建和关闭。
并且 TCP 是有状态的,连接断开后 NAT 会删除条目,服务器返回的数据包可能进不了内网。而 UDP 是无状态的,本地关闭连接对 NAT 毫无感知,此后一段时间里数据仍可进入内网。
实现比思考简单得多,这里就不讲解了。
需要注意的是,JS 向服务器发送 UDP 包时需携带一个 uuid 数据,用于之后获取数据时的关联。
在线演示:https://www.etherdream.com/test/stunex.html
不过并非所有情况下都能成功获取,例如有些操作系统禁用了端口不可到达的响应,有些运营商会丢弃这类 ICMP 包。
如果演示页面显示 fail,那就是无法获取了。(如果是 network err 可能测试服务关了~)
当然,本文纯属开脑洞而已,顺便分享一点点网络小知识~