写在前面

sk_buff 是 Linux 网络栈最核心、也最容易被误解的数据结构之一。它并不是“装着一个完整网络包的结构体”,而是贯穿驱动、协议栈、队列、Socket、Netfilter、tc、BPF 与硬件 offload 的控制对象和所有权载体

解释 VLAN metadata、inner/outer header、encapsulation、隧道 GSO/checksum,以及 IPv4/IPv6 分片队列和 frag_list 重组后的非线性布局。

本文以本地 Linux 5.10.209 源码为主线;涉及演进时对照 Linux 6.1.99,涉及网卡驱动时结合 Firefly RK3588 SDK Linux 5.10.198。本文是源码分析,不把尚未执行的 QEMU、tracepoint 或开发板实验写成已验证结论。

Linux sk_buff 深度解析(七):VLAN、隧道、分片与重组

阅读前先建立四个问题

阅读任何 skb 代码路径,都建议反复追问:

  1. 当前 skb->data 指向哪一层协议头,linear 与 non-linear 数据分别在哪里?
  2. 哪些字段刚被修改,下一层为什么依赖这些字段?
  3. 当前谁持有 skb shell、head、frag page 以及 socket/dst/conntrack 等外部引用?
  4. 成功、失败、重试、redirect 和丢弃分支分别由谁消费或释放 skb?

本篇核心结论

  • VLAN tag 可能在报文字节中,也可能只存在 skb metadata 中。
  • 隧道 skb 同时保存 outer/inner header offset,offload 依赖这些元数据理解内层协议。
  • 分片产生多个独立 skb,重组队列必须持有并在超时/错误时释放全部片段。
  • 重组结果常用非线性布局或 frag_list,不能假设重新拼成一块连续内存。

1. VLAN、VXLAN、Geneve、GRE 与多层 Header

结论摘要

skb 可用两种方式表达 VLAN:tag 仍在 packet bytes 中,或硬件已剥离并存入 vlan_tci/vlan_proto metadata。隧道通过 outer 与 inner header offset、encapsulation、inner protocol 和 GSO/checksum metadata 同时描述两套协议栈。封装需要 headroom/COW,解封装需要正确 pull/reset metadata;错误维护会影响路由、checksum、GSO 和 BPF/tc 解析。

一、VLAN 两种形态

包内 VLAN header

1
Ethernet dst/src → 802.1Q/802.1ad header → inner EtherType → payload

占用 packet bytes,data/len/header offset 反映真实 4 字节或多层 tag。

hardware-accelerated metadata

网卡 RX 可剥离 tag,把:

  • VLAN protocol;
  • TCI(VID/PCP/DEI);
  • present bit

写入 skb 字段。packet bytes 中已没有该 tag,但协议栈仍能通过 helper 查询。

__vlan_hwaccel_put_tag() 写 metadata。TX 网卡若支持 VLAN insertion,可根据 metadata 在硬件发包时重新插入。

二、skb_vlan_untag

skb_vlan_untag()kernel-5.10/net/core/skbuff.c:5499。它把 packet bytes 中的 VLAN header 解析/移除并转为 metadata,涉及:

  • 确保 header 线性可读;
  • pull/memmove;
  • protocol 更新;
  • MAC header/len 调整;
  • COW/错误释放。

不能只减 len,否则 Ethernet 地址和 header offset 会错位。

三、VLAN push/pop

TC/BPF/bridge/VLAN device 可 push/pop tag:

  • push 需要 headroom 或 expand;
  • pop 需要确保 header linear;
  • 修改 EtherType;
  • 更新 MAC/network header;
  • 处理 checksum/GSO;
  • metadata 与包内 tag 之间转换。

QinQ 可能有多层 tag,单个 vlan_tci 只表达当前 offload metadata 层,其余仍可能在 bytes 中。

四、隧道封装布局

以 VXLAN 为例:

1
2
3
4
5
6
7
8
outer Ethernet
outer IP
outer UDP
VXLAN
inner Ethernet
inner IP
inner TCP/UDP
payload

skb 需要同时保存:

  • outer MAC/network/transport header;
  • inner MAC/network/transport header;
  • outer protocol 与 inner protocol;
  • encapsulation=1
  • GSO tunnel type;
  • outer/inner checksum 状态;
  • dst/tunnel metadata。

五、inner header helper

skb_reset/set_inner_mac_header()、network、transport helper 位于 kernel-5.10/include/linux/skbuff.h:2480-2553 附近。

典型封装前:

1
2
3
4
原 mac/network/transport → 保存为 inner_*
skb_push outer headers
重设 outer mac/network/transport
encapsulation = 1

由于 offset 相对 head,push data 不会自动破坏已保存 inner header 地址,但 expand head 后必须由 helper/复制逻辑保持 offset。

六、headroom 与 COW

封装向前添加几十字节 outer header,必须:

  1. 计算 LL_RESERVED_SPACE、隧道 header 等需求;
  2. skb_cow_head() 确保 head 可写且空间足;
  3. 重新获取旧 header 指针;
  4. push 并填写 outer header;
  5. 更新 len/protocol/checksum/GSO。

clone 的 skb 若直接写共享 head,会同时破坏其它路径。

七、VXLAN/Geneve/GRE 代表路径

  • VXLAN 实现在 kernel-5.10/drivers/net/vxlan/vxlan_core.c
  • Geneve 在 kernel-5.10/drivers/net/geneve.c
  • GRE 在 kernel-5.10/net/ipv4/ip_gre.c

它们差异在 tunnel header、metadata/options 和 checksum,但 skb 层共同模式是:保存 inner header → COW/headroom → push outer → 设置 encapsulation/GSO → route/output。

八、隧道 GSO

一个大 inner TCP skb 可先封装再由网卡执行 tunnel TSO。gso_type 必须说明:

  • inner TCP/UDP 类型;
  • outer UDP tunnel/GRE;
  • outer checksum 是否需要;
  • partial GSO 等。

设备不支持时,软件可能在正确层次分段。错误的 inner header offset 会让分段器复制/修改错误 header。

九、解封装

RX 隧道 handler:

  1. 验证 outer header/checksum;
  2. pull outer header;
  3. 恢复/重设 inner MAC/network/transport 为当前层;
  4. 更新 dev/protocol/hash/checksum
  5. 清理或转换 tunnel metadata;
  6. 重新进入 GRO/RX stack。

outer checksum 已验证不自动等于 inner checksum 已验证;csum_level 表达层级。

十、字段变化表

操作 data/headroom header offset metadata
VLAN hw RX bytes 中 tag 被剥离 L2 视图已调整 vlan_tci/proto present
VLAN push data 向前/内容移动 MAC/network 调整 metadata 可能清除
tunnel encap push outer headers 原 header 保存为 inner encapsulation/GSO/dst
tunnel decap pull outer inner 提升为当前 dev/protocol/csum 更新

十一、常见误区

  1. vlan_present 时 packet bytes 不一定含 VLAN header。
  2. inner header 不是通过扫描自动得出,封装代码必须正确设置。
  3. encapsulation 不只是一个 flag,还依赖 inner protocol/GSO/checksum。
  4. push outer header 前必须 COW,不能只检查 headroom。
  5. 解封装后 hash/checksum/dev 可能需要重新评估。
  6. 隧道 GSO 支持取决于目标设备 feature,不是设置 gso_type 就一定可硬件执行。

字段与所有权统一核对表

对象或字段 主要修改者/使用者 所有权与释放要点
outer/inner header offsets 隧道封装/解封 同时定位两层协议头
encapsulation/inner_protocol GSO/checksum 告诉 offload 如何解析内层
VLAN metadata 硬件剥离/插入 tag 可在 skb metadata 或报文数据中

最小调用链

1
2
3
4
5
原始 skb
→ VLAN metadata 或 push outer header
→ 设置 inner/outer offsets 与 encapsulation
→ GSO/checksum 处理
→ 解封装恢复内层视图

2. IP 分片、重组与 frag_list

IP 分片、重组与 frag_list

结论摘要

IP 分片是网络协议语义,skb frags[] 是内存布局,两者不是同一概念。输出分片把一个 IP datagram 变为多个独立 skb,每个有自己的 IP header;接收重组把多个 fragment skb 排入 fragment queue,验证 offset/overlap/长度后生成一个重组 skb,可能通过 frag_list/非线性布局避免全量复制。

一、不要混淆两种 frag

1
2
3
4
5
skb frags[]:
一个 skb 内部由多个 page 片段组成

IP fragments:
多个独立 IP packet/skb,共同组成一个 datagram

一个 IP fragment skb 自己仍可拥有多个 frags[];重组后的 skb 也可能非线性。

二、IPv4 输出分片入口

ip_fragment() 位于 kernel-5.10/net/ipv4/ip_output.c:582。触发条件通常是:

  • skb 长度超过输出 MTU;
  • DF 未禁止分片或本地策略允许;
  • GSO/设备 feature 不能在更后面解决;
  • PMTU/协议规则允许。

输出产生多个 skb,每个包含:

  • 复制并调整的 IP header;
  • fragment offset/MF flag;
  • 独立 total length/checksum;
  • datagram ID;
  • 对应 payload slice。

三、fast path 与 slow path

若原 skb 已按合适 frag_list 组织、各子 skb headroom/长度满足要求,分片器可较少复制地重用/调整 skb 链。否则需要:

  • 分配新 skb;
  • 复制 header;
  • skb_copy_bits() 从原逻辑数据提取每片 payload;
  • 维护 page ref、dst、metadata;
  • 逐片调用 output。

分片后每个 skb 的 owner 独立,某一片输出失败时剩余链必须释放。

四、IPv4 接收重组

ip_defrag() 位于 kernel-5.10/net/ipv4/ip_fragment.c:475。fragment key 通常包括:

  • src/dst;
  • IP ID;
  • protocol;
  • user/zone 等上下文。

每片进入 queue:

  1. 校验 offset、MF、长度和边界;
  2. 按 offset 插入树/队列;
  3. 处理 overlap/duplicate;
  4. 更新已收到范围和总长度;
  5. 未完整则 queue 持有 skb;
  6. 完整后 ip_frag_reasm() 生成重组 skb。

queue timeout、内存阈值、overlap policy 或非法片都会释放相应 skb。

五、重组数据布局

重组不必把所有 payload memcpy 到一块大 linear buffer。常见结果:

  • 第一片 skb 成为 head;
  • 必要 header 保持 linear;
  • 后续数据通过 frags/frag_list 连接;
  • 调整 len/data_len/truesize
  • 清除 fragment flag、重算 header;
  • 后续协议使用 pskb_may_pull() 获取所需连续 header。

这也是 frag_list 能表达“多个完整子 skb 组成一个逻辑包”的典型用途。

六、IPv6 差异

IPv6 路由器不进行中途分片,源主机通过 Fragment extension header 分片。接收重组实现位于 kernel-5.10/net/ipv6/reassembly.c

差异包括:

  • fragment header 格式和位置;
  • atomic fragment 处理;
  • extension header 解析;
  • overlap 安全规则;
  • ICMPv6/PMTU 语义。

skb queue、所有权、重组非线性布局的基本问题仍相似。

七、Netfilter defrag

conntrack/NAT 需要完整 L4 header,因此 Netfilter 可在 PRE_ROUTING/LOCAL_OUT 早期 defrag。重组后的包完成 conntrack/NAT 后,输出可能根据 MTU 再次分片。

不同 ip_defrag user 值区分调用场景,避免错误复用 fragment queue 上下文。

八、GRO frag_list 与 IP frag_list

两者都可能使用 frag_list,但语义不同:

  • GRO:多个可聚合的同 flow packet 被当作大 skb 批处理,GSO metadata 保留 segment 边界;
  • IP reassembly:多个 IP fragments 组成原始 datagram,重组后对 L4 表现为一个 IP 包。

不能仅看到 frag_list 就判断来源。

九、所有权时间线

1
2
3
4
5
6
fragment skb arrives
→ defrag queue owns skb
├─ incomplete: retained until more fragments/timeout
├─ invalid/timeout: queue frees
└─ complete: refs/data transferred into reassembled skb
→ L4 stack owns reassembled skb

输出方向原 skb 被分片器消费,产生的新 skb 链分别交 output;调用者需遵守 ip_fragment() 的消费合同。

十、资源与安全

fragment queue 容易被攻击消耗内存。内核使用:

  • 高低阈值;
  • timeout;
  • LRU/eviction;
  • 最大 datagram 长度;
  • overlap/duplicate 检查;
  • hash/secret;
  • per-netns accounting。

truesize 而非单纯 payload 长度更接近真实资源消耗。

十一、常见误区

  1. skb_frag_t 与 IP fragment 不是一个层次。
  2. 重组完成不代表数据一定全 linear。
  3. 分片器可能消费原 skb,不能按普通只读函数理解。
  4. IPv6 路由器不会像 IPv4 一样中途分片。
  5. GRO 聚合与 IP 重组的 frag_list 语义不同。
  6. fragment queue 必须计时和限内存,否则可被 DoS。

字段与所有权统一核对表

对象或字段 主要修改者/使用者 所有权与释放要点
frag_off/len IP 分片 每个片 skb 独立持有 data/page
fragment queue defrag 队列在重组前持有所有片 skb
frag_list/data_len 重组结果 首 skb 持有后续片链并统一释放

最小调用链

1
2
3
4
5
大 IP skb output 分片或网络接收 fragments
→ fragment queue 持有各片
→ 完整性检查/重组
→ frag_list/nonlinear skb
→ 后续协议处理与统一释放

从局部机制回到完整路径

完整路径关系

前面的局部机制只有放回完整生命周期才不容易误判:数据布局决定 helper 能否直接访问;引用计数决定能否原地修改;队列和驱动返回值决定谁仍然拥有 skb。遇到异常路径时,应按“外部引用 → 数据区 → shell”的逆序检查回收。

源码阅读路线

建议不要从 struct sk_buff 声明开始逐字段背诵,而应使用下面的阅读顺序:

  1. 先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造;
  2. 沿成功主线记录 data/len/data_len、header offset、checksum、hash、queue mapping 等字段变化;
  3. 再回到每个失败分支,确认 skb、page、DMA、socket 和记账引用是否回滚;
  4. 最后对照 5.10、6.1 或 BSP 的同名 symbol,区分语义变化、性能优化和 vendor backport。

本篇总结

  1. VLAN tag 可能在报文字节中,也可能只存在 skb metadata 中。
  2. 隧道 skb 同时保存 outer/inner header offset,offload 依赖这些元数据理解内层协议。
  3. 分片产生多个独立 skb,重组队列必须持有并在超时/错误时释放全部片段。
  4. 重组结果常用非线性布局或 frag_list,不能假设重新拼成一块连续内存。

sk_buff 的难点从来不只是字段多,而是同一个对象不断改变数据视图、元数据状态和持有者。只要把“布局、字段、所有权、释放”四条线同时画出来,绝大多数网络栈源码都会从零散函数变成可推导的生命周期。

系列目录

  1. Linux sk_buff 深度解析(一):数据结构、四指针与核心字段
  2. Linux sk_buff 深度解析(二):非线性数据、引用计数与生命周期
  3. Linux sk_buff 深度解析(三):从网卡 DMA 到 Socket 的 RX 路径
  4. Linux sk_buff 深度解析(四):从 sendmsg 到 TX completion
  5. Linux sk_buff 深度解析(五):GRO、GSO、Checksum 与多核流调度
  6. Linux sk_buff 深度解析(六):Netfilter、tc、eBPF 与 XDP
  7. Linux sk_buff 深度解析(七):VLAN、隧道、分片与重组(本文)
  8. Linux sk_buff 深度解析(八):Socket 内存、零拷贝、丢包与版本演进