第10章:用户数据报协议(UDP)导论

1. UDP 是什么?

UDP(User Datagram Protocol,用户数据报协议) 是传输层的一个简单协议。
用一句话理解:

UDP 就像寄明信片——你写好内容直接投进邮筒,不需要对方签收确认,也不保证一定送达,但胜在快速简单。
与之对比,TCP 就像快递,需要签收回执、按顺序送达、出错重寄。

2. UDP 的特性清单

2.1 UDP 有什么


特性 说明
保留消息边界 应用层写入多少数据,对方就收到多少,不会被拆分或合并
错误检测 包含校验和(Checksum),可检测传输中的数据损坏
无连接 不需要建立连接,直接发送

2.2 UDP 没有什么


缺失特性 含义
错误纠正 检测到错误只是丢弃,不会重传
排序 包可能乱序到达,UDP 不管
去重 重复的包不会被过滤
流量控制 发送方不管接收方有没有来得及处理
拥塞控制 网络拥堵时不会自动降速,可能加剧拥堵

3. UDP 的封装结构

参考图 10-1,UDP 数据报封装在 IPv4 数据报中:

 <--------------------- IPv4 数据报 --------------------->
 +--------------------+----------+----------------------+
 |    IPv4 Header     |  UDP     |      UDP Data        |
 |     (20 字节)      |  Header  |    (应用数据)       |
 |  (无 IP Options) | (8 字节) |                      |
 +--------------------+----------+----------------------+
 <------ UDP 数据报 ---------------------------------------->
  • IPv4 头部:标准情况下 20 字节(无选项),其中 Protocol 字段值为 17,表示上层是 UDP
  • UDP 头部:固定 8 字节
  • UDP 数据:应用程序写入的内容
    对于 IPv6,封装方式类似,但 UDP 头部跟在 IPv6 头部链(Header Chain)的末尾,Next Header 字段同样使用值 17

3.1 各层大小关系

IPv4 数据报总长 = 20 (IPv4 头) + 8 (UDP 头) + 数据长度 \text{IPv4 数据报总长} = 20\text{(IPv4 头)} + 8\text{(UDP 头)} + \text{数据长度} IPv4 数据报总长=20IPv4 头)+8UDP 头)+数据长度
UDP 数据报长度 = 8 (UDP 头) + 数据长度 \text{UDP 数据报长度} = 8\text{(UDP 头)} + \text{数据长度} UDP 数据报长度=8UDP 头)+数据长度

4. 为什么要用 UDP?

既然 UDP 这么"不可靠",为什么还要用?

4.1 UDP 的优势

使用 UDP 的理由

低开销
无连接建立 / 无状态维护

天然支持广播和组播
TCP 无法做到

应用层自由控制
重传策略 / 时机 / 粒度

实时性要求高
宁可丢包也不要延迟

4.2 典型应用场景


应用 原因
DNS 查询 请求/响应简单,丢了重发更快
视频流 / 语音通话 宁可花屏也不要卡顿,对延迟敏感
游戏状态同步 旧数据即使丢了也无所谓,要最新
DHCP / TFTP 在没有连接的情况下需要广播发现
NTP(时间同步) 简单查询,一来一回

4.3 与 TCP 的对比

特性          UDP              TCP
--------      --------         --------
连接          无               三次握手建立
可靠性        不保证           保证(重传)
顺序          不保证           保证
流量控制      无               有(滑动窗口)
拥塞控制      无               有
消息边界      保留             不保留(字节流)
头部开销      8 字节           最少 20 字节
延迟          低               较高(确认机制)

5. 消息边界的重要性

UDP 保留消息边界,这是与 TCP 最直观的区别:

UDP 发送:
  write("Hello")   → 一个 UDP 数据报,对方 read() 得到 "Hello"
  write("World")   → 一个 UDP 数据报,对方 read() 得到 "World"
TCP 发送:
  write("Hello")
  write("World")   → 可能被合并,对方 read() 得到 "HelloWorld"
                      也可能被拆分,对方需要多次 read()

6. 关于 IP 分片(预告)

当 UDP 数据报的大小超过链路的 MTU(Maximum Transmission Unit,最大传输单元) 时,IP 层需要将其**分片(Fragment)**成多个较小的 IP 数据包分别发送,接收方再重组。
以太网的 MTU 通常为 1500 字节
UDP 数据最大不分片大小 = 1500 − 20 (IPv4 头) − 8 (UDP 头) = 1472  字节 \text{UDP 数据最大不分片大小} = 1500 - 20\text{(IPv4 头)} - 8\text{(UDP 头)} = 1472\text{ 字节} UDP 数据最大不分片大小=150020IPv4 头)8UDP 头)=1472 字节
超过 1472 字节的 UDP 数据就需要 IP 层分片处理(详见后续章节)。

7. 完整 C++ 示例代码

以下代码演示 UDP 数据报头部的结构、计算校验和,以及基本的发送/解析逻辑。

#include <cstdint>
#include <cstring>
#include <iostream>
#include <string>
#include <vector>
#include <arpa/inet.h>   // htons, ntohs, inet_pton
#include <netinet/ip.h>  // struct iphdr(Linux)
// -------------------------------------------------------
// UDP 头部结构(固定 8 字节)
// 字段均以网络字节序(大端)存储
// -------------------------------------------------------
#pragma pack(push, 1)
struct UDPHeader {
    uint16_t src_port;  // 源端口(2字节)
    uint16_t dst_port;  // 目的端口(2字节)
    uint16_t length;    // UDP 总长度 = UDP头 + 数据(2字节)
    uint16_t checksum;  // 校验和(2字节),0 表示不使用校验和
};
#pragma pack(pop)
// -------------------------------------------------------
// UDP 伪头部(Pseudo Header)
// 用于计算校验和时,加在 UDP 头部之前
// 注意:伪头部不会真正发送,只用于校验和计算
// -------------------------------------------------------
#pragma pack(push, 1)
struct UDPPseudoHeaderv4 {
    uint32_t src_ip;    // 源 IPv4 地址
    uint32_t dst_ip;    // 目的 IPv4 地址
    uint8_t  zero;      // 填充为 0
    uint8_t  protocol;  // 协议号,UDP = 17
    uint16_t udp_len;   // UDP 总长度(与 UDPHeader.length 相同)
};
#pragma pack(pop)
// -------------------------------------------------------
// 计算 Internet 校验和(RFC 1071)
// 对输入数据每 2 字节求和,处理进位后取反
// -------------------------------------------------------
uint16_t internet_checksum(const uint8_t* data, size_t len) {
    uint32_t sum = 0;
    // 每次处理 2 字节,以网络字节序累加
    for (size_t i = 0; i + 1 < len; i += 2) {
        uint16_t word;
        memcpy(&word, data + i, 2);
        sum += ntohs(word);
    }
    // 若长度为奇数,处理最后 1 字节(高位补 0)
    if (len % 2 != 0) {
        sum += static_cast<uint16_t>(data[len - 1]) << 8;
    }
    // 将 32 位累加结果的进位折叠到低 16 位
    while (sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    // 取反得到校验和
    return static_cast<uint16_t>(~sum);
}
// -------------------------------------------------------
// 为 UDP 数据报计算校验和
// 需要源/目的 IP 地址(用于构造伪头部)
// -------------------------------------------------------
uint16_t compute_udp_checksum(
    const std::string& src_ip_str,   // 点分十进制源 IP
    const std::string& dst_ip_str,   // 点分十进制目的 IP
    const UDPHeader&   udp_hdr,      // UDP 头部(网络字节序)
    const uint8_t*     payload,      // UDP 数据
    size_t             payload_len   // UDP 数据长度
) {
    // 将 IP 字符串转换为网络字节序的 uint32_t
    uint32_t src_ip, dst_ip;
    inet_pton(AF_INET, src_ip_str.c_str(), &src_ip);
    inet_pton(AF_INET, dst_ip_str.c_str(), &dst_ip);
    // 构造伪头部
    UDPPseudoHeaderv4 pseudo;
    pseudo.src_ip   = src_ip;
    pseudo.dst_ip   = dst_ip;
    pseudo.zero     = 0;
    pseudo.protocol = 17; // UDP 协议号
    pseudo.udp_len  = udp_hdr.length; // 与 UDP 头中的 length 相同
    // 将伪头部 + UDP 头部 + 数据拼接成一个缓冲区
    size_t total = sizeof(pseudo) + sizeof(udp_hdr) + payload_len;
    std::vector<uint8_t> buf(total, 0);
    size_t offset = 0;
    memcpy(buf.data() + offset, &pseudo, sizeof(pseudo));
    offset += sizeof(pseudo);
    memcpy(buf.data() + offset, &udp_hdr, sizeof(udp_hdr));
    offset += sizeof(udp_hdr);
    if (payload && payload_len > 0) {
        memcpy(buf.data() + offset, payload, payload_len);
    }
    // 计算整体校验和
    return internet_checksum(buf.data(), total);
}
// -------------------------------------------------------
// 构造一个 UDP 数据报(头部 + 数据)
// 返回完整的字节序列(不含 IP 头)
// -------------------------------------------------------
std::vector<uint8_t> build_udp_datagram(
    uint16_t           src_port,
    uint16_t           dst_port,
    const std::string& src_ip,
    const std::string& dst_ip,
    const std::string& message
) {
    const uint8_t* payload     = reinterpret_cast<const uint8_t*>(message.c_str());
    size_t         payload_len = message.size();
    // UDP 总长度 = 头部(8字节)+ 数据
    uint16_t udp_length = static_cast<uint16_t>(8 + payload_len);
    // 先填写 UDP 头部(校验和先置 0)
    UDPHeader hdr;
    hdr.src_port = htons(src_port);
    hdr.dst_port = htons(dst_port);
    hdr.length   = htons(udp_length);
    hdr.checksum = 0; // 先置 0,之后填入
    // 计算校验和
    uint16_t csum = compute_udp_checksum(
        src_ip, dst_ip, hdr, payload, payload_len);
    hdr.checksum = htons(csum);
    // 拼接头部和数据
    std::vector<uint8_t> datagram(8 + payload_len);
    memcpy(datagram.data(), &hdr, 8);
    memcpy(datagram.data() + 8, payload, payload_len);
    return datagram;
}
// -------------------------------------------------------
// 解析并打印 UDP 数据报内容
// -------------------------------------------------------
void parse_and_print_udp(const std::vector<uint8_t>& datagram) {
    if (datagram.size() < 8) {
        std::cerr << "[错误] 数据报太短,不是合法的 UDP\n";
        return;
    }
    // 读取 UDP 头部
    UDPHeader hdr;
    memcpy(&hdr, datagram.data(), 8);
    uint16_t src_port  = ntohs(hdr.src_port);
    uint16_t dst_port  = ntohs(hdr.dst_port);
    uint16_t length    = ntohs(hdr.length);
    uint16_t checksum  = ntohs(hdr.checksum);
    size_t   data_len  = length - 8; // 数据部分长度
    std::cout << "=== UDP 数据报解析 ===\n";
    std::cout << "  源端口      : " << src_port << "\n";
    std::cout << "  目的端口    : " << dst_port << "\n";
    std::cout << "  UDP 总长度  : " << length << " 字节"
              << "(头部 8 + 数据 " << data_len << ")\n";
    std::cout << "  校验和      : 0x" << std::hex << checksum
              << std::dec << "\n";
    // 打印数据内容
    if (datagram.size() >= 8 + data_len) {
        std::string payload(
            reinterpret_cast<const char*>(datagram.data() + 8), data_len);
        std::cout << "  数据内容    : \"" << payload << "\"\n";
    }
}
// -------------------------------------------------------
// 演示 IP 分片阈值计算
// -------------------------------------------------------
void show_fragmentation_threshold() {
    const int IPV4_HEADER   = 20; // 标准 IPv4 头(无选项)
    const int UDP_HEADER    = 8;  // UDP 头固定大小
    const int ETHERNET_MTU  = 1500; // 以太网 MTU
    int max_udp_data = ETHERNET_MTU - IPV4_HEADER - UDP_HEADER;
    std::cout << "\n=== IP 分片阈值 ===\n";
    std::cout << "  以太网 MTU             : " << ETHERNET_MTU << " 字节\n";
    std::cout << "  IPv4 头部              : " << IPV4_HEADER << " 字节\n";
    std::cout << "  UDP 头部               : " << UDP_HEADER << " 字节\n";
    std::cout << "  UDP 数据最大不分片大小 : "
              << max_udp_data << " 字节\n";
    std::cout << "  超过此大小 IP 层将分片!\n";
}
// -------------------------------------------------------
// 主函数
// -------------------------------------------------------
int main() {
    std::string src_ip  = "192.168.1.10";
    std::string dst_ip  = "192.168.1.20";
    uint16_t    src_port = 54321;
    uint16_t    dst_port = 53;       // DNS 默认端口
    std::string message  = "Hello, UDP!";
    std::cout << "构造 UDP 数据报:\n";
    std::cout << "  " << src_ip << ":" << src_port
              << " → " << dst_ip << ":" << dst_port << "\n";
    std::cout << "  数据:\"" << message << "\"\n\n";
    // 构造 UDP 数据报
    auto datagram = build_udp_datagram(
        src_port, dst_port, src_ip, dst_ip, message);
    // 打印原始字节
    std::cout << "原始字节(十六进制):\n  ";
    for (size_t i = 0; i < datagram.size(); ++i) {
        printf("%02X ", datagram[i]);
        if ((i + 1) % 16 == 0) std::cout << "\n  ";
    }
    std::cout << "\n\n";
    // 解析并打印
    parse_and_print_udp(datagram);
    // 显示分片阈值
    show_fragmentation_threshold();
    return 0;
}

https://godbolt.org/z/3n7Gn1aYK
编译运行:

g++ -std=c++17 -o udp_demo udp_demo.cpp && ./udp_demo

预期输出:

构造 UDP 数据报:
  192.168.1.10:54321 → 192.168.1.20:53
  数据:"Hello, UDP!"
原始字节(十六进制):
  D4 31 00 35 00 13 XX XX 48 65 6C 6C 6F 2C 20 55
  44 50 21
=== UDP 数据报解析 ===
  源端口      : 54321
  目的端口    : 53
  UDP 总长度  : 19 字节(头部 8 + 数据 11)
  校验和      : 0xXXXX
  数据内容    : "Hello, UDP!"
=== IP 分片阈值 ===
  以太网 MTU             : 1500 字节
  IPv4 头部              : 20 字节
  UDP 头部               : 8 字节
  UDP 数据最大不分片大小 : 1472 字节
  超过此大小 IP 层将分片!

8. 核心知识点总结

UDP 的定位:
  传输层最简单的协议
  像"明信片"——投出去不管有没有收到
封装结构(IPv4):
  [IPv4 头 20B] + [UDP 头 8B] + [数据]
  Protocol 字段 = 17
UDP 有:  消息边界保留、错误检测(校验和)、无连接
UDP 没有:可靠传输、顺序、去重、流控、拥塞控制
最大不分片数据 = MTU(1500) - IPv4头(20) - UDP头(8) = 1472 字节
超过此大小 → IP 分片(后续章节详述)
适合 UDP 的场景:
  DNS、视频/音频流、游戏、广播/组播、实时控制

UDP 头部详解(第10章 10.2节)

1. UDP 数据报结构总览

UDP(User Datagram Protocol,用户数据报协议)的头部固定为 8 字节,结构极其简单。整个 UDP 数据报由两部分组成:

+---------------------------+---------------------------+
|    Source Port Number     |  Destination Port Number  |
|         (2 字节)           |         (2 字节)           |
+---------------------------+---------------------------+
|         Length            |         Checksum          |
|         (2 字节)           |         (2 字节)           |
+-----------------------------------------------------------+
|                                                           |
|              Payload Data(有效载荷数据,可变长度)          |
|                                                           |
+-----------------------------------------------------------+
        <-- 位编号: 0        15 16        31 -->
  • UDP 头部:固定 8 字节,包含 4 个字段,每个字段 2 字节(16 位)
  • 有效载荷:长度可变,可以为 0 字节

2. 各字段详细说明

2.1 源端口号(Source Port Number)

  • 大小:2 字节(16 位)
  • 范围 0 ∼ 65535 0 \sim 65535 065535(正整数)
  • 作用:标识发送方进程,相当于"寄件人信箱编号"
  • 特殊规则源端口号是可选的,如果发送方不需要对方回复,可以将其设置为 0 0 0

类比理解:想象你寄一封信,收件地址必须写清楚,但寄件地址可以不写(如果你不需要回信)。

2.2 目的端口号(Destination Port Number)

  • 大小:2 字节(16 位)
  • 范围 0 ∼ 65535 0 \sim 65535 065535
  • 作用:标识接收方进程,是**解复用(demultiplex)**的关键字段
什么是解复用?

网络中有很多数据包同时到达一台主机,操作系统需要判断"这个数据包该交给哪个应用程序处理",这个过程就叫解复用

                    IP层收到数据包
                         |
              读取 Protocol 字段(IPv4)
              或 Next Header 字段(IPv6)
                         |
              +----------+---------+
              |                    |
           是 TCP                是 UDP
              |                    |
       TCP端口号解复用         UDP端口号解复用
              |                    |
          交给对应               交给对应
          TCP应用                UDP应用

解复用流程如下:

TCP

UDP

IP层收到数据包

读取协议字段

TCP端口号解复用

UDP端口号解复用

交给对应TCP进程

交给对应UDP进程

2.3 端口号的独立性

关键结论:TCP 的端口号和 UDP 的端口号是相互独立的,互不干扰。
这意味着:

  • TCP 的 80 端口和 UDP 的 80 端口是两个完全不同的端口
  • 两个服务可以同时使用相同的端口号和 IP 地址,只要它们使用不同的传输协议
    完整的套接字标识 = { 协议 , IP地址 , 端口号 } \text{完整的套接字标识} = \{\text{协议}, \text{IP地址}, \text{端口号}\} 完整的套接字标识={协议,IP地址,端口号}

注意:虽然 TCP 和 UDP 的端口号相互独立,但在实践中,一个"知名服务"如果同时支持 TCP 和 UDP,通常会分配相同的端口号(纯粹是为了方便记忆,协议本身并不要求这样做)。
例如 DNS 服务同时使用 TCP/53 和 UDP/53。

2.4 长度字段(Length)

  • 大小:2 字节(16 位)
  • 含义:UDP 头部 + UDP 数据的总字节数
    Length = UDP 头部长度 + UDP 数据长度 = 8 + 数据长度(字节) \text{Length} = \text{UDP 头部长度} + \text{UDP 数据长度} = 8 + \text{数据长度(字节)} Length=UDP 头部长度+UDP 数据长度=8+数据长度(字节)
  • 最小值 8 8 8(即只有头部,数据为 0 字节)
  • 特殊情况:在 IPv6 超大数据报(Jumbogram)中,最小值规则有所不同
    Length 字段是冗余的

场景 UDP数据长度计算方式
UDP over IPv4 UDP数据报长度 = IPv4总长度 − IPv4头部长度 \text{UDP数据报长度} = \text{IPv4总长度} - \text{IPv4头部长度} UDP数据报长度=IPv4总长度IPv4头部长度
UDP over IPv6 UDP数据报长度 = IPv6载荷长度 − 扩展头部长度之和 \text{UDP数据报长度} = \text{IPv6载荷长度} - \text{扩展头部长度之和} UDP数据报长度=IPv6载荷长度扩展头部长度之和

为什么说它"冗余"?因为从 IP 层的信息就能算出 UDP 数据报的长度,Length 字段只是重复了这个信息。尽管如此,Length 字段的值应当与 IP 层计算出的长度一致

2.5 校验和字段(Checksum)

  • 大小:2 字节(16 位)
  • 作用:端到端的错误检测,确保数据在传输过程中没有损坏
    重要特性:校验和是**端到端(end-to-end)**的,计算范围包括:
  • UDP 伪头部(Pseudo-header)—— 来自 IP 头部的字段:
    • 源 IP 地址
    • 目的 IP 地址
    • 协议类型
    • UDP 长度
  • UDP 头部本身
  • UDP 数据
    UDP 伪头部示意:
+-------------------------------+
|       源 IP 地址(4字节)       |  <-- 来自 IP 头部
+-------------------------------+
|      目的 IP 地址(4字节)      |  <-- 来自 IP 头部
+-------------------------------+
|  全0  |  协议(17) | UDP长度   |  <-- 构造字段
+-------------------------------+
|         UDP 头部 + 数据        |
+-------------------------------+

为什么要包含 IP 地址来计算校验和?
因为校验和覆盖了 IP 地址,所以一旦 NAT(网络地址转换)修改了 IP 头部中的源地址或目的地址,必须同步更新 UDP 校验和,否则接收方校验会失败。
Checksum = ∼ ( 伪头部 + UDP头部 + UDP数据 ) 16位反码求和 \text{Checksum} = \sim\left(\text{伪头部} + \text{UDP头部} + \text{UDP数据}\right)_{\text{16位反码求和}} Checksum=∼(伪头部+UDP头部+UDP数据)16位反码求和
其中 ∼ \sim 表示按位取反(one’s complement)。

3. 完整的 C++ 代码示例

以下代码演示如何手动构造和解析 UDP 头部,并计算校验和:

#include <iostream>
#include <cstdint>
#include <cstring>
#include <arpa/inet.h>  // htons, ntohs, inet_addr
// ============================================================
// UDP 头部结构体(严格按照协议格式,共8字节)
// ============================================================
struct UDPHeader {
    uint16_t src_port;   // 源端口号(2字节)
    uint16_t dst_port;   // 目的端口号(2字节)
    uint16_t length;     // UDP头部+数据总长度(2字节)
    uint16_t checksum;   // 校验和(2字节)
};
// ============================================================
// UDP 伪头部结构体(用于计算校验和,不实际传输)
// ============================================================
struct PseudoHeader {
    uint32_t src_ip;     // 源IP地址(4字节)
    uint32_t dst_ip;     // 目的IP地址(4字节)
    uint8_t  zero;       // 固定为0(填充字节)
    uint8_t  protocol;   // 协议号:UDP = 17
    uint16_t udp_length; // UDP长度(与UDP头部中的length相同)
};
// ============================================================
// 计算16位反码校验和
// 参数: data  - 待计算的数据指针
//       len   - 数据字节数
// 返回: 16位校验和(网络字节序)
// ============================================================
uint16_t calculateChecksum(const uint8_t* data, size_t len) {
    uint32_t sum = 0;
    // 每次取2字节(16位)累加
    while (len > 1) {
        // 将两个字节组合成16位整数并累加
        sum += (static_cast<uint16_t>(data[0]) << 8) | data[1];
        data += 2;
        len  -= 2;
    }
    // 如果数据长度为奇数,最后一个字节单独处理(高位补0)
    if (len == 1) {
        sum += static_cast<uint16_t>(*data) << 8;
    }
    // 将32位累加结果折叠成16位:把高16位加到低16位
    while (sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    // 取反得到最终校验和
    return static_cast<uint16_t>(~sum);
}
// ============================================================
// 计算 UDP 校验和
// 参数: pseudo  - 伪头部
//       udphdr  - UDP头部(checksum字段需预先置0)
//       payload - 载荷数据指针
//       pay_len - 载荷数据字节数
// 返回: 16位校验和
// ============================================================
uint16_t udpChecksum(const PseudoHeader& pseudo,
                     const UDPHeader&   udphdr,
                     const uint8_t*     payload,
                     size_t             pay_len)
{
    // 将伪头部、UDP头部、载荷拼接到一个缓冲区中
    size_t total = sizeof(PseudoHeader) + sizeof(UDPHeader) + pay_len;
    uint8_t* buf = new uint8_t[total];
    size_t offset = 0;
    // 拷贝伪头部
    memcpy(buf + offset, &pseudo, sizeof(PseudoHeader));
    offset += sizeof(PseudoHeader);
    // 拷贝UDP头部
    memcpy(buf + offset, &udphdr, sizeof(UDPHeader));
    offset += sizeof(UDPHeader);
    // 拷贝载荷
    memcpy(buf + offset, payload, pay_len);
    // 计算校验和
    uint16_t cs = calculateChecksum(buf, total);
    delete[] buf;
    return cs;
}
// ============================================================
// 构造并打印一个 UDP 数据报(演示用)
// ============================================================
void buildAndShowUDPDatagram(
    const char*    src_ip_str,   // 源IP字符串,如 "192.168.1.1"
    const char*    dst_ip_str,   // 目的IP字符串,如 "192.168.1.2"
    uint16_t       src_port,     // 源端口
    uint16_t       dst_port,     // 目的端口
    const uint8_t* payload,      // 载荷数据
    size_t         pay_len       // 载荷长度(字节)
) {
    // ---------- 1. 构造 UDP 头部 ----------
    UDPHeader udphdr;
    // htons: host to network short,将主机字节序转为网络字节序(大端)
    udphdr.src_port = htons(src_port);
    udphdr.dst_port = htons(dst_port);
    // UDP总长度 = 8字节头部 + 载荷长度
    udphdr.length   = htons(static_cast<uint16_t>(8 + pay_len));
    udphdr.checksum = 0;  // 计算前先置0
    // ---------- 2. 构造伪头部 ----------
    PseudoHeader pseudo;
    pseudo.src_ip     = inet_addr(src_ip_str);  // 字符串IP转32位整数(网络字节序)
    pseudo.dst_ip     = inet_addr(dst_ip_str);
    pseudo.zero       = 0;
    pseudo.protocol   = 17;                     // UDP 协议号固定为 17
    pseudo.udp_length = udphdr.length;          // 与UDP头部中的length相同
    // ---------- 3. 计算校验和 ----------
    udphdr.checksum = udpChecksum(pseudo, udphdr, payload, pay_len);
    // ---------- 4. 打印结果 ----------
    std::cout << "========== UDP 数据报构造结果 ==========\n";
    std::cout << "源IP:        " << src_ip_str << "\n";
    std::cout << "目的IP:      " << dst_ip_str << "\n";
    std::cout << "源端口:      " << src_port   << "\n";
    std::cout << "目的端口:    " << dst_port   << "\n";
    std::cout << "UDP总长度:   " << (8 + pay_len) << " 字节\n";
    // ntohs: network to host short,将网络字节序转为主机字节序(用于显示)
    std::cout << "校验和:      0x" << std::hex << ntohs(udphdr.checksum) << std::dec << "\n";
    std::cout << "载荷内容:    ";
    for (size_t i = 0; i < pay_len; ++i) {
        std::cout << static_cast<char>(payload[i]);
    }
    std::cout << "\n";
    std::cout << "========================================\n";
}
// ============================================================
// 主函数:演示UDP数据报的构造
// ============================================================
int main() {
    // 模拟一段 DNS 查询载荷(简化示意,非真实DNS格式)
    const char* msg = "Hello UDP";
    const uint8_t* payload = reinterpret_cast<const uint8_t*>(msg);
    size_t pay_len = strlen(msg);
    // 构造并显示一个 UDP 数据报
    // 源IP: 192.168.1.10, 目的IP: 192.168.1.1
    // 源端口: 12345(随机临时端口), 目的端口: 53(DNS服务)
    buildAndShowUDPDatagram(
        "192.168.1.10",
        "192.168.1.1",
        12345,
        53,
        payload,
        pay_len
    );
    return 0;
}

https://godbolt.org/z/n6obadT3e
编译命令(Linux/macOS):

g++ -std=c++17 -o udp_demo udp_demo.cpp && ./udp_demo

4. UDP 数据报传输过程演示

发送方(192.168.1.10:12345)
        |
        | 构造 UDP 数据报
        | [源端口=12345][目的端口=53][长度=17][校验和=0xXXXX][Hello UDP]
        |
        v
     路由器/NAT
        |
        | 若 NAT 修改了 IP 地址
        | --> 必须同步修改 UDP 校验和!
        |
        v
接收方(192.168.1.1:53)
        |
        | 1. IP层读取 Protocol=17,识别为UDP
        | 2. 将数据报交给 UDP 模块
        | 3. UDP 读取目的端口=53,找到对应的 DNS 进程
        | 4. 验证校验和
        | 5. 将载荷 "Hello UDP" 交给 DNS 应用

5. 知识要点总结


字段 大小 是否必须 核心作用
源端口号 2字节 可选(可为0) 标识发送方进程
目的端口号 2字节 必须 解复用,找到目标进程
长度 2字节 必须 指明UDP报文总长度(冗余但需一致)
校验和 2字节 实践中通常必须 端到端错误检测(含IP伪头部)

核心公式回顾:
UDP 总长度:
UDP Length = 8 + 数据字节数 \text{UDP Length} = 8 + \text{数据字节数} UDP Length=8+数据字节数
IPv4 中 UDP 数据报长度验证:
UDP数据报长度 = IPv4总长度 − IPv4头部长度 \text{UDP数据报长度} = \text{IPv4总长度} - \text{IPv4头部长度} UDP数据报长度=IPv4总长度IPv4头部长度
IPv6 中 UDP 数据报长度验证:
UDP数据报长度 = IPv6载荷长度 − ∑ 扩展头部长度 \text{UDP数据报长度} = \text{IPv6载荷长度} - \sum\text{扩展头部长度} UDP数据报长度=IPv6载荷长度扩展头部长度
校验和计算(16位反码求和再取反):
Checksum = ∼ ( ∑ i W i )   m o d   2 16 \text{Checksum} = \sim\left(\sum_{i} W_i\right)_{\bmod 2^{16}} Checksum=∼(iWi)mod216
其中 W i W_i Wi 是每个16位字, ∼ \sim 表示按位取反。

UDP 校验和详解(第10章 10.3节)

1. 什么是 UDP 校验和?

UDP 校验和是第一个我们遇到的端到端传输层校验和(ICMP 虽然有校验和,但它不是真正的传输层协议)。
它的覆盖范围是:

校验和覆盖范围 = 伪头部(Pseudo-Header)+ UDP头部 + UDP数据

端到端的含义是:

  • 最初的发送方计算
  • 最终的接收方验证
  • 中途路由器不重新计算(除非经过 NAT)

2. 与 IPv4 头部校验和的区别


比较项 IPv4 头部校验和 UDP 校验和
覆盖范围 仅 IPv4 头部 UDP头部 + 数据 + 伪头部
是否每跳重算 是(因为 TTL 每跳减1) 否(端到端)
是否必须 必须 IPv4中可选(强烈建议),IPv6中必须

为什么 IPv6 中 UDP 校验和是强制的?
因为 IPv6 头部本身没有头部校验和字段,所以传输层必须自己负责错误检测,不能依赖网络层。

3. 校验和计算的两个特殊细节

3.1 奇数字节的填充(Padding)

UDP 数据的长度可能是奇数字节,但校验和算法每次处理 16 位(2字节)
解决方案:在奇数长度的数据末尾虚拟追加一个值为 0 0 0 的填充字节,仅用于计算,不实际发送

若数据为奇数字节:
[数据字节 0][数据字节 1] ... [数据字节 N][虚拟填充 0x00]
                                          ^^^^^^^^^^^^^^^^
                                          仅用于计算,不传输

3.2 伪头部(Pseudo-Header)

UDP 校验和计算引入了一个"虚拟"的伪头部,它来自 IP 层的字段:
IPv4 伪头部(12 字节)结构:

 0               15 16              31
+----------------------------------+
|         源 IP 地址(4字节)        |  <-- 来自 IPv4 头部
+----------------------------------+
|        目的 IP 地址(4字节)       |  <-- 来自 IPv4 头部
+--------+---------+---------------+
|  Zero  | Proto   |  UDP Length   |  <-- Zero=0, Proto=17, Length来自UDP头部
| (1字节) | (1字节) |   (2字节)     |
+--------+---------+---------------+

IPv6 伪头部(40 字节)结构类似,但地址更长(各16字节)。
伪头部的目的:让 UDP 层能够验证数据报是否到达了正确的目的地,防止:

  • IP 层接受了一个地址错误的数据报
  • IP 层把一个属于其他传输协议的数据报错误地交给了 UDP

这是一种"层违规(layering violation)":UDP(传输层)直接处理了 IP(网络层)的字段。但这被认为是可以接受的小代价,用于换取更可靠的错误检测。

4. 校验和的完整计算过程

将以下内容拼接后,对所有 16 位字做反码求和,再取反:
Checksum = ∼ ( ∑ i W i )   m o d   ( 2 16 − 1 ) \text{Checksum} = \sim \left( \sum_{i} W_i \right)_{\bmod (2^{16}-1)} Checksum=∼(iWi)mod(2161)
其中 W i W_i Wi 是每个 16 位字, ∼ \sim 表示按位取反(one’s complement),求和采用反码加法(进位回卷到最低位)。
拼接顺序如下:

+=======================+
|   伪头部(12字节)     |  <-- 不传输,仅用于计算
+=======================+
|   UDP 头部(8字节)    |  checksum字段先置0
+=======================+
|   UDP 数据(N字节)    |
+=======================+
|  填充字节(若N为奇数) |  <-- 0x00,不传输
+=======================+

注意:UDP Length 字段在校验和中出现了两次——一次在伪头部,一次在 UDP 头部。

5. 校验和特殊值的含义


校验和值 含义
0xFFFF \text{0xFFFF} 0xFFFF 计算结果为全1,正常存储(当计算结果为0x0000时,用0xFFFF代替)
0x0000 \text{0x0000} 0x0000 发送方未计算校验和(表示"我没算,接收方无需验证")

为什么计算结果为 0 0 0 时要存成 0xFFFF \text{0xFFFF} 0xFFFF
在反码算术中, 0x0000 \text{0x0000} 0x0000 0xFFFF \text{0xFFFF} 0xFFFF 是等价的(互为反码),因此用 0xFFFF \text{0xFFFF} 0xFFFF 表示"校验和恰好计算为0",而保留 0x0000 \text{0x0000} 0x0000 专门表示"未计算校验和"。
∼ 0 x 0000 = 0 x F F F F (反码取反) \sim 0x0000 = 0xFFFF \quad \text{(反码取反)} 0x0000=0xFFFF(反码取反)
0 x 0000 ⇒ 存储为    0 x F F F F 0x0000 \Rightarrow \text{存储为} \; 0xFFFF 0x0000存储为0xFFFF

6. 接收方的处理逻辑

收到 UDP 数据报

校验和字段 == 0?

发送方未计算校验和
直接接受数据报

接收方重新计算校验和

计算结果 == 0xFFFF?

校验和正确
接受数据报

校验和错误
静默丢弃数据报
不发送错误消息

“静默丢弃”:UDP 检测到校验和错误后,直接丢弃数据报,不通知发送方,只更新内部统计计数器。这与 TCP 不同,TCP 会通过重传机制恢复数据。

7. 为什么校验和不能被关闭?

历史教训:1980年代,部分厂商(如 Sun)为了加速 NFS(网络文件系统,基于 UDP)而默认关闭 UDP 校验和
这引发了严重问题:

问题1:路由器软硬件 Bug
  路由器在转发数据报时可能悄悄修改其中的比特
  --> 没有校验和 --> 接收方无法检测 --> 数据静默损坏
问题2:数据链路层保护不够
  部分老旧协议(如 SLIP,串行线路IP)没有数据链路层校验
  --> 单靠链路层 CRC 不足以覆盖所有情况
问题3:NAT 的存在
  NAT 修改 IP 地址后必须同步更新 UDP 校验和
  --> 若校验和被关闭,NAT 的改动就无法被验证

RFC 1122 明确规定

  • UDP 校验和必须默认开启
  • 若收到的数据报校验和非零,接收方必须验证

8. NAT 与 UDP 校验和的关系

由于伪头部包含了 IP 层的源/目的地址,当数据报经过 NAT 时:

原始数据报:
  源IP = 192.168.1.10  目的IP = 8.8.8.8
  校验和 = 0xABCD(基于上述IP计算)
经过 NAT 后:
  源IP = 203.0.113.5(公网IP)  目的IP = 8.8.8.8
  校验和 = ???(必须重新计算!)

NAT 必须同时修改:

  1. IP 头部的地址字段
  2. IP 头部的校验和
  3. UDP 伪头部隐含的地址(重算 UDP 校验和)
  4. 若端口号也变了,还需更新端口相关校验
    这意味着 NAT 在同一时刻修改了多个协议层的内容,是一种"层违规"。但这是由伪头部机制本身决定的,NAT 别无选择。

9. 部分校验和(UDP-Lite)

对于多媒体应用(如视频流、语音通话),少量数据损坏通常可以接受(人耳/人眼有一定容错性),但为了保护头部信息(端口号等),提出了部分校验和的概念:

  • 只对载荷的指定部分(由应用层决定)计算校验和
  • 头部仍然被完整保护
  • UDP-Lite 协议(Section 10.6)中实现

10. 完整 C++ 代码示例

#include <iostream>
#include <cstdint>
#include <cstring>
#include <vector>
#include <iomanip>
#include <arpa/inet.h>   // htons, ntohs, inet_addr
// ============================================================
// UDP 头部结构(8字节)
// ============================================================
struct UDPHeader {
    uint16_t src_port;   // 源端口号
    uint16_t dst_port;   // 目的端口号
    uint16_t length;     // UDP总长度(头部+数据)
    uint16_t checksum;   // 校验和(计算时先置0)
};
// ============================================================
// IPv4 伪头部结构(12字节,仅用于校验和计算,不传输)
// ============================================================
struct PseudoHeaderV4 {
    uint32_t src_ip;     // 源IPv4地址(4字节)
    uint32_t dst_ip;     // 目的IPv4地址(4字节)
    uint8_t  zero;       // 固定为0(填充)
    uint8_t  protocol;   // 协议号:UDP = 17(0x11)
    uint16_t udp_length; // UDP长度(与UDP头部中length字段相同)
};
// ============================================================
// 16位反码求和(Internet Checksum 核心算法)
// 参数: data - 数据指针
//       len  - 数据字节数(可以是奇数,内部自动填充)
// 返回: 最终校验和(已取反,即存入头部的值)
// ============================================================
uint16_t internetChecksum(const uint8_t* data, size_t len) {
    uint32_t sum = 0;
    // 每次取 2 字节累加(大端序:高字节在前)
    while (len > 1) {
        uint16_t word = (static_cast<uint16_t>(data[0]) << 8)
                      |  static_cast<uint16_t>(data[1]);
        sum += word;
        data += 2;
        len  -= 2;
    }
    // 若还剩 1 个字节(奇数长度),虚拟在末尾补 0x00 后处理
    // 相当于:[最后一字节, 0x00] 组成 16 位字
    if (len == 1) {
        sum += static_cast<uint16_t>(data[0]) << 8;
        // 低8位为虚拟的 0x00,不需要加
    }
    // 反码折叠:将 32 位结果折叠成 16 位
    // 原理:超出 16 位的进位(高 16 位)加回低 16 位(反码加法)
    while (sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    // 取反得到校验和
    // 特殊处理:若结果为 0x0000,按协议规定存储为 0xFFFF
    uint16_t result = static_cast<uint16_t>(~sum);
    if (result == 0x0000) {
        result = 0xFFFF;  // 0x0000 保留给"未计算校验和",真正为0时用0xFFFF表示
    }
    return result;
}
// ============================================================
// 计算 UDP/IPv4 校验和
// 参数: src_ip  - 源IP(网络字节序,来自inet_addr等)
//       dst_ip  - 目的IP(网络字节序)
//       udphdr  - UDP头部(checksum字段应预先置0)
//       payload - UDP数据
//       pay_len - UDP数据字节数
// 返回: 校验和(主机字节序,存入udphdr.checksum前无需再转换)
// ============================================================
uint16_t computeUDPChecksum(uint32_t          src_ip,
                             uint32_t          dst_ip,
                             const UDPHeader&  udphdr,
                             const uint8_t*    payload,
                             size_t            pay_len)
{
    // 构造伪头部
    PseudoHeaderV4 pseudo;
    pseudo.src_ip     = src_ip;
    pseudo.dst_ip     = dst_ip;
    pseudo.zero       = 0;
    pseudo.protocol   = 17;              // UDP协议号
    pseudo.udp_length = udphdr.length;   // 与UDP头部length相同
    // 拼接:伪头部 + UDP头部 + UDP数据(+ 可能的填充字节)
    // 注意:填充字节由 internetChecksum 内部自动处理
    size_t buf_size = sizeof(PseudoHeaderV4) + sizeof(UDPHeader) + pay_len;
    std::vector<uint8_t> buf(buf_size, 0);  // 初始化为0,天然处理了奇数填充
    size_t offset = 0;
    // 拷贝伪头部
    memcpy(buf.data() + offset, &pseudo, sizeof(PseudoHeaderV4));
    offset += sizeof(PseudoHeaderV4);
    // 拷贝UDP头部
    memcpy(buf.data() + offset, &udphdr, sizeof(UDPHeader));
    offset += sizeof(UDPHeader);
    // 拷贝UDP数据
    memcpy(buf.data() + offset, payload, pay_len);
    // 计算校验和(buf_size可能为奇数,internetChecksum内部处理填充)
    return internetChecksum(buf.data(), buf_size);
}
// ============================================================
// 接收方验证校验和
// 原理:将整个(伪头部+UDP头部+数据)再做一次反码求和
//       若结果为 0xFFFF,则校验和正确
// 返回: true = 校验和正确,false = 校验和错误
// ============================================================
bool verifyUDPChecksum(uint32_t         src_ip,
                        uint32_t         dst_ip,
                        const UDPHeader& udphdr,
                        const uint8_t*   payload,
                        size_t           pay_len)
{
    // 校验和为0表示发送方未计算,直接接受
    if (udphdr.checksum == 0x0000) {
        std::cout << "[验证] 发送方未计算校验和,直接接受\n";
        return true;
    }
    // 构造用于验证的缓冲区(与计算时完全相同)
    PseudoHeaderV4 pseudo;
    pseudo.src_ip     = src_ip;
    pseudo.dst_ip     = dst_ip;
    pseudo.zero       = 0;
    pseudo.protocol   = 17;
    pseudo.udp_length = udphdr.length;
    size_t buf_size = sizeof(PseudoHeaderV4) + sizeof(UDPHeader) + pay_len;
    std::vector<uint8_t> buf(buf_size, 0);
    size_t offset = 0;
    memcpy(buf.data() + offset, &pseudo, sizeof(PseudoHeaderV4));
    offset += sizeof(PseudoHeaderV4);
    memcpy(buf.data() + offset, &udphdr, sizeof(UDPHeader));
    offset += sizeof(UDPHeader);
    memcpy(buf.data() + offset, payload, pay_len);
    // 接收方对整个数据(含已填入的checksum)做反码求和
    // 若无错误,结果应为 0xFFFF(所有位为1)
    uint32_t sum = 0;
    const uint8_t* data = buf.data();
    size_t len = buf_size;
    while (len > 1) {
        uint16_t word = (static_cast<uint16_t>(data[0]) << 8)
                      |  static_cast<uint16_t>(data[1]);
        sum += word;
        data += 2;
        len  -= 2;
    }
    if (len == 1) {
        sum += static_cast<uint16_t>(data[0]) << 8;
    }
    while (sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    // 结果应为 0xFFFF 表示无错误
    return (static_cast<uint16_t>(sum) == 0xFFFF);
}
// ============================================================
// 主函数:演示发送方计算和接收方验证 UDP 校验和
// ============================================================
int main() {
    // ---------- 发送方 ----------
    const char* src_ip_str = "192.168.1.10";
    const char* dst_ip_str = "8.8.8.8";
    uint16_t    src_port   = 54321;
    uint16_t    dst_port   = 53;      // DNS 端口
    // 模拟一段奇数长度的 UDP 数据(5字节,触发填充逻辑)
    const uint8_t payload[] = {'H', 'e', 'l', 'l', 'o'};  // 5字节(奇数)
    size_t pay_len = sizeof(payload);
    // 构造 UDP 头部
    UDPHeader udphdr;
    udphdr.src_port = htons(src_port);
    udphdr.dst_port = htons(dst_port);
    udphdr.length   = htons(static_cast<uint16_t>(8 + pay_len));
    udphdr.checksum = 0;  // 计算前必须置0
    uint32_t src_ip = inet_addr(src_ip_str);
    uint32_t dst_ip = inet_addr(dst_ip_str);
    // 计算校验和
    uint16_t cs = computeUDPChecksum(src_ip, dst_ip, udphdr, payload, pay_len);
    udphdr.checksum = htons(cs);  // 转为网络字节序存入头部
    // 打印发送方结果
    std::cout << "========== 发送方构造 UDP 数据报 ==========\n";
    std::cout << "源IP:        " << src_ip_str << "\n";
    std::cout << "目的IP:      " << dst_ip_str << "\n";
    std::cout << "源端口:      " << src_port << "\n";
    std::cout << "目的端口:    " << dst_port << "\n";
    std::cout << "UDP总长度:   " << (8 + pay_len) << " 字节\n";
    std::cout << "数据:        Hello(5字节,奇数,自动填充0x00)\n";
    std::cout << "计算校验和:  0x" << std::hex << std::uppercase
              << std::setw(4) << std::setfill('0') << cs << std::dec << "\n\n";
    // ---------- 接收方(无损传输) ----------
    std::cout << "========== 接收方验证(无误码) ==========\n";
    bool ok = verifyUDPChecksum(src_ip, dst_ip, udphdr, payload, pay_len);
    std::cout << "[验证结果] " << (ok ? "校验和正确,接受数据报" : "校验和错误,静默丢弃!") << "\n\n";
    // ---------- 模拟传输中数据被篡改 ----------
    uint8_t corrupted_payload[] = {'H', 'e', 'l', 'l', 'X'};  // 最后一字节被改
    std::cout << "========== 接收方验证(数据被篡改: Hello -> HellX) ==========\n";
    bool ok2 = verifyUDPChecksum(src_ip, dst_ip, udphdr, corrupted_payload, pay_len);
    std::cout << "[验证结果] " << (ok2 ? "校验和正确,接受数据报" : "校验和错误,静默丢弃!") << "\n";
    return 0;
}

https://godbolt.org/z/ezhzjzY5M
编译与运行:

g++ -std=c++17 -o udp_checksum udp_checksum.cpp && ./udp_checksum

预期输出:

========== 发送方构造 UDP 数据报 ==========
源IP:        192.168.1.10
目的IP:      8.8.8.8
源端口:      54321
目的端口:    53
UDP总长度:   13 字节
数据:        Hello(5字节,奇数,自动填充0x00)
计算校验和:  0x????
========== 接收方验证(无误码) ==========
[验证结果] 校验和正确,接受数据报
========== 接收方验证(数据被篡改: Hello -> HellX) ==========
[验证结果] 校验和错误,静默丢弃!

11. 校验和计算全流程 ASCII 示意

发送方计算校验和:
  伪头部(12字节)
  +-----------------+
  | 192.168.1.10    | 源IP(4字节)
  +-----------------+
  | 8.8.8.8         | 目的IP(4字节)
  +-----------------+
  | 0x00 | 17 | 13  | Zero + Proto + UDP Length(4字节)
  +-----------------+
  UDP头部(8字节)
  +-----------------+
  | 54321  |  53    | 源端口 + 目的端口
  +-----------------+
  | 13     |  0000  | Length + Checksum(先置0)
  +-----------------+
  UDP数据(5字节 + 1虚拟填充字节)
  +-----------------+
  | H e l l o | 00 | (0x00是虚拟填充,不传输)
  +-----------------+
         |
         v
  对所有16位字做反码累加求和
         |
         v
  结果取反 --> Checksum
  (若结果为0x0000,存储0xFFFF)
接收方验证(将checksum也纳入计算):
  同样的拼接(含已填入的checksum)
         |
         v
  反码求和
         |
         v
  结果 == 0xFFFF?
    是 --> 正确,接受
    否 --> 错误,静默丢弃

12. 知识要点总结


要点 说明
端到端 发送方计算,接收方验证,中途不改动
伪头部 12字节(IPv4),含源/目的IP,不传输,仅用于计算
奇数填充 数据为奇数字节时虚拟补一个 0 x 00 0x00 0x00,不传输
0 x 0000 0x0000 0x0000 表示发送方未计算校验和,接收方无需验证
0 x F F F F 0xFFFF 0xFFFF 计算结果为0时的替代存储值
NAT 影响 NAT 改IP地址后,必须同步重算 UDP 校验和
RFC 1122 要求默认开启,且收到非零校验和时必须验证

公式回顾:
计算时(发送方),checksum 字段先置 0 0 0
Checksum = ∼ ( ∑ i = 1 N W i ) 反码 \text{Checksum} = \sim \left( \sum_{i=1}^{N} W_i \right)_{\text{反码}} Checksum=∼(i=1NWi)反码
验证时(接收方),checksum 已填入,对整体求和:
∑ i = 1 N W i = 反码 0 xFFFF ⇒ 正确 \sum_{i=1}^{N} W_i \stackrel{\text{反码}}{=} 0\text{xFFFF} \Rightarrow \text{正确} i=1NWi=反码0xFFFF正确
其中 W i W_i Wi 是每个 16 位字, ∼ \sim 表示按位取反。

UDP 实例分析 & IPv6 伪头部详解(第10章 10.4~10.5节)

1. 实验环境说明

本节使用 sock 程序向目标主机发送 UDP 数据报,并用 tcpdump 抓包观察。

实验拓扑:
  发送方                      接收方
10.0.0.5 ─────────────────► 10.0.0.3
 port 46274                  port 9 (discard)

tcpdump 抓包命令:

tcpdump -n -p -s 1500 -vvv host 10.0.0.3 and \( udp or icmp \)

各参数含义:

参数 含义
-n 不将 IP 地址转换为主机名(直接显示数字IP)
-p 不将网络接口置为混杂模式
-s 1500 最多捕获每个包的前 1500 字节
-vvv 极详细的输出模式
host 10.0.0.3 and \( udp or icmp \) 只捕获与 10.0.0.3 之间的 UDP 或 ICMP 流量

2. 场景一:服务端正常运行

sock 命令:

sock -v -u -i 10.0.0.3 discard

参数说明:

  • -v:显示详细信息(含临时端口号)
  • -u:使用 UDP(默认是 TCP)
  • -i:主动发送数据而非等待标准输入
    程序输出:
connected on 10.0.0.5.46274 to 10.0.0.3
wrote 1024 bytes
... (重复 1023 次)

tcpdump 抓包输出(Listing 10-1):

1 22:52:53.102838 10.0.0.5.46274 > 10.0.0.3.9:
               [udp sum ok] udp 1024 (DF) (ttl 64, id 24462, len 1052)
2 22:52:53.102964 10.0.0.5.46274 > 10.0.0.3.9:
               [udp sum ok] udp 1024 (DF) (ttl 64, id 24463, len 1052)
3 22:52:53.103091 10.0.0.5.46274 > 10.0.0.3.9:
               [udp sum ok] udp 1024 (DF) (ttl 64, id 24464, len 1052)
4 22:52:53.103215 10.0.0.5.46274 > 10.0.0.3.9:
               [udp sum ok] udp 1024 (DF) (ttl 64, id 24465, len 1052)
... 重复 1020 次 ...

2.1 数据包大小分析

每个 UDP/IPv4 数据报的总长度 = 1052 = 1052 =1052 字节,拆分如下:
总长度 = 20 ⏟ IPv4头部 + 8 ⏟ UDP头部 + 1024 ⏟ UDP载荷 = 1052  字节 \text{总长度} = \underbrace{20}_{\text{IPv4头部}} + \underbrace{8}_{\text{UDP头部}} + \underbrace{1024}_{\text{UDP载荷}} = 1052 \text{ 字节} 总长度=IPv4头部 20+UDP头部 8+UDP载荷 1024=1052 字节

2.2 从 tcpdump 输出中读到的信息


字段 观察值 含义
udp sum ok 出现在每个包 UDP 校验和已启用且经 tcpdump 验证正确
DF 每个包都有 Don’t Fragment 位置1,不允许分片
ttl 64 固定为 64 IPv4 生存时间字段
id 24462... 每包递增1 IPv4 标识字段,每个数据报唯一
包间隔 100   μ s 100\,\mu s 100μs 相邻包时间戳之差约为 0.000126s

2.3 UDP 无确认机制的含义

发送方                         接收方
   |                              |
   |──── UDP datagram 1 ─────────►|
   |──── UDP datagram 2 ─────────►|
   |──── UDP datagram 3 ─────────►|  (无任何ACK返回)
   |──── ...                     |
   |──── UDP datagram 1024 ──────►|
   |                              |
   发送完毕,但发送方不知道
   对方是否真的收到了!

与 TCP 不同,UDP 不握手、不确认,发送方无法知道数据是否真正到达。

3. 场景二:服务端已关闭(ICMP Port Unreachable)

第二次运行 sock,但 discard 服务已停止:

connected on 10.0.0.5.46294 to 10.0.0.3
wrote 1 bytes
write returned -1, expected 1024: Connection refused

tcpdump 抓包输出(Listing 10-2):

1 22:55:07.223094 10.0.0.5.46294 > 10.0.0.3.9:
               [udp sum ok] udp 1024 (DF) (ttl 64, id 37874, len 1052)
2 22:55:07.223134 10.0.0.3 > 10.0.0.5: icmp:
               10.0.0.3 udp port 9 unreachable for
                  10.0.0.5.46294 > 10.0.0.3.9:
                        udp 1024 (DF) (ttl 64, id 37874, len 1052)
               [tos 0xc0]  (ttl 255, id 63302, len 576)

3.1 交互流程

服务端 10.0.0.3:9 客户端 10.0.0.5:46294 服务端 10.0.0.3:9 客户端 10.0.0.5:46294 端口9无服务监听! write() 返回 -1 Connection refused UDP datagram (1024字节) id=37874, DF, ttl=64 ICMPv4 Destination Unreachable Port Unreachable 包含原始数据报前556字节

3.2 ICMP Port Unreachable 消息详解

当目的主机收到 UDP 数据报,但对应端口没有任何进程在监听时,内核会自动生成一个 ICMPv4 目的不可达(端口不可达) 消息返回给发送方。
ICMP 报文中包含了原始数据报的前 556 字节,让发送方能够确认是哪个端口不可达。

ICMP Port Unreachable 消息结构:
+-----------------------------+
| ICMP 类型=3 代码=3           |  目的不可达,端口不可达
+-----------------------------+
| 原始 UDP 数据报(前556字节) |  帮助发送方定位问题
+-----------------------------+

3.3 临时端口号的变化


运行次数 源端口号
第一次 46274
第二次 46294

每次运行程序,操作系统会分配不同的临时端口(Ephemeral Port)
RFC 建议临时端口范围为 49152 ∼ 65535 49152 \sim 65535 4915265535,但此处观察到的 46274 46274 46274 46294 46294 46294 均低于此范围,属于不符合规范的行为
Linux 系统可通过以下方式查看/修改本地端口范围:

# 查看当前范围
cat /proc/sys/net/ipv4/ip_local_port_range
# 修改范围(例如改为 32768 ~ 60999)
echo "32768 60999" > /proc/sys/net/ipv4/ip_local_port_range

4. UDP over IPv6 的伪头部(40 字节)

4.1 IPv6 伪头部结构

 0              15 16             31
+----------------------------------+
|                                  |
|      源 IPv6 地址(16字节)       |  来自 IPv6 头部
|                                  |
|                                  |
+----------------------------------+
|                                  |
|     目的 IPv6 地址(16字节)      |  来自 IPv6 头部
|                                  |
|                                  |
+----------------------------------+
|         Length(4字节)           |  32位,比IPv4伪头部更大
+----------------------------------+
|    Reserved(0)(3字节)  |NextHdr|  Next Header = 17(UDP)
+----------------------------------+
       <-- 共 40 字节 -->

4.2 IPv4 伪头部 vs IPv6 伪头部对比


字段 IPv4 伪头部(12字节) IPv6 伪头部(40字节)
源地址 4 字节 16 字节
目的地址 4 字节 16 字节
填充/保留 1字节零 3字节零
协议/Next Header 1字节(值=17) 1字节(值=17)
长度字段 2字节(16位) 4字节(32位)
总计 12 字节 40 字节

4.3 为什么 IPv6 中 UDP 校验和是强制的?

IPv4 : IP头部校验和 ⏟ 保护IP头部 + UDP校验和(可选) ⏟ 保护UDP \text{IPv4}: \underbrace{\text{IP头部校验和}}_{\text{保护IP头部}} + \underbrace{\text{UDP校验和(可选)}}_{\text{保护UDP}} IPv4:保护IP头部 IP头部校验和+保护UDP UDP校验和(可选)
IPv6 : 无IP头部校验和 ⏟ ! + UDP校验和(强制) ⏟ 必须保护所有内容 \text{IPv6}: \underbrace{\text{无IP头部校验和}}_{\text{!}} + \underbrace{\text{UDP校验和(强制)}}_{\text{必须保护所有内容}} IPv6: IP头部校验和+必须保护所有内容 UDP校验和(强制)
IPv6 设计时为了提高转发效率,去掉了 IP 头部校验和(由路由器每跳重算会带来性能开销)。因此,UDP 在 IPv6 中必须启用校验和,否则数据错误将无法被检测。

4.4 Next Header 字段的含义

IPv6 使用扩展头部链的设计,数据报可能经过多个扩展头:

IPv6基本头部
Next Header=43

路由扩展头
Next Header=60

目的选项扩展头
Next Header=17

UDP头部
载荷

伪头部中的 Next Header 字段,取的是最后一个扩展头部中的 Next Header 值(即直接指向 UDP 的那个,值为 17 17 17),而不是 IPv6 基本头部中的值。

5. 完整 C++ 代码示例

以下代码演示 IPv4 和 IPv6 两种伪头部的构造与校验和计算:

#include <iostream>
#include <cstdint>
#include <cstring>
#include <vector>
#include <iomanip>
#include <arpa/inet.h>   // htons, htonl, ntohs, inet_addr, inet_pton
// ============================================================
// UDP 头部(8字节,IPv4和IPv6通用)
// ============================================================
struct UDPHeader {
    uint16_t src_port;
    uint16_t dst_port;
    uint16_t length;
    uint16_t checksum;
};
// ============================================================
// IPv4 伪头部(12字节)
// ============================================================
struct PseudoV4 {
    uint32_t src_ip;     // 源IPv4地址(网络字节序)
    uint32_t dst_ip;     // 目的IPv4地址(网络字节序)
    uint8_t  zero;       // 固定为0
    uint8_t  protocol;   // UDP = 17
    uint16_t udp_len;    // UDP总长度(网络字节序)
};
// ============================================================
// IPv6 伪头部(40字节)
// ============================================================
struct PseudoV6 {
    uint8_t  src_ip[16]; // 源IPv6地址(16字节)
    uint8_t  dst_ip[16]; // 目的IPv6地址(16字节)
    uint32_t udp_len;    // UDP总长度(32位,网络字节序)
    uint8_t  reserved[3];// 保留字段,固定为0
    uint8_t  next_hdr;   // Next Header = 17(UDP)
};
// ============================================================
// 16位反码校验和计算(Internet Checksum)
// 参数: data - 数据缓冲区指针
//       len  - 字节数(可以为奇数,内部自动补0处理)
// 返回: 校验和(若结果为0x0000则返回0xFFFF)
// ============================================================
uint16_t internetChecksum(const uint8_t* data, size_t len) {
    uint32_t sum = 0;
    // 每次取2字节(16位)做反码累加
    while (len > 1) {
        uint16_t word = (static_cast<uint16_t>(data[0]) << 8)
                      |  static_cast<uint16_t>(data[1]);
        sum += word;
        data += 2;
        len  -= 2;
    }
    // 奇数字节:最后1字节高位对齐,低位虚拟补0
    if (len == 1) {
        sum += static_cast<uint16_t>(data[0]) << 8;
    }
    // 反码折叠:将进位加回低16位
    while (sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    uint16_t result = static_cast<uint16_t>(~sum);
    // 0x0000保留给"未计算"标记,真正为0时存0xFFFF
    return (result == 0x0000) ? 0xFFFF : result;
}
// ============================================================
// 计算 UDP/IPv4 校验和
// ============================================================
uint16_t udpChecksumV4(const char*      src_ip_str,
                        const char*      dst_ip_str,
                        const UDPHeader& hdr,
                        const uint8_t*   payload,
                        size_t           pay_len)
{
    PseudoV4 pseudo;
    pseudo.src_ip   = inet_addr(src_ip_str);  // 字符串IP转网络字节序整数
    pseudo.dst_ip   = inet_addr(dst_ip_str);
    pseudo.zero     = 0;
    pseudo.protocol = 17;
    pseudo.udp_len  = hdr.length;             // 已经是网络字节序
    // 拼接:伪头部 + UDP头部 + 载荷
    size_t total = sizeof(PseudoV4) + sizeof(UDPHeader) + pay_len;
    std::vector<uint8_t> buf(total, 0);
    size_t off = 0;
    memcpy(buf.data() + off, &pseudo, sizeof(PseudoV4));  off += sizeof(PseudoV4);
    memcpy(buf.data() + off, &hdr,    sizeof(UDPHeader)); off += sizeof(UDPHeader);
    memcpy(buf.data() + off, payload, pay_len);
    return internetChecksum(buf.data(), total);
}
// ============================================================
// 计算 UDP/IPv6 校验和
// ============================================================
uint16_t udpChecksumV6(const char*      src_ip6_str,
                        const char*      dst_ip6_str,
                        const UDPHeader& hdr,
                        const uint8_t*   payload,
                        size_t           pay_len)
{
    PseudoV6 pseudo;
    memset(&pseudo, 0, sizeof(PseudoV6));
    // inet_pton: 将IPv6字符串地址转为16字节二进制(网络字节序)
    inet_pton(AF_INET6, src_ip6_str, pseudo.src_ip);
    inet_pton(AF_INET6, dst_ip6_str, pseudo.dst_ip);
    // IPv6伪头部的长度字段是32位
    uint32_t udp_total = ntohs(hdr.length);  // 先转为主机字节序
    pseudo.udp_len  = htonl(udp_total);      // 再转为32位网络字节序
    pseudo.next_hdr = 17;                    // UDP协议号
    // 拼接:伪头部(40字节) + UDP头部(8字节) + 载荷
    size_t total = sizeof(PseudoV6) + sizeof(UDPHeader) + pay_len;
    std::vector<uint8_t> buf(total, 0);
    size_t off = 0;
    memcpy(buf.data() + off, &pseudo, sizeof(PseudoV6));  off += sizeof(PseudoV6);
    memcpy(buf.data() + off, &hdr,    sizeof(UDPHeader)); off += sizeof(UDPHeader);
    memcpy(buf.data() + off, payload, pay_len);
    return internetChecksum(buf.data(), total);
}
// ============================================================
// 打印十六进制校验和
// ============================================================
void printChecksum(const std::string& label, uint16_t cs) {
    std::cout << label << ": 0x"
              << std::hex << std::uppercase
              << std::setw(4) << std::setfill('0') << cs
              << std::dec << "\n";
}
// ============================================================
// 主函数:分别演示 IPv4 和 IPv6 的 UDP 校验和计算
// ============================================================
int main() {
    // 模拟1024字节的载荷(全部填充为 'A')
    const size_t PAY_LEN = 1024;
    std::vector<uint8_t> payload(PAY_LEN, 'A');
    // -------- UDP/IPv4 场景(对应 Listing 10-1)--------
    {
        UDPHeader hdr;
        hdr.src_port = htons(46274);   // 客户端临时端口
        hdr.dst_port = htons(9);       // discard 服务端口
        hdr.length   = htons(static_cast<uint16_t>(8 + PAY_LEN));
        hdr.checksum = 0;              // 计算前必须置0
        uint16_t cs = udpChecksumV4("10.0.0.5", "10.0.0.3",
                                    hdr, payload.data(), PAY_LEN);
        hdr.checksum = htons(cs);
        std::cout << "===== UDP/IPv4 校验和计算 =====\n";
        std::cout << "源:       10.0.0.5:46274\n";
        std::cout << "目的:     10.0.0.3:9 (discard)\n";
        std::cout << "载荷大小: " << PAY_LEN << " 字节\n";
        std::cout << "UDP总长:  " << (8 + PAY_LEN) << " 字节\n";
        printChecksum("UDP校验和", cs);
        std::cout << "\n";
    }
    // -------- UDP/IPv6 场景 --------
    {
        const char* src6 = "2001:db8::5";    // 示例源IPv6地址
        const char* dst6 = "2001:db8::3";    // 示例目的IPv6地址
        UDPHeader hdr;
        hdr.src_port = htons(46274);
        hdr.dst_port = htons(9);
        hdr.length   = htons(static_cast<uint16_t>(8 + PAY_LEN));
        hdr.checksum = 0;
        uint16_t cs = udpChecksumV6(src6, dst6,
                                    hdr, payload.data(), PAY_LEN);
        hdr.checksum = htons(cs);
        std::cout << "===== UDP/IPv6 校验和计算 =====\n";
        std::cout << "源:       " << src6 << ":46274\n";
        std::cout << "目的:     " << dst6 << ":9\n";
        std::cout << "载荷大小: " << PAY_LEN << " 字节\n";
        std::cout << "伪头部:   40 字节(IPv6地址各16字节+32位长度+Next Header)\n";
        printChecksum("UDP校验和", cs);
        std::cout << "注意: IPv6中UDP校验和是强制的!\n";
    }
    return 0;
}

https://godbolt.org/z/Ez3qYTb6v
编译与运行:

g++ -std=c++17 -o udp_v4v6 udp_v4v6.cpp && ./udp_v4v6

6. 两种场景完整对比(ASCII 流程图)

场景一:服务端正在运行
─────────────────────────────────────────────────
客户端 10.0.0.5:46274          服务端 10.0.0.3:9
    │                               │
    ├──UDP(1024字节)──────────────►│  discard服务接收
    ├──UDP(1024字节)──────────────►│  直接丢弃数据
    ├──...(共1024个)──────────────►│  不回复任何内容
    │                               │
    结果:全部发出,但UDP无法确认是否到达
场景二:服务端已关闭
─────────────────────────────────────────────────
客户端 10.0.0.5:46294          服务端 10.0.0.3:9
    │                               │
    ├──UDP(1024字节)──────────────►│  端口9无监听进程
    │                               │
    │◄── ICMPv4 Port Unreachable ──┤  内核自动生成ICMP
    │    (含原始包前556字节)        │
    │                               │
    write()返回-1
    "Connection refused"
    后续数据报不再发送

7. 知识要点总结


要点 说明
UDP 无确认 发送方不知道数据是否到达,与 TCP 相比缺乏可靠性保证
ICMP Port Unreachable 目的端口无监听时,接收方内核返回此 ICMP,应用层表现为 Connection refused
临时端口号 每次运行程序可能分配不同端口,Linux 可通过 /proc/sys/net/ipv4/ip_local_port_range 配置
IPv6 伪头部 40 字节(IPv4 是 12 字节),地址更长,长度字段扩展为 32 位
IPv6 强制校验和 因 IPv6 头部无校验和字段,UDP 在 IPv6 中必须启用校验和
Next Header 取扩展头链中最后一个头部的 Next Header 值( = 17 = 17 =17 对应 UDP)

IPv4 vs IPv6 伪头部大小:
4 + 4 + 1 + 1 + 2 ⏟ IPv4伪头部 = 12  字节 \underbrace{4 + 4 + 1 + 1 + 2}_{\text{IPv4伪头部}} = 12 \text{ 字节} IPv4伪头部 4+4+1+1+2=12 字节
16 + 16 + 4 + 3 + 1 ⏟ IPv6伪头部 = 40  字节 \underbrace{16 + 16 + 4 + 3 + 1}_{\text{IPv6伪头部}} = 40 \text{ 字节} IPv6伪头部 16+16+4+3+1=40 字节

UDP 与 IPv6 详解(第10章 10.5节)

1. UDP 在 IPv6 下的主要变化

UDP 协议本身非常简单,从 IPv4 切换到 IPv6 时只需做少量调整。最核心的两点变化是:

IPv4 下的 UDP                     IPv6 下的 UDP
─────────────────                 ─────────────────
地址长度:32 位(4字节)     →    地址长度:128 位(16字节)
IP头部有校验和               →    IP头部无校验和
UDP校验和:可选(建议开启)  →    UDP校验和:强制必须开启
伪头部:12 字节              →    伪头部:40 字节
Length字段:16位             →    伪头部中Length:32位

2. 为什么 IPv6 中 UDP 校验和是强制的?

IPv4 头部本身有一个校验和,能够保护 IP 层的地址信息(源/目的 IP 地址)。即便 UDP 校验和被关闭,至少 IP 头部校验和还能捕获地址错误。
IPv6 则完全去掉了 IP 头部校验和(为了提高路由器转发速度):

IPv4 的错误保护层次:
┌───────────────────────────────────┐
│  应用层                           │
├───────────────────────────────────┤
│  UDP(校验和可选,覆盖头部+数据)  │  ← 第2道防线
├───────────────────────────────────┤
│  IPv4(头部校验和,仅覆盖IP头部) │  ← 第1道防线
└───────────────────────────────────┘
IPv6 的错误保护层次:
┌───────────────────────────────────┐
│  应用层                           │
├───────────────────────────────────┤
│  UDP(校验和强制,覆盖伪头部+数据)│  ← 唯一防线,不能省略!
├───────────────────────────────────┤
│  IPv6(无头部校验和)             │  ← 没有防线
└───────────────────────────────────┘

如果 UDP/IPv6 也禁用校验和,那么从源到目的的整个传输过程中,没有任何机制能检测 IP 地址是否正确、数据是否被修改。这是不可接受的,因此 RFC 2460 第8节规定:UDP over IPv6 必须计算并验证校验和

3. IPv6 对 UDP 包大小的两个影响

3.1 最小 MTU 变大


协议 最小 MTU(所有主机必须支持)
IPv4 576 字节
IPv6 1280 字节

MTU(最大传输单元)决定了一个数据报不需要分片就能通过网络的最大尺寸。IPv6 将最小 MTU 从 576 字节提高到 1280 字节,意味着 UDP 在 IPv6 网络中可以发送更大的单个数据报而不必担心分片。

3.2 超大数据报(Jumbogram)

IPv6 支持超大数据报(Jumbogram),即长度超过 65535 65535 65535 字节的数据包。
通过 IPv6 的 Jumbo Payload 选项,载荷长度字段扩展到 32 位
最大 Jumbogram 载荷长度 = 2 32 − 1 = 4,294,967,295  字节 ≈ 4  GB \text{最大 Jumbogram 载荷长度} = 2^{32} - 1 = 4{,}294{,}967{,}295 \text{ 字节} \approx 4 \text{ GB} 最大 Jumbogram 载荷长度=2321=4,294,967,295 字节4 GB
这对于高性能计算、超大文件传输等场景非常有用。

4. Jumbogram 下 UDP Length 字段的问题

UDP 头部中的 Length 字段只有 16 位,最大只能表示:
2 16 − 1 = 65535  字节 2^{16} - 1 = 65535 \text{ 字节} 2161=65535 字节
但 Jumbogram 的载荷可以远超这个值,产生矛盾:

UDP 头部 Length 字段(16位):
┌──────────────────────────────────────┐
│ 最大表示值:65535 字节               │
│ 实际数据:可能超过 4GB !            │  <-- 放不下!
└──────────────────────────────────────┘

解决方案(RFC 2675):
当 UDP/IPv6 数据报超过 65535 字节时,UDP 头部的 Length 字段强制设置为 0,用这个特殊值表示"这是一个 Jumbogram,真实长度请从 IPv6 Jumbo Payload 选项中读取"。

4.1 发送方如何计算 Jumbogram 的真实 UDP 长度

UDP真实长度 = UDP头部(8字节) + UDP数据长度 \text{UDP真实长度} = \text{UDP头部(8字节)} + \text{UDP数据长度} UDP真实长度=UDP头部(8字节)+UDP数据长度
这个值写入伪头部的 Length 字段(32位,放得下),而 UDP 头部自身的 Length 字段置 0 0 0

4.2 接收方如何还原 Jumbogram 的 UDP 长度

UDP数据报长度 = Jumbo Payload 选项中的总载荷长度 ⏟ 32位 − ∑ IPv6扩展头部长度 \text{UDP数据报长度} = \underbrace{\text{Jumbo Payload 选项中的总载荷长度}}_{\text{32位}} - \sum \text{IPv6扩展头部长度} UDP数据报长度=32 Jumbo Payload 选项中的总载荷长度IPv6扩展头部长度
其中 IPv6 基本头部固定为 40 字节,不计入载荷长度。

4.3 特殊情况:Length=0 但没有 Jumbo Payload 选项

若收到一个 UDP 头部 Length=0,但 IPv6 头部中并没有 Jumbo Payload 选项,则可以从 IPv6 的普通 Payload Length 字段(非零)推算出 UDP 数据报长度:
UDP长度 = IPv6 Payload Length − ∑ 扩展头部长度 \text{UDP长度} = \text{IPv6 Payload Length} - \sum \text{扩展头部长度} UDP长度=IPv6 Payload Length扩展头部长度

5. 各场景下 UDP Length 字段的处理规则

构造 UDP/IPv6 数据报

UDP总长度 > 65535?

正常填写 UDP Length 字段
值 = 8 + 数据字节数

UDP Length 字段置 0
启用 Jumbo Payload 选项

伪头部 Length 字段
填入真实的32位长度

伪头部 Length 字段
与 UDP Length 相同

计算校验和

6. IPv6 伪头部 Length 字段为何不冗余?

在 UDP 场景下,伪头部的 Length 字段与 UDP 头部的 Length 字段相同,是冗余的
但在 TCP 中,TCP 头部本身没有独立的 Length 字段(TCP 数据长度由 IP 层的总长度减去头部长度推算),因此伪头部的 Length 字段对于 TCP 来说是不冗余的,是唯一明确记录载荷长度的地方。
这就是为什么即使对 UDP 是冗余的,IPv6 伪头部仍然保留了这个 32 位的 Length 字段——为了 TCP/IPv6 的需要,统一使用相同的伪头部结构。

伪头部 Length 字段的冗余性:
          UDP/IPv4   UDP/IPv6   TCP/IPv4   TCP/IPv6
          ─────────  ─────────  ─────────  ─────────
冗余?       是          是         否          否
用途       一致性检查  一致性检查  唯一来源    唯一来源

7. 完整 C++ 代码示例

以下代码演示普通 UDP/IPv6 和 Jumbogram 两种场景下的长度处理与校验和计算:

#include <iostream>
#include <cstdint>
#include <cstring>
#include <vector>
#include <iomanip>
#include <arpa/inet.h>   // htons, htonl, ntohs, ntohl, inet_pton
// ============================================================
// UDP 头部(8字节)
// ============================================================
struct UDPHeader {
    uint16_t src_port;
    uint16_t dst_port;
    uint16_t length;    // 普通模式填实际长度;Jumbogram 模式置 0
    uint16_t checksum;
};
// ============================================================
// IPv6 伪头部(40字节)
// 用于 UDP/IPv6 校验和计算,不实际传输
// ============================================================
struct PseudoV6 {
    uint8_t  src_ip[16];   // 源 IPv6 地址(16字节)
    uint8_t  dst_ip[16];   // 目的 IPv6 地址(16字节)
    uint32_t udp_length;   // UDP总长度(32位,网络字节序)
    uint8_t  reserved[3];  // 保留,固定为 0
    uint8_t  next_hdr;     // Next Header = 17(UDP)
};
// ============================================================
// 16位反码校验和(Internet Checksum)
// ============================================================
uint16_t internetChecksum(const uint8_t* data, size_t len) {
    uint32_t sum = 0;
    // 每次取 2 字节累加
    while (len > 1) {
        uint16_t w = (static_cast<uint16_t>(data[0]) << 8)
                   |  static_cast<uint16_t>(data[1]);
        sum += w;
        data += 2;
        len  -= 2;
    }
    // 奇数字节:末尾虚拟补 0x00
    if (len == 1) {
        sum += static_cast<uint16_t>(data[0]) << 8;
    }
    // 反码折叠:进位回卷
    while (sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    uint16_t result = static_cast<uint16_t>(~sum);
    // 结果为 0 时用 0xFFFF 代替(0x0000 保留给"未计算"标记)
    return (result == 0x0000) ? 0xFFFF : result;
}
// ============================================================
// 计算 UDP/IPv6 校验和(同时支持普通模式和 Jumbogram 模式)
// 参数:
//   src6      - 源 IPv6 地址字符串,如 "2001:db8::1"
//   dst6      - 目的 IPv6 地址字符串
//   udphdr    - UDP 头部(checksum 字段须预先置 0)
//   payload   - 载荷数据指针
//   pay_len   - 载荷字节数
//   is_jumbo  - 是否为 Jumbogram 模式
// ============================================================
uint16_t udpChecksumV6(const char*      src6,
                        const char*      dst6,
                        const UDPHeader& udphdr,
                        const uint8_t*   payload,
                        size_t           pay_len,
                        bool             is_jumbo)
{
    PseudoV6 pseudo;
    memset(&pseudo, 0, sizeof(PseudoV6));
    // 将 IPv6 字符串地址转为 16 字节二进制(网络字节序)
    if (inet_pton(AF_INET6, src6, pseudo.src_ip) != 1) {
        std::cerr << "无效的源 IPv6 地址: " << src6 << "\n";
        return 0;
    }
    if (inet_pton(AF_INET6, dst6, pseudo.dst_ip) != 1) {
        std::cerr << "无效的目的 IPv6 地址: " << dst6 << "\n";
        return 0;
    }
    // 伪头部的 Length 字段(32位)始终填真实长度
    // 即使 UDP 头部 Length 字段置 0(Jumbogram 情况),伪头部仍填真实值
    uint64_t real_udp_len = 8 + static_cast<uint64_t>(pay_len);
    pseudo.udp_length = htonl(static_cast<uint32_t>(real_udp_len));
    pseudo.next_hdr   = 17;   // UDP 协议号
    // 拼接:伪头部(40字节) + UDP 头部(8字节) + 载荷
    size_t total = sizeof(PseudoV6) + sizeof(UDPHeader) + pay_len;
    std::vector<uint8_t> buf(total, 0);
    size_t off = 0;
    memcpy(buf.data() + off, &pseudo, sizeof(PseudoV6));  off += sizeof(PseudoV6);
    memcpy(buf.data() + off, &udphdr, sizeof(UDPHeader)); off += sizeof(UDPHeader);
    memcpy(buf.data() + off, payload, pay_len);
    return internetChecksum(buf.data(), total);
}
// ============================================================
// 构造并打印一个 UDP/IPv6 数据报(普通或 Jumbogram 模式)
// ============================================================
void buildUDPv6(const char*    src6,
                const char*    dst6,
                uint16_t       src_port,
                uint16_t       dst_port,
                size_t         pay_len,
                bool           is_jumbo)
{
    // 构造虚拟载荷(全填充为 'X')
    std::vector<uint8_t> payload(pay_len, 'X');
    UDPHeader hdr;
    hdr.src_port = htons(src_port);
    hdr.dst_port = htons(dst_port);
    hdr.checksum = 0;   // 计算前必须置 0
    if (is_jumbo) {
        // Jumbogram:UDP 头部 Length 字段置 0
        // 真实长度通过 IPv6 Jumbo Payload 选项携带
        hdr.length = 0;
    } else {
        // 普通模式:填写真实长度(8 + 载荷字节数)
        hdr.length = htons(static_cast<uint16_t>(8 + pay_len));
    }
    // 计算校验和(伪头部 Length 字段始终使用真实长度)
    uint16_t cs = udpChecksumV6(src6, dst6, hdr, payload.data(), pay_len, is_jumbo);
    hdr.checksum = htons(cs);
    // 打印结果
    std::cout << (is_jumbo ? "===== Jumbogram 模式 =====" : "===== 普通模式 =====") << "\n";
    std::cout << "源:            " << src6 << ":" << src_port << "\n";
    std::cout << "目的:          " << dst6 << ":" << dst_port << "\n";
    std::cout << "载荷大小:      " << pay_len << " 字节\n";
    std::cout << "UDP总长度:     " << (8 + pay_len) << " 字节\n";
    if (is_jumbo) {
        std::cout << "UDP Length字段: 0(Jumbogram,真实长度由IPv6 Jumbo Payload选项携带)\n";
    } else {
        std::cout << "UDP Length字段: " << (8 + pay_len) << "\n";
    }
    std::cout << "伪头部Length:  " << (8 + pay_len) << "(32位,始终为真实长度)\n";
    std::cout << "UDP校验和:     0x"
              << std::hex << std::uppercase
              << std::setw(4) << std::setfill('0') << cs
              << std::dec << "\n\n";
}
// ============================================================
// 主函数:演示普通 UDP/IPv6 和 Jumbogram 两种场景
// ============================================================
int main() {
    const char* src6 = "2001:db8::1";
    const char* dst6 = "2001:db8::2";
    // 场景 1:普通 UDP/IPv6(载荷 1024 字节,远小于 65535)
    buildUDPv6(src6, dst6, 54321, 9, 1024, false);
    // 场景 2:Jumbogram(载荷 100000 字节,超过 65535)
    // 注意:此处演示逻辑,实际发送需要操作系统和网卡支持
    buildUDPv6(src6, dst6, 54321, 9, 100000, true);
    // 场景 3:验证 IPv6 最小 MTU 边界
    // IPv6 要求所有链路至少支持 1280 字节 MTU
    // UDP 头部8字节 + IPv6头部40字节 = 48字节开销
    // 因此单包不分片最大载荷(在最小MTU下)= 1280 - 40 - 8 = 1232 字节
    uint32_t ipv6_min_mtu  = 1280;
    uint32_t ipv6_hdr_size = 40;
    uint32_t udp_hdr_size  = 8;
    uint32_t max_payload_no_frag = ipv6_min_mtu - ipv6_hdr_size - udp_hdr_size;
    std::cout << "===== IPv6 最小 MTU 分析 =====\n";
    std::cout << "IPv6 最小 MTU:          " << ipv6_min_mtu << " 字节\n";
    std::cout << "IPv6 基本头部:          " << ipv6_hdr_size << " 字节\n";
    std::cout << "UDP 头部:               " << udp_hdr_size << " 字节\n";
    std::cout << "最大不分片 UDP 载荷:    "
              << max_payload_no_frag << " 字节\n";
    // Jumbogram 理论最大载荷
    uint64_t jumbo_max = (1ULL << 32) - 1 - 8;  // 32位长度最大值减去UDP头部8字节
    std::cout << "\nJumbogram 理论最大 UDP 载荷: "
              << jumbo_max << " 字节 (~"
              << (jumbo_max / (1024*1024*1024)) << " GB)\n";
    return 0;
}

https://godbolt.org/z/9zr93d4dT
编译与运行:

g++ -std=c++17 -o udp_ipv6 udp_ipv6.cpp && ./udp_ipv6

8. Jumbogram 下 Length 字段处理流程(ASCII 示意)

发送方(Jumbogram,数据 > 65535 字节):
  UDP 头部:
  ┌──────────┬──────────┬──────────┬──────────┐
  │ src_port │ dst_port │ length=0 │ checksum │
  └──────────┴──────────┴──────────┴──────────┘
                              ↑
                      置0!真实长度
                      放在IPv6选项里
  IPv6 扩展选项(Jumbo Payload Option):
  ┌──────────────────────────────────┐
  │  Jumbo Payload Length = 实际值   │  ← 32位,容纳超大长度
  └──────────────────────────────────┘
  伪头部(用于校验和):
  ┌──────────────────────────────────┐
  │  Length = 8 + 实际数据长度       │  ← 始终填真实值
  └──────────────────────────────────┘
接收方还原 UDP 长度:
  Jumbo Payload Length
         │
         │  减去所有扩展头部长度
         ↓
  UDP 数据报长度 = Jumbo Payload Length - Σ(扩展头部长度)

9. MTU 对比

进化

Jumbo Payload 选项

IPv4 最小 MTU
576 字节

IPv6 最小 MTU
1280 字节

Jumbogram
最大约 4GB


场景 最大 UDP 载荷
IPv4 最小 MTU(576字节,不分片) 576 − 20 − 8 = 548 576 - 20 - 8 = 548 576208=548 字节
IPv6 最小 MTU(1280字节,不分片) 1280 − 40 − 8 = 1232 1280 - 40 - 8 = 1232 1280408=1232 字节
普通 IPv6(无 Jumbogram) 65535 − 8 = 65527 65535 - 8 = 65527 655358=65527 字节(UDP Length 16位限制)
IPv6 Jumbogram 2 32 − 1 − 8 ≈ 4  GB 2^{32} - 1 - 8 \approx 4\text{ GB} 232184 GB

10. 知识要点总结


要点 说明
IPv6 无 IP 层校验和 UDP 校验和在 IPv6 中强制启用,是唯一的端到端保护
伪头部扩大到 40 字节 源/目的 IPv6 地址各 16 字节,Length 扩展为 32 位
IPv6 最小 MTU = 1280 比 IPv4 的 576 字节更大,有利于减少分片
Jumbogram 超过 65535 字节的数据报,通过 IPv6 Jumbo Payload 选项支持
Jumbogram 的 UDP Length UDP 头部 Length 字段置 0,真实长度放在 IPv6 扩展选项中
伪头部 Length 不置 0 即使是 Jumbogram,伪头部的 Length 字段仍填真实 32 位长度
Length 字段对 TCP 不冗余 TCP 头部本身无长度字段,伪头部的 Length 是 TCP 长度的唯一来源

关键公式:
普通 UDP/IPv6 最大不分片载荷(最小 MTU 下):
最大载荷 = 1280 ⏟ IPv6最小MTU − 40 ⏟ IPv6头部 − 8 ⏟ UDP头部 = 1232  字节 \text{最大载荷} = \underbrace{1280}_{\text{IPv6最小MTU}} - \underbrace{40}_{\text{IPv6头部}} - \underbrace{8}_{\text{UDP头部}} = 1232 \text{ 字节} 最大载荷=IPv6最小MTU 1280IPv6头部 40UDP头部 8=1232 字节
Jumbogram 下接收方还原 UDP 长度:
UDP长度 = L jumbo ⏟ Jumbo Payload Length − ∑ i L ext , i ⏟ 各扩展头部长度 \text{UDP长度} = \underbrace{L_{\text{jumbo}}}_{\text{Jumbo Payload Length}} - \sum_{i} \underbrace{L_{\text{ext},i}}_{\text{各扩展头部长度}} UDP长度=Jumbo Payload Length Ljumboi各扩展头部长度 Lext,i
Jumbogram 下 UDP 伪头部 Length(32位真实长度):
伪头部 Length = 8 + UDP数据字节数 ( ≤ 2 32 − 1 ) \text{伪头部 Length} = 8 + \text{UDP数据字节数} \quad (\leq 2^{32}-1) 伪头部 Length=8+UDP数据字节数(2321)

Teredo:IPv6穿越IPv4网络的隧道技术详解

1. 背景与动机

IPv6的推广远比预期缓慢,因此出现了多种过渡机制。6to4(RFC3056)是其中一种,但它存在NAT穿透难题和扩展性问题。Teredo(RFC4380)正是为解决这些问题而设计的——它的名字来自一种蛀船虫的拉丁学名,因为它能"钻穿"IPv4网络的障碍。

Teredo 在 Microsoft Windows 中被广泛内置,是目前最常见的 IPv6 过渡机制之一。

2. 整体架构

2.1 三种角色

角色 类比 功能
Teredo 客户端 普通用户 双栈主机,通过 UDP/IPv4 封装收发 IPv6 报文
Teredo 服务器 STUN 服务器 帮助客户端获取 IPv6 地址,探测 NAT 类型
Teredo 中继(Relay) TURN 服务器 转发客户端与原生 IPv6 主机之间的流量(最后手段)

服务器必须包含中继的全部功能,但反之不成立。

2.2 网络拓扑示意

+------------------+          +------------------+
|   Teredo 客户端   |          |   Teredo 服务器   |
|  (IPv4/IPv6双栈)  |<-------->|  teredo.ipv6.    |
+--------+---------+  资格认证  |  microsoft.com   |
         |                    +--------+---------+
         | NAT N1                      |
         |                             | IPv4 Internet
+--------v---------+          +--------v---------+
|    NAT 网关 N1    |          |   Teredo 中继     |
| (映射地址/端口)   |          | (广播 2001::/32)  |
+--------+---------+          +--------+---------+
         |                             |
         |      IPv4 Internet          | IPv6 Internet
         +-----------------------------+
                                       |
                              +--------v---------+
                              |  原生 IPv6 主机 C |
                              +------------------+

3. Teredo 地址结构(128位)

Image 3 展示了完整的地址格式:

|<----------------------- 128 bits IPv6 地址 ---------------------->|
+------------------+------------------+-------+--------+------------+
| Teredo IPv6前缀  | 服务器IPv4地址   | Flags | 映射端口| 映射IPv4   |
|    (32 bits)     |    (32 bits)     |(16bit)|(16 bits)|  (32 bits) |
+------------------+------------------+-------+--------+------------+
       2001::/32         Teredo服务器                   客户端映射地址
                                                      (每位取反做混淆)

3.1 Flags 字段(16位)细节

Flags = C ⏟ 1 0 ⏟ 1 Random1 ⏟ 4 U ⏟ 1 G ⏟ 1 Random2 ⏟ 8 \text{Flags} = \underbrace{C}_{1} \underbrace{0}_{1} \underbrace{\text{Random1}}_{4} \underbrace{U}_{1} \underbrace{G}_{1} \underbrace{\text{Random2}}_{8} Flags=1 C1 04 Random11 U1 G8 Random2

位字段 含义
C(锥形NAT位) 曾用于标识 Cone NAT,现已废弃,应置 0
0 固定为 0
Random1(4位) 随机数,防止地址猜测攻击(RFC5991)
U(Universal) 保留,置 0
G(Group) 保留,置 0
Random2(8位) 随机数,进一步增强安全性

3.2 地址混淆

映射端口和映射IPv4地址使用**逐位取反(bitwise invert)**的方式存储,目的是防止某些NAT设备错误地修改这些字段。
存储值 = ∼ 实际值 \text{存储值} = \sim \text{实际值} 存储值=∼实际值

4. 资格认证(Qualification)流程

客户端必须完成资格认证才能获得 Teredo 地址。

客户端                          服务器
  |                               |
  |--- ICMPv6 RS (路由器请求) --->|   使用链路本地地址,经 Teredo 服务端口
  |    (Origin Indication封装)    |   默认端口 UDP 3544
  |                               |
  |<-- ICMPv6 RA (路由器通告) ----|   包含 Prefix Information 选项
  |    (Origin Indication封装)    |   携带有效的 Teredo 前缀
  |                               |
  | 客户端根据前缀和 Origin 信息    |
  | 构造 Teredo IPv6 地址          |
  |  --> 客户端状态变为"已认证"    |

认证成功后,客户端依据图10-7的格式组装出自己的 Teredo IPv6 地址。

5. 封装格式(图10-6)

Teredo 有两种封装格式:

5.1 简单封装(Simple Encapsulation)

+--------------------+
|   IPv4 头部        |  20 字节,无选项
+--------------------+
|   UDP 头部         |  8 字节
+--------------------+
|  封装的IPv6数据报  |  可变长度
+--------------------+
|  Trailers(可选)  |  可变长度,若存在
+--------------------+

5.2 Origin Indication 封装

在 UDP 头和 IPv6 数据报之间插入一个 8 字节的 Origin Indication 字段:

+--------------------+
|   IPv4 头部        |  20 字节
+--------------------+
|   UDP 头部         |  8 字节
+--------------------+
|  Origin Indication |  8 字节(仅 Origin Indication 封装时存在)
|  Zero(0) | 源端口  |  <- 端口经逐位取反混淆
|  源(映射)IPv4地址|  <- 地址经逐位取反混淆
+--------------------+
|  封装的IPv6数据报  |  可变长度
+--------------------+
|  Trailers(可选)  |  可变长度
+--------------------+

Origin Indication 的作用:告知客户端自己的映射地址和端口(NAT出口处的公网地址/端口)。

6. 三种通信场景

场景一:与同一链路上的 Teredo 客户端通信

使用 IPv4 组播地址 224.0.0.253 进行发现。
发送bubble包(无数据载荷的最小 Teredo 包)探测目标是否在同一链路。
bubble 包的 IPv6 头中 Next Header 字段置为 0x3b(无下一头部)。

场景二:与 IPv4 Internet 上的 Teredo 客户端通信

由于 Teredo 地址中已经编码了对方的映射 IPv4 地址和端口,可以直接发送封装包到对方 NAT。
对于限制性 NAT,使用 bubble 包做**UDP打洞(hole punching)**建立 NAT 映射。

场景三:与原生 IPv6 主机通信

客户端                  Teredo服务器            IPv6主机C          Teredo中继
  |                         |                      |                  |
  |-- ICMPv6 Echo Request ->|-- 转发到IPv6主机 --->|                  |
  |   (含64位随机数)        |                      |                  |
  |                         |                      |-- Echo Reply --->|
  |                         |                      |  (路由到最近中继)|
  |<--------------------------------------- Echo Reply 转回 ----------|
  |                                                                   |
  | 记录中继的IPv4地址,后续直接发往中继(Simple Encapsulation)       |

7. Teredo Trailers(扩展尾部)

Trailers 附加在 IPv6 载荷之后,采用 TLV(类型-长度-值)格式,与 ICMPv6 ND 选项格式相同。

7.1 类型字段的高两位含义

Type [ 7 : 6 ] = { 01 不识别则丢弃该包 其他 不识别则跳过,继续处理后续 Trailer \text{Type}[7:6] = \begin{cases} 01 & \text{不识别则丢弃该包} \\ \text{其他} & \text{不识别则跳过,继续处理后续 Trailer} \end{cases} Type[7:6]={01其他不识别则丢弃该包不识别则跳过,继续处理后续 Trailer

7.2 已定义的 Trailer 类型

类型 名称 长度 用途
0x01 Nonce 0x04 32位随机数,防重放攻击;与 HP 或 SNS 配合使用
0x02 Random Port 0x02 发送方预测的映射端口号,用于 PP 扩展
0x03 Alternate Address [8,26] 列出同一 NAT 后的其他可用地址/端口(最多4组),用于 HP 扩展
0x04 ND Discovery Option 0x02 包含 TeredoDiscoverySolicitation(0x00) 或 TeredoDiscoveryAdvertisement(0x01),用于 SLR 扩展

7.3 各扩展说明

扩展缩写 全称 功能简述
SNS Symmetric NAT Support 支持对称型 NAT
UP UPnP-Enabled Symmetric NAT 支持启用 UPnP 的对称 NAT
PP Port-Preserving Symmetric NAT 支持端口保留型对称 NAT
SP Sequential Port-Symmetric NAT 支持端口顺序分配的对称 NAT
HP Hairpinning 支持 NAT 发夹转发
SLR Server Load Reduction 使用直连 bubble 刷新 NAT 状态,降低服务器负载

UP 和 PP 扩展依赖 SNS 扩展,其余扩展可独立使用。

8. NAT 类型与 Teredo 兼容性

NAT类型                           Teredo支持情况
-----------                       ---------------
Cone NAT(端点无关映射+过滤)      无需扩展,直接支持
对称型 NAT(地址/端口相关映射)    需要 SNS 等扩展

Cone NAT 是家庭网络最常见的类型,Teredo 对其支持最好。
对称型 NAT 需要配合相应扩展(SNS/UP/PP/SP)才能工作。

9. C++ 代码示例:解析 Teredo IPv6 地址

下面的代码演示如何从一个 128 位 IPv6 地址中提取 Teredo 各字段。

#include <cstdint>
#include <cstring>
#include <cstdio>
#include <arpa/inet.h>
// Teredo 地址结构(对应128位IPv6地址的各字段)
struct TeredoAddress {
    uint32_t teredo_prefix;   // 前32位:Teredo前缀(2001:0000)
    uint32_t server_ipv4;     // 32位:Teredo服务器IPv4地址
    uint16_t flags;           // 16位:标志字段
    uint16_t mapped_port_obs; // 16位:映射端口(已混淆,即取反)
    uint32_t mapped_ip_obs;   // 32位:映射IPv4地址(已混淆,即取反)
};
// 从原始IPv6地址字节流(16字节,网络字节序)解析Teredo地址
void parse_teredo(const uint8_t ipv6[16], TeredoAddress &out) {
    // 逐字段复制,注意网络字节序(大端)
    uint32_t tmp32;
    uint16_t tmp16;
    // 前缀:字节0~3
    memcpy(&tmp32, ipv6 + 0, 4);
    out.teredo_prefix = ntohl(tmp32);
    // 服务器IPv4:字节4~7
    memcpy(&tmp32, ipv6 + 4, 4);
    out.server_ipv4 = ntohl(tmp32);
    // Flags:字节8~9
    memcpy(&tmp16, ipv6 + 8, 2);
    out.flags = ntohs(tmp16);
    // 映射端口(混淆):字节10~11
    memcpy(&tmp16, ipv6 + 10, 2);
    out.mapped_port_obs = ntohs(tmp16);
    // 映射IPv4(混淆):字节12~15
    memcpy(&tmp32, ipv6 + 12, 4);
    out.mapped_ip_obs = ntohl(tmp32);
}
// 去除混淆:逐位取反(complement)还原实际值
uint16_t deobfuscate_port(uint16_t obs) {
    return ~obs;  // 按位取反
}
uint32_t deobfuscate_ip(uint32_t obs) {
    return ~obs;  // 按位取反
}
// 将uint32_t的IPv4地址格式化为点分十进制字符串
void format_ipv4(uint32_t ip, char buf[INET_ADDRSTRLEN]) {
    uint32_t net = htonl(ip);
    inet_ntop(AF_INET, &net, buf, INET_ADDRSTRLEN);
}
int main() {
    // 示例:构造一个 Teredo IPv6 地址的字节表示
    // 格式:2001:0000 | server_ip | flags | ~port | ~client_ip
    // 假设:
    //   服务器IP  = 65.54.227.120  (0x4136E378)
    //   flags     = 0x0000
    //   客户端IP  = 192.168.1.100  (0xC0A80164)
    //   客户端端口= 12345          (0x3039)
    uint8_t ipv6[16] = {
        0x20, 0x01,             // Teredo前缀高16位
        0x00, 0x00,             // Teredo前缀低16位
        0x41, 0x36, 0xE3, 0x78, // 服务器IPv4: 65.54.227.120
        0x00, 0x00,             // Flags = 0
        // 映射端口取反: ~12345 = ~0x3039 = 0xCFC6
        0xCF, 0xC6,
        // 映射IP取反: ~0xC0A80164 = 0x3F57FE9B
        0x3F, 0x57, 0xFE, 0x9B
    };
    TeredoAddress addr;
    parse_teredo(ipv6, addr);
    // 还原真实端口和IP
    uint16_t real_port = deobfuscate_port(addr.mapped_port_obs);
    uint32_t real_ip   = deobfuscate_ip(addr.mapped_ip_obs);
    char server_ip_str[INET_ADDRSTRLEN];
    char client_ip_str[INET_ADDRSTRLEN];
    format_ipv4(addr.server_ipv4, server_ip_str);
    format_ipv4(real_ip, client_ip_str);
    printf("=== Teredo 地址解析结果 ===\n");
    printf("Teredo 前缀:       0x%08X (2001::/32)\n", addr.teredo_prefix);
    printf("服务器 IPv4:       %s\n", server_ip_str);
    printf("Flags:             0x%04X\n", addr.flags);
    printf("  C位(Cone NAT):   %d (已废弃)\n", (addr.flags >> 15) & 1);
    printf("  Random1(4bit):   %d\n", (addr.flags >> 10) & 0xF);
    printf("  U位:             %d\n", (addr.flags >> 9) & 1);
    printf("  G位:             %d\n", (addr.flags >> 8) & 1);
    printf("  Random2(8bit):   %d\n", addr.flags & 0xFF);
    printf("映射端口(混淆值):  0x%04X\n", addr.mapped_port_obs);
    printf("实际映射端口:      %u\n", real_port);
    printf("实际映射 IPv4:     %s\n", client_ip_str);
    return 0;
}

编译与运行:

g++ -o teredo_parse teredo_parse.cpp
./teredo_parse

预期输出:

=== Teredo 地址解析结果 ===
Teredo 前缀:       0x20010000 (2001::/32)
服务器 IPv4:       65.54.227.120
Flags:             0x0000
  C位(Cone NAT):   0 (已废弃)
  Random1(4bit):   0
  U位:             0
  G位:             0
  Random2(8bit):   0
映射端口(混淆值):  0xCFC6
实际映射端口:      12345
实际映射 IPv4:     192.168.1.100

10. 完整流程图(Mermaid)

同链路 Teredo 客户端

IPv4网络中的 Teredo 客户端

原生 IPv6 主机

客户端启动

配置服务器地址
默认 UDP 3544

发送 ICMPv6 RS
使用 Origin Indication 封装

服务器响应?

认证失败
参见 RFC4380

收到 ICMPv6 RA
包含 Teredo 前缀和映射信息

构造 Teredo IPv6 地址
前缀+服务器IP+Flags+映射端口+映射IP

客户端已认证

目标类型?

IPv4组播 224.0.0.253
发送 bubble 包探测

直接发送封装包到对方 NAT
必要时 bubble 打洞

已知中继?

Simple Encapsulation
直接发往中继

经服务器发送 ICMPv6 Echo Request

IPv6主机回复 Echo Reply
路由到最近中继

客户端记录中继 IPv4 地址

11. 关键要点总结

  1. Teredo 的本质:用 UDP/IPv4 作为载体传输 IPv6 报文,绕过 IPv4-only 基础设施。
  2. 地址构造规则 Teredo地址 = 前缀 ( 2001 : : / 32 ) + 服务器IP + Flags + 端口 ~ + 客户端IP ~ \text{Teredo地址} = \text{前缀}(2001::/32) + \text{服务器IP} + \text{Flags} + \widetilde{\text{端口}} + \widetilde{\text{客户端IP}} Teredo地址=前缀(2001::/32)+服务器IP+Flags+端口 +客户端IP ,其中 x ~ \widetilde{x} x 表示按位取反。
  3. 两种封装:Simple Encapsulation 用于普通传输;Origin Indication Encapsulation 用于传递映射地址信息。
  4. Trailers 的作用:通过 TLV 格式附加扩展信息,支持对称 NAT、防重放、发夹转发等高级特性。
  5. 使用原则:Teredo 是最后手段——一旦发现其他 IPv6 连接方式(如直连或 6to4),节点应立即停止使用 Teredo 隧道。

Teredo:IPv6穿越IPv4网络的隧道技术详解

1. 背景与动机

IPv6的推广远比预期缓慢,因此出现了多种过渡机制。6to4(RFC3056)是其中一种,但它存在NAT穿透难题和扩展性问题。Teredo(RFC4380)正是为解决这些问题而设计的——它的名字来自一种蛀船虫的拉丁学名,因为它能"钻穿"IPv4网络的障碍。

Teredo 在 Microsoft Windows 中被广泛内置,是目前最常见的 IPv6 过渡机制之一。

2. 整体架构

2.1 三种角色


角色 类比 功能
Teredo 客户端 普通用户 双栈主机,通过 UDP/IPv4 封装收发 IPv6 报文
Teredo 服务器 STUN 服务器 帮助客户端获取 IPv6 地址,探测 NAT 类型
Teredo 中继(Relay) TURN 服务器 转发客户端与原生 IPv6 主机之间的流量(最后手段)

服务器必须包含中继的全部功能,但反之不成立。

2.2 网络拓扑示意

+------------------+          +------------------+
|   Teredo 客户端   |          |   Teredo 服务器   |
|  (IPv4/IPv6双栈)  |<-------->|  teredo.ipv6.    |
+--------+---------+  资格认证  |  microsoft.com   |
         |                    +--------+---------+
         | NAT N1                      |
         |                             | IPv4 Internet
+--------v---------+          +--------v---------+
|    NAT 网关 N1    |          |   Teredo 中继     |
| (映射地址/端口)   |          | (广播 2001::/32)  |
+--------+---------+          +--------+---------+
         |                             |
         |      IPv4 Internet          | IPv6 Internet
         +-----------------------------+
                                       |
                              +--------v---------+
                              |  原生 IPv6 主机 C |
                              +------------------+

3. Teredo 地址结构(128位)

Image 3 展示了完整的地址格式:

|<----------------------- 128 bits IPv6 地址 ---------------------->|
+------------------+------------------+-------+--------+------------+
| Teredo IPv6前缀  | 服务器IPv4地址   | Flags | 映射端口| 映射IPv4   |
|    (32 bits)     |    (32 bits)     |(16bit)|(16 bits)|  (32 bits) |
+------------------+------------------+-------+--------+------------+
       2001::/32         Teredo服务器                   客户端映射地址
                                                      (每位取反做混淆)

3.1 Flags 字段(16位)细节

Flags = C ⏟ 1 0 ⏟ 1 Random1 ⏟ 4 U ⏟ 1 G ⏟ 1 Random2 ⏟ 8 \text{Flags} = \underbrace{C}_{1} \underbrace{0}_{1} \underbrace{\text{Random1}}_{4} \underbrace{U}_{1} \underbrace{G}_{1} \underbrace{\text{Random2}}_{8} Flags=1 C1 04 Random11 U1 G8 Random2

位字段 含义
C(锥形NAT位) 曾用于标识 Cone NAT,现已废弃,应置 0
0 固定为 0
Random1(4位) 随机数,防止地址猜测攻击(RFC5991)
U(Universal) 保留,置 0
G(Group) 保留,置 0
Random2(8位) 随机数,进一步增强安全性

3.2 地址混淆

映射端口和映射IPv4地址使用**逐位取反(bitwise invert)**的方式存储,目的是防止某些NAT设备错误地修改这些字段。
存储值 = ∼ 实际值 \text{存储值} = \sim \text{实际值} 存储值=∼实际值

4. 资格认证(Qualification)流程

客户端必须完成资格认证才能获得 Teredo 地址。

客户端                          服务器
  |                               |
  |--- ICMPv6 RS (路由器请求) --->|   使用链路本地地址,经 Teredo 服务端口
  |    (Origin Indication封装)    |   默认端口 UDP 3544
  |                               |
  |<-- ICMPv6 RA (路由器通告) ----|   包含 Prefix Information 选项
  |    (Origin Indication封装)    |   携带有效的 Teredo 前缀
  |                               |
  | 客户端根据前缀和 Origin 信息    |
  | 构造 Teredo IPv6 地址          |
  |  --> 客户端状态变为"已认证"    |

认证成功后,客户端依据图10-7的格式组装出自己的 Teredo IPv6 地址。

5. 封装格式(图10-6)

Teredo 有两种封装格式:

5.1 简单封装(Simple Encapsulation)

+--------------------+
|   IPv4 头部        |  20 字节,无选项
+--------------------+
|   UDP 头部         |  8 字节
+--------------------+
|  封装的IPv6数据报  |  可变长度
+--------------------+
|  Trailers(可选)  |  可变长度,若存在
+--------------------+

5.2 Origin Indication 封装

在 UDP 头和 IPv6 数据报之间插入一个 8 字节的 Origin Indication 字段:

+--------------------+
|   IPv4 头部        |  20 字节
+--------------------+
|   UDP 头部         |  8 字节
+--------------------+
|  Origin Indication |  8 字节(仅 Origin Indication 封装时存在)
|  Zero(0) | 源端口  |  <- 端口经逐位取反混淆
|  源(映射)IPv4地址|  <- 地址经逐位取反混淆
+--------------------+
|  封装的IPv6数据报  |  可变长度
+--------------------+
|  Trailers(可选)  |  可变长度
+--------------------+

Origin Indication 的作用:告知客户端自己的映射地址和端口(NAT出口处的公网地址/端口)。

6. 三种通信场景

场景一:与同一链路上的 Teredo 客户端通信

使用 IPv4 组播地址 224.0.0.253 进行发现。
发送bubble包(无数据载荷的最小 Teredo 包)探测目标是否在同一链路。
bubble 包的 IPv6 头中 Next Header 字段置为 0x3b(无下一头部)。

场景二:与 IPv4 Internet 上的 Teredo 客户端通信

由于 Teredo 地址中已经编码了对方的映射 IPv4 地址和端口,可以直接发送封装包到对方 NAT。
对于限制性 NAT,使用 bubble 包做**UDP打洞(hole punching)**建立 NAT 映射。

场景三:与原生 IPv6 主机通信

客户端                  Teredo服务器            IPv6主机C          Teredo中继
  |                         |                      |                  |
  |-- ICMPv6 Echo Request ->|-- 转发到IPv6主机 --->|                  |
  |   (含64位随机数)        |                      |                  |
  |                         |                      |-- Echo Reply --->|
  |                         |                      |  (路由到最近中继)|
  |<--------------------------------------- Echo Reply 转回 ----------|
  |                                                                   |
  | 记录中继的IPv4地址,后续直接发往中继(Simple Encapsulation)       |

7. Teredo Trailers(扩展尾部)

Trailers 附加在 IPv6 载荷之后,采用 TLV(类型-长度-值)格式,与 ICMPv6 ND 选项格式相同。

7.1 类型字段的高两位含义

Type [ 7 : 6 ] = { 01 不识别则丢弃该包 其他 不识别则跳过,继续处理后续 Trailer \text{Type}[7:6] = \begin{cases} 01 & \text{不识别则丢弃该包} \\ \text{其他} & \text{不识别则跳过,继续处理后续 Trailer} \end{cases} Type[7:6]={01其他不识别则丢弃该包不识别则跳过,继续处理后续 Trailer

7.2 已定义的 Trailer 类型


类型 名称 长度 用途
0x01 Nonce 0x04 32位随机数,防重放攻击;与 HP 或 SNS 配合使用
0x02 Random Port 0x02 发送方预测的映射端口号,用于 PP 扩展
0x03 Alternate Address [8,26] 列出同一 NAT 后的其他可用地址/端口(最多4组),用于 HP 扩展
0x04 ND Discovery Option 0x02 包含 TeredoDiscoverySolicitation(0x00) 或 TeredoDiscoveryAdvertisement(0x01),用于 SLR 扩展

7.3 各扩展说明


扩展缩写 全称 功能简述
SNS Symmetric NAT Support 支持对称型 NAT
UP UPnP-Enabled Symmetric NAT 支持启用 UPnP 的对称 NAT
PP Port-Preserving Symmetric NAT 支持端口保留型对称 NAT
SP Sequential Port-Symmetric NAT 支持端口顺序分配的对称 NAT
HP Hairpinning 支持 NAT 发夹转发
SLR Server Load Reduction 使用直连 bubble 刷新 NAT 状态,降低服务器负载

UP 和 PP 扩展依赖 SNS 扩展,其余扩展可独立使用。

8. NAT 类型与 Teredo 兼容性

NAT类型                           Teredo支持情况
-----------                       ---------------
Cone NAT(端点无关映射+过滤)      无需扩展,直接支持
对称型 NAT(地址/端口相关映射)    需要 SNS 等扩展

Cone NAT 是家庭网络最常见的类型,Teredo 对其支持最好。
对称型 NAT 需要配合相应扩展(SNS/UP/PP/SP)才能工作。

9. C++ 代码示例:解析 Teredo IPv6 地址

下面的代码演示如何从一个 128 位 IPv6 地址中提取 Teredo 各字段。

#include <cstdint>
#include <cstring>
#include <cstdio>
#include <arpa/inet.h>
// Teredo 地址结构(对应128位IPv6地址的各字段)
struct TeredoAddress {
    uint32_t teredo_prefix;   // 前32位:Teredo前缀(2001:0000)
    uint32_t server_ipv4;     // 32位:Teredo服务器IPv4地址
    uint16_t flags;           // 16位:标志字段
    uint16_t mapped_port_obs; // 16位:映射端口(已混淆,即取反)
    uint32_t mapped_ip_obs;   // 32位:映射IPv4地址(已混淆,即取反)
};
// 从原始IPv6地址字节流(16字节,网络字节序)解析Teredo地址
void parse_teredo(const uint8_t ipv6[16], TeredoAddress &out) {
    // 逐字段复制,注意网络字节序(大端)
    uint32_t tmp32;
    uint16_t tmp16;
    // 前缀:字节0~3
    memcpy(&tmp32, ipv6 + 0, 4);
    out.teredo_prefix = ntohl(tmp32);
    // 服务器IPv4:字节4~7
    memcpy(&tmp32, ipv6 + 4, 4);
    out.server_ipv4 = ntohl(tmp32);
    // Flags:字节8~9
    memcpy(&tmp16, ipv6 + 8, 2);
    out.flags = ntohs(tmp16);
    // 映射端口(混淆):字节10~11
    memcpy(&tmp16, ipv6 + 10, 2);
    out.mapped_port_obs = ntohs(tmp16);
    // 映射IPv4(混淆):字节12~15
    memcpy(&tmp32, ipv6 + 12, 4);
    out.mapped_ip_obs = ntohl(tmp32);
}
// 去除混淆:逐位取反(complement)还原实际值
uint16_t deobfuscate_port(uint16_t obs) {
    return ~obs;  // 按位取反
}
uint32_t deobfuscate_ip(uint32_t obs) {
    return ~obs;  // 按位取反
}
// 将uint32_t的IPv4地址格式化为点分十进制字符串
void format_ipv4(uint32_t ip, char buf[INET_ADDRSTRLEN]) {
    uint32_t net = htonl(ip);
    inet_ntop(AF_INET, &net, buf, INET_ADDRSTRLEN);
}
int main() {
    // 示例:构造一个 Teredo IPv6 地址的字节表示
    // 格式:2001:0000 | server_ip | flags | ~port | ~client_ip
    // 假设:
    //   服务器IP  = 65.54.227.120  (0x4136E378)
    //   flags     = 0x0000
    //   客户端IP  = 192.168.1.100  (0xC0A80164)
    //   客户端端口= 12345          (0x3039)
    uint8_t ipv6[16] = {
        0x20, 0x01,             // Teredo前缀高16位
        0x00, 0x00,             // Teredo前缀低16位
        0x41, 0x36, 0xE3, 0x78, // 服务器IPv4: 65.54.227.120
        0x00, 0x00,             // Flags = 0
        // 映射端口取反: ~12345 = ~0x3039 = 0xCFC6
        0xCF, 0xC6,
        // 映射IP取反: ~0xC0A80164 = 0x3F57FE9B
        0x3F, 0x57, 0xFE, 0x9B
    };
    TeredoAddress addr;
    parse_teredo(ipv6, addr);
    // 还原真实端口和IP
    uint16_t real_port = deobfuscate_port(addr.mapped_port_obs);
    uint32_t real_ip   = deobfuscate_ip(addr.mapped_ip_obs);
    char server_ip_str[INET_ADDRSTRLEN];
    char client_ip_str[INET_ADDRSTRLEN];
    format_ipv4(addr.server_ipv4, server_ip_str);
    format_ipv4(real_ip, client_ip_str);
    printf("=== Teredo 地址解析结果 ===\n");
    printf("Teredo 前缀:       0x%08X (2001::/32)\n", addr.teredo_prefix);
    printf("服务器 IPv4:       %s\n", server_ip_str);
    printf("Flags:             0x%04X\n", addr.flags);
    printf("  C位(Cone NAT):   %d (已废弃)\n", (addr.flags >> 15) & 1);
    printf("  Random1(4bit):   %d\n", (addr.flags >> 10) & 0xF);
    printf("  U位:             %d\n", (addr.flags >> 9) & 1);
    printf("  G位:             %d\n", (addr.flags >> 8) & 1);
    printf("  Random2(8bit):   %d\n", addr.flags & 0xFF);
    printf("映射端口(混淆值):  0x%04X\n", addr.mapped_port_obs);
    printf("实际映射端口:      %u\n", real_port);
    printf("实际映射 IPv4:     %s\n", client_ip_str);
    return 0;
}

编译与运行:

g++ -o teredo_parse teredo_parse.cpp
./teredo_parse

预期输出:

=== Teredo 地址解析结果 ===
Teredo 前缀:       0x20010000 (2001::/32)
服务器 IPv4:       65.54.227.120
Flags:             0x0000
  C位(Cone NAT):   0 (已废弃)
  Random1(4bit):   0
  U位:             0
  G位:             0
  Random2(8bit):   0
映射端口(混淆值):  0xCFC6
实际映射端口:      12345
实际映射 IPv4:     192.168.1.100

10. 完整流程图(Mermaid)

同链路 Teredo 客户端

IPv4网络中的 Teredo 客户端

原生 IPv6 主机

客户端启动

配置服务器地址
默认 UDP 3544

发送 ICMPv6 RS
使用 Origin Indication 封装

服务器响应?

认证失败
参见 RFC4380

收到 ICMPv6 RA
包含 Teredo 前缀和映射信息

构造 Teredo IPv6 地址
前缀+服务器IP+Flags+映射端口+映射IP

客户端已认证

目标类型?

IPv4组播 224.0.0.253
发送 bubble 包探测

直接发送封装包到对方 NAT
必要时 bubble 打洞

已知中继?

Simple Encapsulation
直接发往中继

经服务器发送 ICMPv6 Echo Request

IPv6主机回复 Echo Reply
路由到最近中继

客户端记录中继 IPv4 地址

11. 关键要点总结

  1. Teredo 的本质:用 UDP/IPv4 作为载体传输 IPv6 报文,绕过 IPv4-only 基础设施。
  2. 地址构造规则 Teredo地址 = 前缀 ( 2001 : : / 32 ) + 服务器IP + Flags + 端口 ~ + 客户端IP ~ \text{Teredo地址} = \text{前缀}(2001::/32) + \text{服务器IP} + \text{Flags} + \widetilde{\text{端口}} + \widetilde{\text{客户端IP}} Teredo地址=前缀(2001::/32)+服务器IP+Flags+端口 +客户端IP ,其中 x ~ \widetilde{x} x 表示按位取反。
  3. 两种封装:Simple Encapsulation 用于普通传输;Origin Indication Encapsulation 用于传递映射地址信息。
  4. Trailers 的作用:通过 TLV 格式附加扩展信息,支持对称 NAT、防重放、发夹转发等高级特性。
  5. 使用原则:Teredo 是最后手段——一旦发现其他 IPv6 连接方式(如直连或 6to4),节点应立即停止使用 Teredo 隧道。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐