深蓝黑背景封面:右侧大标题「TTL 加一,就多摸一跳」,副标题「逐跳探测与 ICMP 超时报文」;左侧原创示意图——源主机向右依次经过三个路由器到达目标,三个探测包分别标 TTL=1、TTL=2、TTL=3,每个包在对应跳折返,虚线回箭标注「ICMP 超时」;左上标注「面试经典 · 网络」,右上标注「Day 05 · 11-02」 图 1:本文封面,TTL 每加一,探测就深一跳。

先说明属性:这是一篇历史笔试题复盘,题目出自本地历史笔试资料,与任何公司的当前招聘无关。题目大意:traceroute 是如何工作的,为什么默认用 UDP。这道题漂亮在它不需要任何特权协议——只用 IP 头部一个字段和两种现成的 ICMP 报文,就把整条转发路径反推了出来。

核心机制:拿 TTL 当尺子用。

IP 头部有个 TTL(Time To Live)字段,每经过一个路由器减一,减到零就丢弃。RFC 792 规定:路由器丢弃 TTL 归零的报文时,可以向源主机回一个 ICMP Time Exceeded 报文(type 11,code 0 表示传输中超时),而这个回报文的源地址,正是这台路由器自己。

traceroute 的做法是把这个「报错义务」变成测量工具:先发出 TTL=1 的探测包,它在第一个路由器处归零被丢,源主机收到来自第一跳的 ICMP 超时——第一跳是谁,暴露了。再发 TTL=2,第二跳暴露。如此递增,每加一就多摸一跳,直到探测包真正抵达目标主机。Linux 实现的 man 手册写得很清楚:默认最大探测 30 跳,每个 TTL 默认发 3 个探测包——既能量出这一跳的往返时延分布,也容忍单个包丢失。

终止信号:为什么默认用 UDP。

逐跳探测只差最后一环:怎么知道「到终点了」?中间跳回的都是 ICMP 超时,终点必须回一个不一样的报文。经典实现的答案很巧妙:探测包用 UDP,目的端口设成一个几乎不会有程序监听的高位端口(默认从 33434 起递增)。包到达目标主机后,UDP 层发现端口没人收,协议栈回一个 ICMP Destination Unreachable,code 3 即「端口不可达」。源主机一看:这次回来的不是超时而是不可达——到头了,收工。

用 UDP 而不是 TCP 的原因也在这里:探测包不该打扰目标上的任何真实服务,选一个「不存在的端口」让协议栈代为报错,目标应用全程无感知。另一类实现(Windows 的 tracert、Linux 的 traceroute -I)用 ICMP Echo 做探测包,到达目标时收到的是正常的回显应答(Echo Reply),语义相同;差别在于过防火墙的待遇——很多网络默认放行 ICMP 回显却拦陌生 UDP 高位端口,也有的反过来,所以现代实现还提供 traceroute -T,用 TCP SYN 伪装成正常连接来穿透过滤。

把机制用代码跑一遍。

下面这段语义模拟实现了 TTL 递减、超时回包、每跳三包、星号超时与两种终止报文,用 Node.js 22.15.0 直接运行(node traceroute.js):

'use strict';

// traceroute 语义模拟:TTL 递增探测,ICMP 超时 vs 端口不可达
function probe(routers, target, ttl, { dropReplies = new Set(), icmpEcho = false } = {}) {
  let hopsLeft = ttl;
  for (let hop = 0; hop < routers.length; hop++) {
    hopsLeft--; // 每经过一个路由器 TTL 减一
    if (hopsLeft === 0) { // 归零被丢弃,该跳回 ICMP Time Exceeded
      if (dropReplies.has(hop)) return { hop, reply: null }; // 超时未回:星号
      return { hop, reply: { from: routers[hop], type: 11, code: 0, note: 'Time Exceeded' } };
    }
  }
  // 穿过全部路由器抵达目标:UDP 高端口无人监听 -> 端口不可达;ICMP echo -> 正常回显
  return icmpEcho
    ? { hop: routers.length, reply: { from: target, type: 0, code: 0, note: 'Echo Reply' } }
    : { hop: routers.length, reply: { from: target, type: 3, code: 3, note: 'Port Unreachable' } };
}

function traceroute(routers, target, opts = {}) {
  const rows = [];
  for (let ttl = 1; ttl <= 30; ttl++) {
    const results = [];
    for (let q = 0; q < 3; q++) { // 每个 TTL 默认发 3 个探测包
      const r = probe(routers, target, ttl, opts);
      results.push(r.reply ? `${r.reply.from} ${r.reply.note}` : '*');
    }
    const reached = results.some((s) => s.includes('Unreachable') || s.includes('Echo Reply'));
    rows.push({ ttl, results });
    if (reached) break; // 拿到「到达」信号即结束
  }
  return rows;
}

const routers = ['192.168.1.1', '10.10.0.1', '172.16.0.2'];
const target = '203.0.113.8';

console.log('--- UDP 默认方法(目标触发端口不可达)---');
for (const r of traceroute(routers, target)) console.log(`TTL=${r.ttl}  ${r.results.join(' | ')}`);
console.log('--- 第 2 跳不回包(被过滤/丢包),显示星号 ---');
for (const r of traceroute(routers, target, { dropReplies: new Set([1]) }))
  console.log(`TTL=${r.ttl}  ${r.results.join(' | ')}`);
console.log('--- ICMP Echo 方法(目标正常回显)---');
for (const r of traceroute(routers, target, { icmpEcho: true }))
  console.log(`TTL=${r.ttl}  ${r.results.join(' | ')}`);

实测输出(节选):

--- UDP 默认方法(目标触发端口不可达)---
TTL=1  192.168.1.1 Time Exceeded | 192.168.1.1 Time Exceeded | 192.168.1.1 Time Exceeded
TTL=2  10.10.0.1 Time Exceeded | 10.10.0.1 Time Exceeded | 10.10.0.1 Time Exceeded
TTL=3  172.16.0.2 Time Exceeded | 172.16.0.2 Time Exceeded | 172.16.0.2 Time Exceeded
TTL=4  203.0.113.8 Port Unreachable | 203.0.113.8 Port Unreachable | 203.0.113.8 Port Unreachable
--- 第 2 跳不回包(被过滤/丢包),显示星号 ---
TTL=2  * | * | *
--- ICMP Echo 方法(目标正常回显)---
TTL=4  203.0.113.8 Echo Reply | 203.0.113.8 Echo Reply | 203.0.113.8 Echo Reply

机制拆解图:上半部分时序——源主机依次发出 TTL=1、2、3 的探测包,第 1 个包在第一个路由器 TTL 归零被丢弃并回 ICMP 超时报文,报文源地址暴露了这一跳是谁;TTL 递增重复此过程,直到包抵达目标。上半右注「每个 TTL 发 3 个包:估往返时延、容忍单个丢包」。下半部分两个实现差异卡:左「经典实现用 UDP 高端口」——探测包指向一个几乎不会监听的高位端口,到达目标时触发「端口不可达」,以此区别于中间跳的超时报文;右「另一类实现用 ICMP Echo」——到达目标时收到正常回显,语义相同、过防火墙的待遇不同。底部读输出指南:星号是没回包(被过滤或丢包),某跳起时延陡增说明从那一跳开始拥塞 图 2:超时报文暴露每一跳,不可达报文宣告终点。

怎么读真实输出,可能的追问。

真实输出里每一行是一跳,三个数字是三个探测包的往返时延。星号表示这个包没在超时时间内等到回包——可能是路由器限流了 ICMP、回包被过滤,也可能只是丢了。如果某一跳之后时延整体陡增,说明从那一跳开始的链路在劣化;跨机房调用模型服务抖动时,这就是「哪一跳开始变差」的第一证据。「为什么同一跳可能显示多个地址?」等价多路径负载均衡下,三个探测包可能走了不同路径,实现会把每个应答者都列出来。「traceroute 看到的就是真实转发路径吗?」只是当时、那类探测包走的路径;往返路径可能不对称,换 UDP/ICMP/TCP 方法结果也可能不同——这也是排查时要多试几种方法的原因。

回到笔试现场,建议的答题结构:TTL 递减与 ICMP 超时报文是第一层,UDP 高端口触发端口不可达作终止信号是第二层,每跳三包、星号、30 跳上限等工程细节是第三层,最后补一句 Windows 用 ICMP Echo 的实现差异与防火墙穿透。你排查网络抖动时用过 traceroute 或 mtr 吗?星号最多的一跳最后查出是什么原因?欢迎分享。