bigywater.tech
网络协议 · 13

TTL 与 traceroute:逐跳探测的原理

TTL 到底是怎么变成“traceroute 的每一跳”的?为什么有的跳显示 * * *,同一跳还会出现几个不同的地址?这篇在真实公网路径上抓包,逐个看探测包、路由器回的 ICMP,再自己用 IP_TTL 写一个 traceroute,最后用收到的包的 TTL 估算跳数并和实测对比。

15 跳
到一个公网地址的系统 traceroute 结果:第 5、14 跳全无应答,6 个跳出现多个地址
11/0 与 3/3
中间路由器回 ICMP Time Exceeded,目的地回端口不可达;引用的原包里 TTL 都是 1
15 / 20 对 6 / 20
同样 20 个探测:目的端口从 33435 起收到 15 个回复,从 34500 起只有 6 个

TTL 是什么

实测 · 本机执行输出
  net.inet.ip.ttl: 64
  TCP 套接字 getsockopt(IP_TTL) 默认 = 64;setsockopt 设成 7 以后读回 = 7
  UDP 套接字 getsockopt(IP_TTL) 默认 = 64;setsockopt 设成 7 以后读回 = 7
  给本机(127.0.0.1)发 TTL=1 的 UDP 包:对方收到 b'ttl1';抓到的包 TTL = 1——不经过路由器,TTL 不会被减
  ping -m 1 到公网地址:第 1 跳的路由器回了 “Time to live exceeded”(它是本机局域网网关)
  ping -m 2 到公网地址:第 2 跳的路由器回了 “Time to live exceeded”(它是私有地址)
  ping -m 3 到公网地址:第 3 跳的路由器回了 “Time to live exceeded”(它是私有地址)
  ping -m 1 到默认网关(它就是目的地,不需要再转发):收到应答

TTL 是 IP 头里 1 字节的生存时间,每过一个路由器减 1,到 0 就丢弃并回 ICMP Time Exceeded。系统默认 64,套接字上可用 IP_TTL 读写;发给本机不经路由器,所以不会被减。

traceroute 的输出

实测 · 本机执行输出
  第  1 跳  1/3 个应答  来自 1 个不同地址  RTT 3.2 ms  (本机局域网网关)
  第  2 跳  3/3 个应答  来自 1 个不同地址  RTT 4.3 / 2.7 / 3.0 ms  (私有地址)
  第  3 跳  3/3 个应答  来自 1 个不同地址  RTT 3.1 / 2.8 / 3.2 ms  (私有地址)
  第  4 跳  3/3 个应答  来自 1 个不同地址  RTT 4.5 / 6.8 / 12.0 ms  (公网地址)
  第  5 跳  0/3 个应答  (* * *)
  第  6 跳  2/3 个应答  来自 2 个不同地址  RTT 16.6 / 7.6 ms  (公网地址)
  第  7 跳  1/3 个应答  来自 1 个不同地址  RTT 237.2 ms  (公网地址)
  第  8 跳  3/3 个应答  来自 3 个不同地址  RTT 39.2 / 62.4 / 39.2 ms  (公网地址)
  第  9 跳  2/3 个应答  来自 2 个不同地址  RTT 50.0 / 48.2 ms  (公网地址)
  第 10 跳  3/3 个应答  来自 3 个不同地址  RTT 53.0 / 59.5 / 55.5 ms  (公网地址)
  第 11 跳  3/3 个应答  来自 1 个不同地址  RTT 49.8 / 63.0 / 53.8 ms  (公网地址)
  第 12 跳  3/3 个应答  来自 3 个不同地址  RTT 54.9 / 55.9 / 50.9 ms  (公网地址)
  第 13 跳  1/3 个应答  来自 1 个不同地址  RTT 182.9 ms  (公网地址)
  第 14 跳  0/3 个应答  (* * *)
  第 15 跳  3/3 个应答  来自 2 个不同地址  RTT 194.9 / 236.1 / 196.5 ms  (公网地址)
  目的地在第 15 跳应答;全部无应答的跳:[5, 14](共 2 个);同一跳的 3 个探测来自不止一个地址的跳有 6 个([6, 8, 9, 10, 12, 15]);最大 RTT 237.2 ms

每跳通常发 3 个探测,看到的是该跳的地址和 RTT。实测 15 跳里有 2 跳全无应答、6 跳的探测来自多个地址(路径上有多条并行链路),RTT 随跳数增大。

线上的样子

实测 · 本机执行输出
  抓到 16 个 UDP 探测包、11 个 ICMP 回复(类型 11 = Time Exceeded,类型 3 = Destination Unreachable)
    序号  发出的 TTL  目的端口  IP 总长  UDP 长度  →  回复 ICMP 类型/代码  引用原包里的 TTL  引用的目的端口  回复包自己的 TTL  RTT
      1        1      33435      40       20      11/0                  1               33435           64          2.6 ms
      2        2      33436      40       20      11/0                  1               33436           63          2.7 ms
      3        3      33437      40       20      11/0                  1               33437           62          2.7 ms
      4        4      33438      40       20      11/0                  1               33438          252          6.0 ms
      5        5      33439      40       20      无回复
      6        6      33440      40       20      无回复
      7        7      33441      40       20      无回复
      8        8      33442      40       20      11/0                  1               33442          248         37.7 ms
      9        9      33443      40       20      无回复
     10       10      33444      40       20      11/0                  1               33444          245         54.6 ms
     11       11      33445      40       20      11/0                  1               33445          244         54.6 ms
     12       12      33446      40       20      11/0                  1               33446          244         56.7 ms
     13       13      33447      40       20      无回复
     14       14      33448      40       20      11/0                  1               33448          239        243.4 ms
     15       15      33449      40       20      11/0                  1               33449          239        242.2 ms
     16       16      33450      40       20       3/3                  1               33450           48        199.4 ms
  目的端口从 33435 开始,每个探测加 1(到 33450);源端口都是同一个:True;所有探测 IP 总长都是 40 字节(UDP 长度 20,载荷 12 字节)
  Time Exceeded 10 个:引用原包里的 TTL 全是 [1](被引用的是路由器丢弃它时手里的那份包头);Destination Unreachable 1 个:代码 [3](3 = 端口不可达),来自目的地本身:True
  回复包自己的 TTL 取值: [48, 62, 63, 64, 239, 244, 245, 248, 252]——前三跳依次是 64、63、62(同一类设备,每多一跳回程少 1),后面几跳是 252、248、246……(接近 255,是另一类设备)

探测是 UDP 包,TTL 递增,目的端口从 33435 起逐个加 1;中间路由器回 ICMP 11/0 并引用原包头(其中的端口用来对应探测),目的地因为没人监听该端口回 3/3 端口不可达,traceroute 就知道到了。

自己实现

实测 · 本机执行输出
  目的端口从 33435 起、间隔 40 ms 发 TTL=1~20 共 20 个探测(抓包确认发出 20 个):收到 ICMP 回复 15 个;其中来自目的地本身的有 6 个
  目的端口从 33435 起、间隔 250 ms 发 TTL=1~20 共 20 个探测(抓包确认发出 20 个):收到 ICMP 回复 15 个;其中来自目的地本身的有 5 个
  目的端口从 33435 起、间隔 1000 ms 发 TTL=1~20 共 20 个探测(抓包确认发出 20 个):收到 ICMP 回复 18 个;其中来自目的地本身的有 6 个
  目的端口从 34500 起、间隔 250 ms 发 TTL=1~20 共 20 个探测(抓包确认发出 20 个):收到 ICMP 回复 6 个;其中来自目的地本身的有 0 个
  取回复最多的一轮(端口 33435 起、间隔 1000 ms)和系统 traceroute(-q 1)逐跳对比:两边都有应答的跳里地址相同 8 个、不同 4 个;只有我的有应答 6 个、只有系统的有应答 1 个
  那一轮里无应答的中间跳(取前 4 个):[6, 7]。对每个再间隔 1 s 发 8 个探测:TTL=6 收到 3/8;TTL=7 收到 0/8——“*” 只表示这次没收到回复,不一定是路由器不存在;不回的原因这里没有验证

设 IP_TTL 发 UDP 探测就能做 traceroute,回复要读 ICMP(没有原始套接字权限时从抓包里读)。探测间隔和目的端口都影响收到的回复数,无应答的跳多发几次有时能收到。

用 TTL 估跳数

实测 · 本机执行输出
  试了 5 个公网目的地,TCP 能连通 4 个
    目的地     SYN-ACK 的 TTL  推测初始值  按 TTL 估的跳数(初始值−TTL+1)  traceroute 实测到达的跳数
    目的地1         48             64                 17                       15
    目的地2         51             64                 14                       14
    目的地3         51             64                 14                       13
    目的地4         51             64                 14                       未到达
  估计与实测相同的有 1 / 4 个;差值(估计−实测):[2, 0, 1, None]
  给目的地 1 的 TCP 连接设 IP_TTL=n 再 connect(超时 1.2 s):n=15 是第一次成功的;n 更小时都失败,错误码分布 No route to host(65): n=[1, 2, 3];Resource temporarily unavailable(35): n=[4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];traceroute 说该目的地在第 15 跳
  给目的地 2 的 TCP 连接设 IP_TTL=n 再 connect(超时 1.2 s):n=14 是第一次成功的;n 更小时都失败,错误码分布 No route to host(65): n=[1, 2, 3, 10];Resource temporarily unavailable(35): n=[4, 5, 6, 7, 8, 9, 11, 12, 13];traceroute 说该目的地在第 14 跳

收到的包的 TTL 向上取到常见初始值,初始值 − TTL + 1 约等于跳数,实测与 traceroute 差 0~2;把 TCP 连接的 IP_TTL 从 1 往上试,第一次能连通的值与 traceroute 到达的跳数吻合。

分步演示

TTL 与 traceroute
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。公网路径上别人的路由器地址一律不写出来,只记类别与 RTT;每次重跑路径与应答的跳都会略有不同,文中数字来自最后一次运行;自己实现的 traceroute 通过 tcpdump 抓包读取 ICMP 回复(没有原始套接字权限),不是读套接字
    • 没有验证:IPv6 的 Hop Limit 与 traceroute6;TCP / ICMP 方式的 traceroute(-T / -I);路由器不回 Time Exceeded 或限速的具体原因;为什么目的端口 34500 起收到的回复明显更少(可能与路径上按端口的转发选择或过滤有关,未证实);多路径的哈希依据;MPLS 对 traceroute 的影响;connect 错误码 65 与 35 的完整对应关系
    京ICP备2026067502号-1京公网安备11010502063487号