bigywater.tech
网络协议 · 18

DSCP 与 ECN:服务类型字段里的两种标记

IP 头里的服务类型字段到底是什么?DSCP 和 ECN 各占几位?标了 DSCP 的包到了公网上会怎样?这篇用 IP_TOS 把各种 DSCP/ECN 值发到线上,抓包验证,再沿着一条真实公网路径看每一跳收到的 TOS,以及 TCP 的 ECN 协商。

0xb8
EF(DSCP 46)对应的 TOS 字节;发出后线上原样出现,但公网路径从第 2 跳起看到的是 0x00
0xba → 0x02
发出 EF + ECT(0),第 2 跳起 DSCP 被清掉而 ECN 位保留
4 / 4
连通的公网主机里接受本机 ECN 请求的个数(SYN-ACK 带 ECE)

TOS 字节

实测 · 本机执行输出
  lo0:发了 12 个,抓到 12 个
    CS0        设置 0x00 → 读回 0x00 → 线上 TOS 字节 0x00(DSCP 0,ECN 0) → 接收方 IP_RECVTOS 读到 0x00
    CS1        设置 0x20 → 读回 0x20 → 线上 TOS 字节 0x20(DSCP 8,ECN 0) → 接收方 IP_RECVTOS 读到 0x20
    AF11       设置 0x28 → 读回 0x28 → 线上 TOS 字节 0x28(DSCP 10,ECN 0) → 接收方 IP_RECVTOS 读到 0x28
    AF21       设置 0x48 → 读回 0x48 → 线上 TOS 字节 0x48(DSCP 18,ECN 0) → 接收方 IP_RECVTOS 读到 0x48
    AF31       设置 0x68 → 读回 0x68 → 线上 TOS 字节 0x68(DSCP 26,ECN 0) → 接收方 IP_RECVTOS 读到 0x68
    AF41       设置 0x88 → 读回 0x88 → 线上 TOS 字节 0x88(DSCP 34,ECN 0) → 接收方 IP_RECVTOS 读到 0x88
    EF         设置 0xb8 → 读回 0xb8 → 线上 TOS 字节 0xb8(DSCP 46,ECN 0) → 接收方 IP_RECVTOS 读到 0xb8
    CS6        设置 0xc0 → 读回 0xc0 → 线上 TOS 字节 0xc0(DSCP 48,ECN 0) → 接收方 IP_RECVTOS 读到 0xc0
    CS7        设置 0xe0 → 读回 0xe0 → 线上 TOS 字节 0xe0(DSCP 56,ECN 0) → 接收方 IP_RECVTOS 读到 0xe0
    EF+ECT(0)  设置 0xba → 读回 0xba → 线上 TOS 字节 0xba(DSCP 46,ECN 2) → 接收方 IP_RECVTOS 读到 0xba
    EF+ECT(1)  设置 0xb9 → 读回 0xb9 → 线上 TOS 字节 0xb9(DSCP 46,ECN 1) → 接收方 IP_RECVTOS 读到 0xb9
    EF+CE      设置 0xbb → 读回 0xbb → 线上 TOS 字节 0xbb(DSCP 46,ECN 3) → 接收方 IP_RECVTOS 读到 0xbb
  en0:发了 12 个,抓到 12 个
    CS0        设置 0x00 → 读回 0x00 → 线上 TOS 字节 0x00(DSCP 0,ECN 0)
    CS1        设置 0x20 → 读回 0x20 → 线上 TOS 字节 0x20(DSCP 8,ECN 0)
    AF11       设置 0x28 → 读回 0x28 → 线上 TOS 字节 0x28(DSCP 10,ECN 0)
    AF21       设置 0x48 → 读回 0x48 → 线上 TOS 字节 0x48(DSCP 18,ECN 0)
    AF31       设置 0x68 → 读回 0x68 → 线上 TOS 字节 0x68(DSCP 26,ECN 0)
    AF41       设置 0x88 → 读回 0x88 → 线上 TOS 字节 0x88(DSCP 34,ECN 0)
    EF         设置 0xb8 → 读回 0xb8 → 线上 TOS 字节 0xb8(DSCP 46,ECN 0)
    CS6        设置 0xc0 → 读回 0xc0 → 线上 TOS 字节 0xc0(DSCP 48,ECN 0)
    CS7        设置 0xe0 → 读回 0xe0 → 线上 TOS 字节 0xe0(DSCP 56,ECN 0)
    EF+ECT(0)  设置 0xba → 读回 0xba → 线上 TOS 字节 0xba(DSCP 46,ECN 2)
    EF+ECT(1)  设置 0xb9 → 读回 0xb9 → 线上 TOS 字节 0xb9(DSCP 46,ECN 1)
    EF+CE      设置 0xbb → 读回 0xbb → 线上 TOS 字节 0xbb(DSCP 46,ECN 3)

高 6 位是 DSCP(服务等级,EF = 46、AF41 = 34 等),低 2 位是 ECN。IP_TOS 设的字节在线上原样出现,接收方可以用 IP_RECVTOS 读出来。

公网路径

实测 · 本机执行输出
    TTL(第几跳)  发 0xb8  发 0x88  发 0x28  发 0x00  发 0xba
         1         --        --        --       0x00      0xba  
         2        0x00      0x00      0x00      0x00      0x02  
         3        0x00      0x00      0x00      0x00      0x02  
         4        0x00      0x00      0x00      0x00      0x02  
         5         --        --       0x00      0x00      0x02  
         6        0x00       --        --        --       0x02  
         7        0x00       --        --        --        --   
         8        0x00      0x00      0x00       --        --   
         9         --        --       0x00       --       0x02  
        10         --        --        --        --        --   
        11        0x00      0x00      0x00      0x00      0x02  
        12        0x00      0x00      0x00      0x00      0x02  
  发出 0xb8:收到引用的 8 跳里,与原值不同的跳数 8;第一个不同出现在第 2 跳,之后各跳看到的值 [0]
  发出 0x88:收到引用的 6 跳里,与原值不同的跳数 6;第一个不同出现在第 2 跳,之后各跳看到的值 [0]
  发出 0x28:收到引用的 8 跳里,与原值不同的跳数 8;第一个不同出现在第 2 跳,之后各跳看到的值 [0]
  发出 0x00:收到引用的 7 跳里,与原值不同的跳数 0;第一个不同出现在第 None 跳,所有跳看到的都与发出的相同
  发出 0xba:收到引用的 9 跳里,与原值不同的跳数 8;第一个不同出现在第 2 跳,之后各跳看到的值 [2]
  “--”表示该跳没有回超时报文。引用的是路由器收到这个包时的头部:第 N 跳看到的 TOS 已经是经过前 N−1 跳之后的值

实验的这条路径上,DSCP 标记被清成 0 之后才到后面的路由器,说明 DSCP 通常只在自己管辖的网络内有意义;ECN 位没被清掉。

流量类别

实测 · 本机执行输出
  lo0:BK(后台) 读回 200 → 线上 TOS 0x00;BE(默认) 读回 0 → 线上 TOS 0x00;VI(视频) 读回 700 → 线上 TOS 0x00;VO(语音) 读回 800 → 线上 TOS 0x00;CTL(控制) 读回 900 → 线上 TOS 0x00
  en0:BK(后台) 读回 200 → 线上 TOS 0x00;BE(默认) 读回 0 → 线上 TOS 0x00;VI(视频) 读回 700 → 线上 TOS 0x00;VO(语音) 读回 800 → 线上 TOS 0x00;CTL(控制) 读回 900 → 线上 TOS 0x00

SO_TRAFFIC_CLASS 告诉系统数据的类别,但不会替你写 DSCP;想让包带标记要自己设 IP_TOS。

ECN

实测 · 本机执行输出
  net.inet.tcp.ecn: 1
  主机 A:本机 SYN 标志 [SEW],SYN-ACK 标志 [S.E] → 接受 ECN;有数据的包:本机发出 1 个,IP ECN 位取值 [2];收到 2 个,ECN 位取值 [0](0 = 不支持,1 / 2 = ECT,3 = CE 已拥塞标记)
  主机 B:本机 SYN 标志 [SEW],SYN-ACK 标志 [S.E] → 接受 ECN;有数据的包:本机发出 3 个,IP ECN 位取值 [0, 2];收到 2 个,ECN 位取值 [0](0 = 不支持,1 / 2 = ECT,3 = CE 已拥塞标记)
  主机 C:本机 SYN 标志 [SEW],SYN-ACK 标志 [S.E] → 接受 ECN;有数据的包:本机发出 1 个,IP ECN 位取值 [2];收到 1 个,ECN 位取值 [0](0 = 不支持,1 / 2 = ECT,3 = CE 已拥塞标记)
  主机 D:没有连通
  主机 E:本机 SYN 标志 [SEW],SYN-ACK 标志 [S.E] → 接受 ECN;有数据的包:本机发出 1 个,IP ECN 位取值 [2];收到 1 个,ECN 位取值 [0](0 = 不支持,1 / 2 = ECT,3 = CE 已拥塞标记)
  ECN 位含义:00 不支持 ECN;10 = ECT(0) 和 01 = ECT(1) 表示发送方支持;11 = CE 表示路径上的路由器在队列拥塞时打了标记,而没有丢包

TCP 主动连接时 SYN 带 ECE + CWR,对方回只带 ECE 的 SYN-ACK 表示接受;之后双方的数据包 IP 头里设 ECT(0),路由器拥塞时可以改成 CE 代替丢包。这个实验看到了协商成功和 ECT(0),没有看到 CE。

套接字设置

实测 · 本机执行输出
  客户端(IP_TOS=0xb8) 的 FIN:1 个包,TOS 字节取值 ['0xb8']
  客户端(IP_TOS=0xb8) 的 SYN:1 个包,TOS 字节取值 ['0xb8']
  客户端(IP_TOS=0xb8) 的 带数据/ACK:4 个包,TOS 字节取值 ['0xb8']
  服务端(没设 TOS) 的 FIN:1 个包,TOS 字节取值 ['0x00']
  服务端(没设 TOS) 的 SYN:1 个包,TOS 字节取值 ['0x00']
  服务端(没设 TOS) 的 带数据/ACK:4 个包,TOS 字节取值 ['0x00']

IP_TOS 按套接字设置,连接的另一端不会继承;TCP 的 SYN、数据、FIN 都带这个值。

分步演示

DSCP 与 ECN
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。第一、五节在 lo0 / en0 上抓包;第二节沿一条真实公网路径用 TTL 逐跳探测,靠 ICMP 超时报文引用的原包头读取每一跳收到的 TOS(同一跳有时不回,表中用 -- 表示),设备地址不写出;第四节连了 5 个公网主机,其中 1 个没有连通
    • 没有验证:DSCP 被清除的确切位置(第 1 跳的回应这一轮缺失);Wi-Fi 的 WMM / 802.11e 队列优先级实际如何映射 DSCP 或 SO_TRAFFIC_CLASS;路由器在拥塞时打 CE 的行为;TCP 的 ECN 回应流程(ECE / CWR 的收发);其它运营商网络是否保留 DSCP;IPv6 的流量类别字段
    京ICP备2026067502号-1京公网安备11010502063487号