Appearance
NAT 与 ICMP
概念
NAT(Network Address Translation,网络地址转换)是在"私有网络与公网的交界处"改写 IP 数据报首部里地址字段的技术——它让一个单位内部的成百上千台私有地址主机,通过极少量公网地址访问互联网;ICMP(Internet Control Message Protocol,网际控制报文协议)是 IP 层的"信使",负责把"送不到、走太久、参数不对"这三类坏消息回报给源主机,并提供回送探测能力。
一句话说清两者的关系:NAT 解决"地址不够用"(并在中间偷偷改了地址),ICMP 解决"出错了要通知谁"(在最极端的情况下,被 NAT 改坏的地址还会让 ICMP 认不出家)。
★ 本章要解决的三件事:
| 问题 | 对应内容 |
|---|---|
| 私有地址的主机怎么上网 | NAT 三种形态,重点是 NAPT(一对多) |
| NAT 的代价是什么 | 破坏端到端透明性、外网不能主动连内网、要重算校验和 |
| 网络出错了怎么通知源主机 | ICMP 差错报告报文 + 询问报文(ping / traceroute 的基础) |
与上一章的衔接:上一章(net/31-subnet.md)算出了"私有地址块与子网前缀",本章让 192.168.x.x 这些地址真的用起来;而上一章讲的"IP 首部检验和每跳重算"在本章有了第二重意义——NAT 改了地址,检验和必须跟着改。
原理
一、NAT 的动因与三种形态
动因:私有地址(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)在公网上"不可路由"——骨干路由器上没有到这些地址的路由条目,见到就直接丢弃。 但单位内部又确实需要大量地址(每台设备一个),于是"内部用私有、出网时换成公有"成了唯一现实的选择。
★ NAT 的三种形态(从"奢侈"到"普及"):
| 形态 | 映射关系 | 公网地址需求 | 典型场合 |
|---|---|---|---|
| 静态 NAT | 一对一,固定绑定 | 每台内网主机一个公网地址 | 对外提供服务的服务器 |
| 动态 NAT | 一对一,从地址池临时借 | 地址池大小 = 并发上网主机数上限 | 企业出口(主机数少于地址池) |
| NAPT / PAT | 多对一,靠端口区分 | 一个公网地址带一整个内网 | 家庭路由器、绝大多数出口 |
⚠️ 考试里的"NAT"通常专指第三种:NAPT(Network Address Port Translation,网络地址端口转换),也叫 PAT(Port Address Translation)或"地址复用"——因为它才是"一个公网 IP 带一片内网"那种人人都在用的形态。 题目写"NAT 把内网地址换成公网地址"时,默认就是 NAPT。
★ NAPT 转发表锚点(本章定死):
text
内网有两台主机, 同时访问同一个服务器 93.184.216.34:80
公网地址只有一个: 203.0.113.5
┌────────────────────┬─────────────────────┬──────────────────────────┐
│ 内网 (私有侧) │ 公网 (全局侧) │ 目的 (外网服务器) │
├────────────────────┼─────────────────────┼──────────────────────────┤
│ 192.168.1.10:1025 │ 203.0.113.5:40001 │ 93.184.216.34:80 │
│ 192.168.1.11:1025 │ 203.0.113.5:40002 │ 93.184.216.34:80 │
└────────────────────┴─────────────────────┴──────────────────────────┘
★ 两台主机的源端口都是 1025 —— 它们"撞车"了
★ 路由器把公网侧端口改成 40001 / 40002, 靠它区分
-> 回包到达 203.0.113.5:40002 时, 查表就知道该还给 192.168.1.11:1025★ 由锚点直接推出的三条结论:
| 结论 | 理由 |
|---|---|
| 仅凭"公网 IP"无法区分内网主机,必须看端口 | 锚点里两台内网主机的源端口完全相同(都是 1025) |
| 出方向改写"源 IP + 源端口",入方向改写"目的 IP + 目的端口" | 对称操作,靠同一张表 |
| 同一对内网主机访问不同服务器,也必须建不同的表项 | 否则回包不知道交给谁(表项键 = 内网 IP + 内网端口 + 目的 IP + 目的端口) |
★ 端口容量账(NAPT 的硬上限):
text
公网侧可用的临时端口范围: 1024 ~ 65535
数量 = 65535 - 1024 + 1 = 64512 个
★ 0 ~ 1023 是"熟知端口", 留给服务端使用, NAPT 一般不动
★ 实际设备还会留一段保留区, 有效值常比 64512 略少
锚点: 一个 192.168.1.0/24 (254 台主机) 内网
每台平均 100 条并发连接 -> 254 x 100 = 25400 <= 64512 够用
每台平均 300 条并发连接 -> 254 x 300 = 76200 > 64512 不够!
★ 不够的后果: 新连接被拒绝 (用户看到"网页打不开")
★ 解决办法: ① 再申请一个公网 IP (端口池翻倍) ② 买端口更多的"多 IP 出口"
③ 缩短连接空闲超时 (更快回收端口)⚠️ NAPT 的"会话超时"是必需品:表项不会永久占用——TCP 连接关闭后经过一小段"等待期"即回收端口;UDP 则靠"一段时间无流量就回收"(通常几十秒)。 若没有回收,64512 个端口很快被耗尽,"能上网的只有前 6 万台设备"就成了硬限制。
二、NAT 的代价:四项不友好
NAT 是"用工程手段把地址枯竭往后拖"的产物,代价必须记住:
| 代价 | 具体表现 |
|---|---|
| ① 破坏端到端透明性 | 公网侧看不到内网真实地址,中间设备可以随意改写地址——互联网原本"任意两台主机可直连"的设计前提被打破 |
| ② 外网不能主动连内网 | 除非事先配置"端口映射 / 静态映射"(DNAT),否则外网发起的连接进不来——这正是"家里的电脑不能被外面直接连"的原因 |
| ③ 校验和必须重算 | 改了 IP 地址 → IP 首部检验和要重算;若同时改端口 → TCP/UDP 伪首部的检验和也要重算 |
| ④ 载荷里嵌了地址的协议会失效 | IPSec、FTP 主动模式、SIP 等把 IP/端口写在应用层载荷里——NAPT 只改首部,改不到载荷,要靠 ALG(应用层网关)这种"打补丁的中间盒"逐个破解 |
⚠️ 第 ③ 条是"为什么 NAT 设备比纯路由器累":纯路由器只改 TTL 并重算 IP 首部检验和;NAPT 还要改端口 + 重算传输层伪首部——一个包被改了三处的校验,转发成本明显更高。 这也解释了"为什么 NAT 会降低转发性能"。
⚠️ 分片与 NAT 的冲突(常考的边角):若一个数据报已被分片,只有第 1 片含传输层首部(端口号)——后续分片里没有端口信息,NAPT 无法按端口建表项。 实际设备的做法是"让第一片先到、把后续分片按 IP 层的"标识"归并"(所以 NAT 设备要维护"分片关联表"),处理复杂度进一步上升。
★ NAT 与 IPv6 的关系(一句话):NAT 是把"地址不够"拖下去,IPv6 是把"地址多到用不完"一步到位——所以在 IPv6 世界里 NAT 本应消失(但现实中仍有企业用 NAT66 图省事,见 net/34-ipv6.md)。
三、ICMP:IP 层的信使
IP 协议自己不保证送达,也不回任何消息——"送不到"这件事必须由别人通知源主机,这个"别人"就是 ICMP。
★ ICMP 报文的结构(首部固定 8 B):
text
┌─────────────────────────── ICMP 报文 (封装在 IP 数据报中) ───────────────┐
│ ┌────────┬────────┬────────────┬────────────────────────────────────┐ │
│ │ 类型 │ 代码 │ 检验和 │ 其余 4 字节 (随类型而变) │ │
│ │ 1 B │ 1 B │ 2 B │ 4 B │ │
│ └────────┴────────┴────────────┴────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ 数据部分 (差错报文则放"出错数据报的首部 + 前 8 字节") │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
★ ICMP 首部固定 8 B; 封装它的 IP 首部里, 协议号 = 1
★ 校验和覆盖"整个 ICMP 报文"(首部 + 数据), 不只首部★ 两大类 ICMP 报文:
| 大类 | 用途 | 代表类型 |
|---|---|---|
| 差错报告报文 | 告诉源主机"你的报文出事了" | 终点不可达(3)、时间超过(11)、参数问题(12)、改变路由(5) |
| 询问报文 | "你在吗 / 现在几点"这类交互 | 回送请求/回答(8 / 0)、时间戳请求/回答(13 / 14) |
★ 类型与代码全表(本章定死):
| 类型 | 名称 | 代码 | 含义 |
|---|---|---|---|
| 0 | 回送回答(Echo Reply) | 0 | ping 的响应 |
| 3 | 终点不可达(Destination Unreachable) | 0 | 网络不可达 |
| 3 | 终点不可达 | 1 | 主机不可达 |
| 3 | 终点不可达 | 2 | 协议不可达 |
| 3 | 终点不可达 | 3 | 端口不可达(UDP 打到一个没监听的端口就会收到它) |
| 3 | 终点不可达 | 4 | 需要分片但 DF 位置 1(路径 MTU 发现的机制来源) |
| 5 | 改变路由(重定向) | 0 | 对网络重定向(让主机把下一跳改成更优的路由器) |
| 8 | 回送请求(Echo Request) | 0 | ping 发出 |
| 9 / 10 | 路由器通告 / 询问 | 0 | 主机发现本网路由器(常被 RIP 之外的场景忽略) |
| 11 | 时间超过(Time Exceeded) | 0 | TTL 减到 0(traceroute 靠它) |
| 11 | 时间超过 | 1 | 分片重组超时(重组定时器到期) |
| 12 | 参数问题(Parameter Problem) | 0 | 首部字段有错,指针指出出错字节 |
| 13 / 14 | 时间戳请求 / 回答 | 0 | 测时延与时钟偏移 |
| 17 / 18 | 地址掩码请求 / 回答 | 0 | 早期主机查本网掩码(已废弃) |
⚠️ 已废弃的两个:类型 4"源点抑制"(Source Quench)在 RFC 6633 里被正式废止——因为"让路由器通知主机减速"这件事已被 TCP 自己的拥塞控制接管,ICMP 再插一手只会帮倒忙(拥塞控制见
net/44-congestion.md);类型 15 / 16"信息请求/回答"也早已废弃。 408 教材仍会列出类型 4,知道"它已废弃"即可。
★ 差错报告报文的载荷:为什么要塞"出错数据报的首部 + 前 8 字节"?
text
ICMP 差错报文的数据部分 =
出错的那个 IP 数据报的 "首部 (20 B)" + "数据部分的前 8 字节"
第①个 20 B 的作用: 让源主机知道是哪一个数据报出错了
(里面有源 IP / 目的 IP / 协议 / 标识)
第②个 8 B 的作用: ★ 前 8 字节恰好覆盖 TCP/UDP 首部里的
"源端口 (2 B) + 目的端口 (2 B) + 其余 4 B"
-> 源主机据此判断"是我哪个进程的报文出错了"
-> 交给对应的 socket 处理
★ 报文总长账:
ICMP 头 8 B + 载荷 (20 + 8) = 36 B
+ 外层 IP 首部 20 B = 56 B
★ 这 56 B 是"一个最小 ICMP 差错报文"的标准尺寸⚠️ 差错的"两次封装"关系要看清楚:内层是被引发差错的那个数据报(原封不动抄首部 + 8 字节),外层是"承载 ICMP 报文的那个 IP 数据报"。 答题时若问"ICMP 差错报文的长度",要明确是"ICMP 报文本身(36 B)"还是"整个 IP 数据报(56 B)"——这两个数都可能被考。
★ 哪些情况下不再发送 ICMP 差错报文(四条,必背):
| 情形 | 理由 |
|---|---|
| ① 对 ICMP 差错报告报文本身,不再发差错 | 否则"差错引发差错"会无限套娃(ICMP 出错的报文再报一次错,就形成链式风暴) |
| ② 对非第一个分片的数据报片,不发差错 | 后续分片里没有传输层首部,且重组前"报错"意义不大 |
| ③ 对具有多播地址的数据报,不发差错 | 一个多播报文可能有成千上万个接收者,逐个报错等于自我放大 |
④ 对特殊地址(127.0.0.0/8、0.0.0.0 等)的数据报,不发差错 | 这些地址本来就不该出现在公网链路上 |
四、ping 与 traceroute:ICMP 的两个应用
★ ping 的原理(最简单):向目标发 ICMP 回送请求(类型 8),对方回 ICMP 回送回答(类型 0)——两者的时间差就是往返时延 RTT。
★ traceroute 的原理(利用 TTL 的"副作用"):
text
Linux 的 traceroute (默认用 UDP):
① 发出 3 个 "TTL = 1" 的数据报, 目的端口取一个高端口 (如 33435 起递增)
② 第 1 跳路由器把 TTL 减到 0 -> 丢弃 -> 回 ICMP 超时 (类型 11, 代码 0)
★ 这个 ICMP 报文的 "源 IP" 就是第 1 跳路由器地址 -> 记下来
③ 再发 "TTL = 2" 的, 拿到第 2 跳; 依次递增, 直到抵达目标
④ 抵达目标时:
- Linux 版: 目标机发现端口没监听 -> 回 ICMP 端口不可达 (类型 3, 代码 3) -> 结束
- Windows 版 (tracert): 用 ICMP 回送请求打过去, 收到回送回答 -> 结束
★ 关键点: 源主机不是"查到"路径的, 而是"故意让每一跳都失败一次"骗出来的⚠️
traceroute为什么"知道自己是第几跳":因为每一轮发出的数据报 TTL 是人工设定的——第 2 轮的数据报只可能"走到第 2 跳就被丢",所以回 ICMP 超时的那个路由器的地址,就是第 2 跳。 这是"用 TTL 做路径测量"的标准技巧。
⚠️ 一个常考的细节:若某一跳不回应(路由器配置成不回 ICMP 或过滤了 ICMP 超时),
traceroute会打印* * *——这一跳之后仍在继续(因为后续数据报照样走),所以"星号"不代表路径断了。
★ ICMP 的位置(一句话):ICMP 属于网络层,但它的报文要被封装进 IP 数据报传输——所以它既是"网络层协议",又是"IP 的用户";这也解释了为什么 IP 首部里的协议号字段会有 1 = ICMP 这一项。
示例
例 1:C 实现——NAPT 转发表与端口容量账
参数:公网地址
203.0.113.5,临时端口从 40001 起分配;内网三台主机访问两个服务器。
#include <stdio.h>
#define MAXN 8
/* NAPT 表项 */
typedef struct {
unsigned lip; /* 内网 IP */
int lport; /* 内网源端口 */
unsigned dip; /* 目的 IP */
int dport; /* 目的端口 */
int gport; /* 公网侧端口 (0 = 空闲) */
} Entry;
unsigned g_next = 40001; /* 公网端口分配起点 */
void show(unsigned x) {
printf("%u.%u.%u.%u", (x >> 24) & 0xFFu, (x >> 16) & 0xFFu,
(x >> 8) & 0xFFu, x & 0xFFu);
}
int translate(Entry *t, int n, unsigned lip, int lport, unsigned dip, int dport) {
int i, free_idx = -1;
for (i = 0; i < n; i++) {
if (t[i].gport == 0) {
if (free_idx < 0) free_idx = i;
continue;
}
if (t[i].lip == lip && t[i].lport == lport && t[i].dip == dip && t[i].dport == dport)
return t[i].gport; /* 命中已有表项 */
}
if (free_idx < 0) return -1; /* 表满 */
t[free_idx].lip = lip;
t[free_idx].lport = lport;
t[free_idx].dip = dip;
t[free_idx].dport = dport;
t[free_idx].gport = (int)g_next++;
return t[free_idx].gport;
}
int main(void) {
Entry t[MAXN];
int i, gp;
unsigned srv1 = (93u << 24) | (184u << 16) | (216u << 8) | 34u;
unsigned srv2 = (198u << 24) | (51u << 16) | (100u << 8) | 7u;
/* 五条请求: 三台内网主机访问两个服务器, 最后一条重复第一条 */
unsigned lip[5] = {0xC0A8010Au, 0xC0A8010Bu, 0xC0A8010Cu, 0xC0A8010Au, 0xC0A8010Au};
int lport[5] = {1025, 1025, 1025, 1025, 1025};
unsigned dip[5] = {0, 0, 0, 0, 0};
int dport[5] = {80, 80, 80, 443, 80};
for (i = 0; i < MAXN; i++) t[i].gport = 0;
dip[0] = dip[1] = dip[2] = srv1;
dip[3] = srv2;
dip[4] = srv1;
printf("① NAPT 转发表 (公网地址 203.0.113.5)\n");
for (i = 0; i < 5; i++) {
gp = translate(t, MAXN, lip[i], lport[i], dip[i], dport[i]);
printf(" 请求 %d: ", i + 1);
show(lip[i]);
printf(":%d -> ", lport[i]);
show(dip[i]);
printf(":%d 分配公网端口 = %d\n", dport[i], gp);
}
printf(" ---- 最终转发表 ----\n");
for (i = 0; i < MAXN; i++) {
if (t[i].gport == 0) continue;
printf(" ");
show(t[i].lip);
printf(":%-5d", t[i].lport);
printf(" <-> 203.0.113.5:%-5d", t[i].gport);
printf(" -> ");
show(t[i].dip);
printf(":%d\n", t[i].dport);
}
printf("\n");
printf("② 端口容量账\n");
printf(" 公网侧可用临时端口 = 65535 - 1024 + 1 = %d 个\n", 65535 - 1024 + 1);
printf(" 254 台主机 x 100 条并发 = %d -> %s\n", 254 * 100,
254 * 100 <= 64512 ? "够用" : "不够");
printf(" 254 台主机 x 300 条并发 = %d -> %s\n", 254 * 300,
254 * 300 <= 64512 ? "够用" : "不够");
printf("\n");
printf("③ ICMP 差错报文长度账\n");
printf(" ICMP 首部 8 + 载荷 (IP 首部 20 + 前 8 字节 8) = %d B\n", 8 + 20 + 8);
printf(" 外层再套 IP 首部 20 B -> 整个 IP 数据报 = %d B\n", 8 + 20 + 8 + 20);
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
① NAPT 转发表 (公网地址 203.0.113.5)
请求 1: 192.168.1.10:1025 -> 93.184.216.34:80 分配公网端口 = 40001
请求 2: 192.168.1.11:1025 -> 93.184.216.34:80 分配公网端口 = 40002
请求 3: 192.168.1.12:1025 -> 93.184.216.34:80 分配公网端口 = 40003
请求 4: 192.168.1.10:1025 -> 198.51.100.7:443 分配公网端口 = 40004
请求 5: 192.168.1.10:1025 -> 93.184.216.34:80 分配公网端口 = 40001
---- 最终转发表 ----
192.168.1.10:1025 <-> 203.0.113.5:40001 -> 93.184.216.34:80
192.168.1.11:1025 <-> 203.0.113.5:40002 -> 93.184.216.34:80
192.168.1.12:1025 <-> 203.0.113.5:40003 -> 93.184.216.34:80
192.168.1.10:1025 <-> 203.0.113.5:40004 -> 198.51.100.7:443
② 端口容量账
公网侧可用临时端口 = 65535 - 1024 + 1 = 64512 个
254 台主机 x 100 条并发 = 25400 -> 够用
254 台主机 x 300 条并发 = 76200 -> 不够
③ ICMP 差错报文长度账
ICMP 首部 8 + 载荷 (IP 首部 20 + 前 8 字节 8) = 36 B
外层再套 IP 首部 20 B -> 整个 IP 数据报 = 56 B⚠️ 三点说明:
- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出逐字一致。
- 请求 5 与请求 1 撞车、却拿到同一个公网端口:表项键是"内网 IP + 内网端口 + 目的 IP + 目的端口"四元组——请求 5 与请求 1 的四元组完全相同,所以命中已有表项、返回 40001。 这正是"同一条连接不重复建表"的实现方式;请求 4 只是目的地址不同,就新建了 40004——这解释了"为什么内网一台主机访问 100 个网站会占 100 个公网端口"。
0xC0A8010A就是192.168.1.10:0xC0 = 192、0xA8 = 168、0x01 = 1、0x0A = 10——用十六进制常量而不是"点分四元组"是为了让show()在运行时拆解,与net/31-subnet.md的写法保持一致。
例 2:Python——ICMP 类型表、NAPT 账与 traceroute 轨迹
def pad(s, w):
"""按"显示宽度"补空格: 汉字算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
print('=== ① ICMP 类型与代码表 ===')
types = [('0', '回送回答', '0', 'ping 的响应'),
('3', '终点不可达', '0', '网络不可达'),
('3', '终点不可达', '1', '主机不可达'),
('3', '终点不可达', '2', '协议不可达'),
('3', '终点不可达', '3', '端口不可达'),
('3', '终点不可达', '4', '需要分片但 DF = 1'),
('4', '源点抑制', '0', '已废弃 (RFC 6633)'),
('5', '改变路由', '0', '对网络重定向'),
('8', '回送请求', '0', 'ping 发出'),
('9/10', '路由器通告/询问', '0', '主机发现本网路由器'),
('11', '时间超过', '0', 'TTL 减到 0 (traceroute 靠它)'),
('11', '时间超过', '1', '分片重组超时'),
('12', '参数问题', '0', '指针指出出错字节'),
('13/14', '时间戳请求/回答', '0', '测时延与时钟偏移')]
print(' ' + pad('类型', 8) + pad('名称', 20) + pad('代码', 6) + '含义')
for t, n, c, m in types:
print(' ' + pad(t, 8) + pad(n, 20) + pad(c, 6) + m)
print(' ★ 差错报告: 3 / 5 / 11 / 12; 询问: 8-0 / 13-14 / 17-18')
print()
print('=== ② ICMP 差错报文的载荷与长度 ===')
print(' ' + pad('部分', 24) + pad('长度', 8) + '作用')
for a, b, c in [('内层: 出错 IP 首部', '20 B', '源/目的 IP、协议、标识 -> 定位是哪个数据报'),
('内层: 出错报文前 8 B', '8 B', '★ 正好含 TCP/UDP 的源端口 + 目的端口'),
('ICMP 首部', '8 B', '类型 + 代码 + 校验和'),
('外层 IP 首部', '20 B', '承载 ICMP 的"信封"')]:
print(' ' + pad(a, 24) + pad(b, 8) + c)
print(' ICMP 报文本身 = 8 + 20 + 8 = %d B' % (8 + 20 + 8))
print(' 整个 IP 数据报 = %d + 20 = %d B' % (8 + 20 + 8, 8 + 20 + 8 + 20))
print(' ★ 为什么恰好取 8 B: 传输层首部前 8 B 里就是两个端口 -> 能认出是"哪个进程"')
print()
print('=== ③ 不再发送 ICMP 差错的四种情形 ===')
for i, s in enumerate(['对 ICMP 差错报告报文本身, 不再发差错 (防链式风暴)',
'对非第一个分片的数据报片, 不发差错 (片里没有端口)',
'对多播地址的数据报, 不发差错 (接收者可能成千上万)',
'对特殊地址 (127.0.0.0/8, 0.0.0.0 等), 不发差错'], 1):
print(' %d. %s' % (i, s))
print()
print('=== ④ NAPT 端口容量账 ===')
pool = 65535 - 1024 + 1
print(' 公网侧可用临时端口 = 65535 - 1024 + 1 = %d 个' % pool)
print(' ' + pad('每台并发', 10) + pad('254 台总连接数', 16) + pad('是否够用', 12) + '缺口')
for per in [50, 100, 200, 300, 500]:
tot = 254 * per
need = 1
while pool * need < tot:
need += 1
print(' ' + pad(str(per), 10) + pad(str(tot), 16) + pad('够用' if tot <= pool else '不够', 12)
+ ('—' if tot <= pool else '需 %d 个公网 IP' % need))
print(' ★ 固定单端口每台 120 条约到顶: (254 x 120 = 30480) x 2 才接近 64512')
print()
print('=== ⑤ NAT 四项代价 ===')
for a, b in [('破坏端到端透明性', '公网侧看不到内网地址, 中间设备可改写'),
('外网不能主动连内网', '除非配置端口映射 (DNAT)'),
('校验和必须重算', 'IP 首部 + TCP/UDP 伪首部'),
('载荷嵌地址的协议失效', 'IPSec / FTP 主动模式 / SIP, 靠 ALG 打补丁')]:
print(' ' + pad(a, 22) + b)
print()
print('=== ⑥ traceroute 轨迹模拟 (源 S, 目的 T, 共 4 跳) ===')
hops = [('R1', '10.0.0.1'), ('R2', '10.0.0.2'), ('R3', '10.0.0.3'), ('T', '203.0.113.9')]
print(' ' + pad('轮次', 8) + pad('发出 TTL', 10) + pad('丢弃它的路由器', 16) + '回应的 ICMP')
for i, (name, addr) in enumerate(hops, 1):
if i < len(hops):
print(' ' + pad(str(i), 8) + pad(str(i), 10) + pad('%s (%s)' % (name, addr), 16)
+ '类型 11 代码 0 (时间超过)')
else:
print(' ' + pad(str(i), 8) + pad(str(i), 10) + pad('%s (%s)' % (name, addr), 16)
+ '类型 3 代码 3 (端口不可达) 或 类型 0 (回送回答)')
print(' 每个探测包 x 3 次, 共发 %d 个报文' % (len(hops) * 3))
print(' ★ Linux 用 UDP 高端口 + 端口不可达判终点; Windows 用 ICMP 回送回答判终点')
print(' ★ 某一跳不回应则显示 "* * *", 但后续仍继续 —— 星号不代表路径断了')
print()
print('=== ⑦ NAT 与 IPv6: 同一问题的两条路 ===')
print(' NAT : 把"地址不够"往后拖, 代价是破坏端到端透明性')
print(' IPv6 : 128 位地址, 多到用不完 -> NAT 本应消失 (见 net/34-ipv6.md)')
print(' 私有块 (net/31-subnet.md): 10.0.0.0/8 + 172.16.0.0/12 + 192.168.0.0/16')
print(' 私有地址总量 = %d 个 (仅占 2^32 的 %.2f%%)'
% (2 ** 24 + 2 ** 20 + 2 ** 16, (2 ** 24 + 2 ** 20 + 2 ** 16) * 100.0 / 2 ** 32))
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① ICMP 类型与代码表 ===
类型 名称 代码 含义
0 回送回答 0 ping 的响应
3 终点不可达 0 网络不可达
3 终点不可达 1 主机不可达
3 终点不可达 2 协议不可达
3 终点不可达 3 端口不可达
3 终点不可达 4 需要分片但 DF = 1
4 源点抑制 0 已废弃 (RFC 6633)
5 改变路由 0 对网络重定向
8 回送请求 0 ping 发出
9/10 路由器通告/询问 0 主机发现本网路由器
11 时间超过 0 TTL 减到 0 (traceroute 靠它)
11 时间超过 1 分片重组超时
12 参数问题 0 指针指出出错字节
13/14 时间戳请求/回答 0 测时延与时钟偏移
★ 差错报告: 3 / 5 / 11 / 12; 询问: 8-0 / 13-14 / 17-18
=== ② ICMP 差错报文的载荷与长度 ===
部分 长度 作用
内层: 出错 IP 首部 20 B 源/目的 IP、协议、标识 -> 定位是哪个数据报
内层: 出错报文前 8 B 8 B ★ 正好含 TCP/UDP 的源端口 + 目的端口
ICMP 首部 8 B 类型 + 代码 + 校验和
外层 IP 首部 20 B 承载 ICMP 的"信封"
ICMP 报文本身 = 8 + 20 + 8 = 36 B
整个 IP 数据报 = 36 + 20 = 56 B
★ 为什么恰好取 8 B: 传输层首部前 8 B 里就是两个端口 -> 能认出是"哪个进程"
=== ③ 不再发送 ICMP 差错的四种情形 ===
1. 对 ICMP 差错报告报文本身, 不再发差错 (防链式风暴)
2. 对非第一个分片的数据报片, 不发差错 (片里没有端口)
3. 对多播地址的数据报, 不发差错 (接收者可能成千上万)
4. 对特殊地址 (127.0.0.0/8, 0.0.0.0 等), 不发差错
=== ④ NAPT 端口容量账 ===
公网侧可用临时端口 = 65535 - 1024 + 1 = 64512 个
每台并发 254 台总连接数 是否够用 缺口
50 12700 够用 —
100 25400 够用 —
200 50800 够用 —
300 76200 不够 需 2 个公网 IP
500 127000 不够 需 2 个公网 IP
★ 固定单端口每台 120 条约到顶: (254 x 120 = 30480) x 2 才接近 64512
=== ⑤ NAT 四项代价 ===
破坏端到端透明性 公网侧看不到内网地址, 中间设备可改写
外网不能主动连内网 除非配置端口映射 (DNAT)
校验和必须重算 IP 首部 + TCP/UDP 伪首部
载荷嵌地址的协议失效 IPSec / FTP 主动模式 / SIP, 靠 ALG 打补丁
=== ⑥ traceroute 轨迹模拟 (源 S, 目的 T, 共 4 跳) ===
轮次 发出 TTL 丢弃它的路由器 回应的 ICMP
1 1 R1 (10.0.0.1) 类型 11 代码 0 (时间超过)
2 2 R2 (10.0.0.2) 类型 11 代码 0 (时间超过)
3 3 R3 (10.0.0.3) 类型 11 代码 0 (时间超过)
4 4 T (203.0.113.9) 类型 3 代码 3 (端口不可达) 或 类型 0 (回送回答)
每个探测包 x 3 次, 共发 12 个报文
★ Linux 用 UDP 高端口 + 端口不可达判终点; Windows 用 ICMP 回送回答判终点
★ 某一跳不回应则显示 "* * *", 但后续仍继续 —— 星号不代表路径断了
=== ⑦ NAT 与 IPv6: 同一问题的两条路 ===
NAT : 把"地址不够"往后拖, 代价是破坏端到端透明性
IPv6 : 128 位地址, 多到用不完 -> NAT 本应消失 (见 net/34-ipv6.md)
私有块 (net/31-subnet.md): 10.0.0.0/8 + 172.16.0.0/12 + 192.168.0.0/16
私有地址总量 = 17891328 个 (仅占 2^32 的 0.42%)七条结论:
- ★ NAPT 的键是四元组:内网 IP + 内网端口 + 目的 IP + 目的端口——锚点里"两台主机源端口都是 1025"仍能区分,靠的就是公网侧端口不同(40001 / 40002)。
- ★ 端口容量是硬上限:公网侧可用临时端口 64512 个——254 台主机每台 300 条并发就要 76200 个端口,一个公网 IP 不够,必须加 IP。
- NAT 的代价集中在"透明性":外网不能主动连内网、校验和要重算、载荷里的地址改不到——所以 NAT 是权宜之计,不是优雅方案。
- ★ ICMP 首部固定 8 B,分差错报告与询问两大类:差错 3 / 5 / 11 / 12;询问 8-0(ping)、13-14(时间戳)。
- ★ 差错报文的载荷 = 出错 IP 首部(20 B)+ 前 8 字节——这 8 B 正好含两个端口,作用是"让源主机知道是哪个进程的报文出错了"。
- ★ 长度账:ICMP 报文本身 36 B、整个 IP 数据报 56 B——答题要分清问的是哪一个。
traceroute靠 TTL 递增 + ICMP 超时:每一跳"故意失败一次",回包的源地址就是该跳路由器——Linux 用端口不可达判终点、Windows 用回送回答判终点。
考点
考点
1. 必背结论
- ★ NAT 三种形态:静态(一对一固定)/ 动态(一对一从池借)/ NAPT·PAT(多对一,靠端口区分);考试说"NAT 让内网共享一个公网 IP"指的必然是 NAPT。
- ★★ NAPT 表项键 = 四元组:内网 IP + 内网端口 + 目的 IP + 目的端口;出方向改"源 IP + 源端口"、入方向改"目的 IP + 目的端口"。
- ★ 端口容量锚点:公网侧可用临时端口
个;254 台主机 × 300 条并发 = 76200 > 64512,不够;× 100 条 = 25400,够用。 - ★ NAT 四项代价:破坏端到端透明性 / 外网不能主动连内网 / 校验和必须重算 / 载荷嵌地址的协议失效(靠 ALG)。
- ★ NAT 要改的校验和有两处:IP 首部检验和 + TCP/UDP 伪首部检验和。
- ★ ICMP 首部 8 B:类型 1 B + 代码 1 B + 检验和 2 B + 其余 4 B;ICMP 报文封装在 IP 数据报里,协议号 = 1。
- ★ ICMP 两大类:差错报告报文(3 / 5 / 11 / 12)+ 询问报文(8-0 / 13-14 / 17-18)。
- ★★ 类型代码必记:3/0 网络不可达、3/1 主机不可达、3/3 端口不可达、3/4 需要分片但 DF = 1、11/0 TTL 超时、11/1 分片重组超时、8 回送请求、0 回送回答、5 改变路由。
- ★ 已废弃:类型 4 源点抑制(RFC 6633)、类型 15/16 信息请求/回答。
- ★★ 差错报文载荷 = 出错 IP 数据报的首部(20 B)+ 数据部分前 8 字节;取 8 字节的原因是要含源端口与目的端口。
- ★ 长度账:ICMP 差错报文本身 36 B、整个 IP 数据报 56 B。
- ★★ 不发 ICMP 差错的四种情形:① 对 ICMP 差错报文本身;② 对非第一个分片;③ 对多播地址;④ 对特殊地址。
- ★
ping= ICMP 回送请求(8)/ 回送回答(0);traceroute= TTL 递增 + ICMP 超时(11/0);Linux 用 UDP 高端口、Windows 用 ICMP 回送请求。 - ★ ICMP 报文只报告差错、不纠正差错——纠正(重传)是 TCP 的事。
2. 高频陷阱
- 认为"NAT 只改 IP 地址":错。NAPT 还要改端口号——否则同一个公网 IP 上两台内网主机的连接无法区分。
- 把 NAPT 的表项键写成"内网 IP + 内网端口"两元组:错。必须带上"目的 IP + 目的端口"——否则同一台主机访问两个不同服务器会共用一条表项,回包就分不清了。
- 认为"公网端口数量是 65536":要区分。理论上限 65536(0 ~ 65535),但 NAPT 实际可用的是
(0 ~ 1023 是熟知端口)。 - 认为"NAT 后外网仍能主动连内网主机":错。必须配端口映射(DNAT)——这是"为什么家里电脑不能被外面直接访问"的原因。
- 忽略"NAT 要重算 TCP/UDP 伪首部校验和":错。伪首部里含源/目的 IP,改了 IP 就必须重算——这是 NAT 设备比普通路由器累的一个原因。
- 认为"ICMP 是传输层协议":错。ICMP 是网络层协议(与 IP 同层),只是它的报文被封装在 IP 数据报里传输。
- 把 ICMP 首部长度当成 4 B:错。是 8 B——类型 1 + 代码 1 + 检验和 2 + 其余 4。
- 把 ICMP 类型与代码记混:"端口不可达"是类型 3 代码 3,不是类型 3 代码 1(那是主机不可达);"TTL 超时"是类型 11 代码 0。
- 认为"ICMP 差错报文只带上出错报文的首部":错。首部 + 数据部分的前 8 字节——少了这 8 字节就认不出是哪个进程的报文。
- 认为"ICMP 差错报文长度就是 8 + 20 = 28 B":错。还要加前 8 字节 → 36 B;若问整个 IP 数据报再加 20 → 56 B。
- 漏掉"对 ICMP 差错报文本身不再发差错":错,这是四条里的第一条——否则会出现差错报文套差错报文的链式风暴。
- 认为"ICMP 差错会发给多播报文":错。多播的接收者可能有成千上万个,逐个报错等于自我放大。
- 把
traceroute说成"查询路由表":错。它靠"故意让每一跳 TTL 归零"骗出 ICMP 超时报文——本质是探测,不是查询。 - 认为"某一跳显示
* * *就说明路径断了":错。只说明这一跳不回 ICMP,后续跳仍可能正常回应。 - 认为"ICMP 可以纠正差错":错。ICMP 只报告差错——重传与纠正是传输层(TCP)的职责。
3. 解题模板("NAT 与 ICMP 计算题")
① NAPT 翻译题:
(1) 建表项键: 内网IP + 内网端口 + 目的IP + 目的端口
(2) 出方向改写: 源 IP -> 公网 IP, 源端口 -> 分配的公网端口
(3) 入方向改写: 目的 IP -> 内网 IP, 目的端口 -> 原内网端口
★ 同一四元组重复出现时命中已有表项, 不新建
② 端口容量题:
可用端口 = 65535 - 1024 + 1 = 64512
需要的端口数 = 内网主机数 x 每台平均并发连接数
比较两者 -> 不够就加公网 IP, 需要 ceil(总连接数 / 64512) 个
③ ICMP 报文长度题:
ICMP 报文 = 8 (首部) + 20 (出错 IP 首部) + 8 (出错报文前 8 B) = 36 B
整个 IP 数据报 = 36 + 20 = 56 B
★ 题目问"ICMP 报文"就别加外层 20; 问"IP 数据报"才加
④ 类型/代码判断题:
不可达 -> 类型 3 (0 网络 / 1 主机 / 3 端口 / 4 需分片但 DF)
超时 -> 类型 11 (0 TTL / 1 重组超时)
重定向 -> 类型 5; 回送 -> 8 (请求) / 0 (回答)
⑤ traceroute 推理题:
第 n 轮发 TTL = n 的数据报 -> 回 ICMP 11/0 的路由器 = 第 n 跳
到达目标: Linux 收 3/3 (端口不可达); Windows 收 0 (回送回答)4. 与相邻章节的接口
net/30-ip.md(IP 数据报与 IPv4 地址):上一章的"私有地址三块"是本章 NAT 的前提;"首部检验和只校验首部、每经路由器重算"与本章"改地址必须重算校验和"直接衔接;"分片与 NAT 的冲突"用的正是上一章的标识与片偏移字段。net/31-subnet.md(子网划分与 CIDR):本章的192.168.x.x就是上一章算出来的私有子网;上一章的"前缀与最长前缀匹配"决定了"哪些地址需要 NAT、哪些可以直接路由"。net/33-routing.md(路由协议):ICMP 的"改变路由(类型 5)"是路由器纠正主机选路的机制,而路由协议(RIP / OSPF)才是路由器之间交换路由的机制——两者层级不同,别混。net/34-ipv6.md(IPv6):ICMPv6 承担了 IPv4 里"ICMP + ARP + IGMP"三者的职责(地址解析、组播管理、差错报告都归它);IPv6 取消了 IP 首部检验和,所以"NAT 改地址要重算校验和"这条在 IPv6 里只剩传输层那一半。net/41-tcp.md与net/42-handshake.md(TCP):"ICMP 差错报文不纠正差错、只报告"是理解"TCP 必须自己重传"的前提;TCP 的伪首部含 IP 地址,正是"为什么 NAT 改 IP 会连带影响传输层校验和"。net/40-udp.md(UDP):"UDP 打到没监听的端口会收到 ICMP 端口不可达(3/3)"是traceroute判定终点的原理——这也是 UDP 与 ICMP 配合最典型的一处。
小结
- NAT 三种形态:静态(一对一固定)/ 动态(一对一借池)/ NAPT·PAT(多对一靠端口)——考试默认指 NAPT。
- ★ NAPT 表项键 = 内网 IP + 内网端口 + 目的 IP + 目的端口;出方向改源、入方向改目的,靠同一张表。
- ★ 端口容量:可用
个;254 台 × 300 条 = 76200 不够,× 100 条 = 25400 够。 - ★ NAT 四项代价:破坏端到端透明性 / 外网不能主动连内网 / 校验和重算 / 载荷嵌地址的协议失效。
- ICMP 首部 8 B,封装在 IP 里(协议号 1)——它是网络层协议,不是传输层。
- ★ 两大类:差错报告(3 / 5 / 11 / 12)+ 询问(8-0 / 13-14);类型 4 与 15/16 已废弃。
- ★★ 类型代码:3/0 网络不可达、3/1 主机不可达、3/3 端口不可达、3/4 需分片但 DF、11/0 TTL 超时、11/1 重组超时。
- ★★ 差错载荷 = 出错 IP 首部 20 B + 前 8 字节(那 8 字节含两个端口);ICMP 报文 36 B、整个 IP 数据报 56 B。
- ★ 不发差错的四种情形:ICMP 差错本身 / 非第一片 / 多播 / 特殊地址。
ping= 回送请求与回答;traceroute= TTL 递增骗 ICMP 超时——Linux 用端口不可达、Windows 用回送回答判终点。- NAT 拖时间、IPv6 才是终局——但 NAT 破坏的端到端透明性,是这一章真正要记住的代价。
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。