Linux sk_buff 深度解析(七):VLAN、隧道、分片与重组
写在前面
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 或开发板实验写成已验证结论。
阅读前先建立四个问题
阅读任何 skb 代码路径,都建议反复追问:
- 当前
skb->data指向哪一层协议头,linear 与 non-linear 数据分别在哪里? - 哪些字段刚被修改,下一层为什么依赖这些字段?
- 当前谁持有 skb shell、head、frag page 以及 socket/dst/conntrack 等外部引用?
- 成功、失败、重试、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 | outer Ethernet |
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 | 原 mac/network/transport → 保存为 inner_* |
由于 offset 相对 head,push data 不会自动破坏已保存 inner header 地址,但 expand head 后必须由 helper/复制逻辑保持 offset。
六、headroom 与 COW
封装向前添加几十字节 outer header,必须:
- 计算
LL_RESERVED_SPACE、隧道 header 等需求; skb_cow_head()确保 head 可写且空间足;- 重新获取旧 header 指针;
- push 并填写 outer header;
- 更新 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:
- 验证 outer header/checksum;
- pull outer header;
- 恢复/重设 inner MAC/network/transport 为当前层;
- 更新
dev/protocol/hash/checksum; - 清理或转换 tunnel metadata;
- 重新进入 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 更新 |
十一、常见误区
vlan_present时 packet bytes 不一定含 VLAN header。- inner header 不是通过扫描自动得出,封装代码必须正确设置。
- encapsulation 不只是一个 flag,还依赖 inner protocol/GSO/checksum。
- push outer header 前必须 COW,不能只检查 headroom。
- 解封装后 hash/checksum/dev 可能需要重新评估。
- 隧道 GSO 支持取决于目标设备 feature,不是设置 gso_type 就一定可硬件执行。
字段与所有权统一核对表
| 对象或字段 | 主要修改者/使用者 | 所有权与释放要点 |
|---|---|---|
| outer/inner header offsets | 隧道封装/解封 | 同时定位两层协议头 |
encapsulation/inner_protocol |
GSO/checksum | 告诉 offload 如何解析内层 |
| VLAN metadata | 硬件剥离/插入 | tag 可在 skb metadata 或报文数据中 |
最小调用链
1 | 原始 skb |
2. IP 分片、重组与 frag_list
结论摘要
IP 分片是网络协议语义,skb frags[] 是内存布局,两者不是同一概念。输出分片把一个 IP datagram 变为多个独立 skb,每个有自己的 IP header;接收重组把多个 fragment skb 排入 fragment queue,验证 offset/overlap/长度后生成一个重组 skb,可能通过 frag_list/非线性布局避免全量复制。
一、不要混淆两种 frag
1 | skb frags[]: |
一个 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:
- 校验 offset、MF、长度和边界;
- 按 offset 插入树/队列;
- 处理 overlap/duplicate;
- 更新已收到范围和总长度;
- 未完整则 queue 持有 skb;
- 完整后
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 | fragment skb arrives |
输出方向原 skb 被分片器消费,产生的新 skb 链分别交 output;调用者需遵守 ip_fragment() 的消费合同。
十、资源与安全
fragment queue 容易被攻击消耗内存。内核使用:
- 高低阈值;
- timeout;
- LRU/eviction;
- 最大 datagram 长度;
- overlap/duplicate 检查;
- hash/secret;
- per-netns accounting。
truesize 而非单纯 payload 长度更接近真实资源消耗。
十一、常见误区
skb_frag_t与 IP fragment 不是一个层次。- 重组完成不代表数据一定全 linear。
- 分片器可能消费原 skb,不能按普通只读函数理解。
- IPv6 路由器不会像 IPv4 一样中途分片。
- GRO 聚合与 IP 重组的 frag_list 语义不同。
- fragment queue 必须计时和限内存,否则可被 DoS。
字段与所有权统一核对表
| 对象或字段 | 主要修改者/使用者 | 所有权与释放要点 |
|---|---|---|
frag_off/len |
IP 分片 | 每个片 skb 独立持有 data/page |
| fragment queue | defrag | 队列在重组前持有所有片 skb |
frag_list/data_len |
重组结果 | 首 skb 持有后续片链并统一释放 |
最小调用链
1 | 大 IP skb output 分片或网络接收 fragments |
从局部机制回到完整路径
前面的局部机制只有放回完整生命周期才不容易误判:数据布局决定 helper 能否直接访问;引用计数决定能否原地修改;队列和驱动返回值决定谁仍然拥有 skb。遇到异常路径时,应按“外部引用 → 数据区 → shell”的逆序检查回收。
源码阅读路线
建议不要从 struct sk_buff 声明开始逐字段背诵,而应使用下面的阅读顺序:
- 先在入口函数确认 skb 是新分配、clone、队列取出还是从驱动 buffer 构造;
- 沿成功主线记录
data/len/data_len、header offset、checksum、hash、queue mapping 等字段变化; - 再回到每个失败分支,确认 skb、page、DMA、socket 和记账引用是否回滚;
- 最后对照 5.10、6.1 或 BSP 的同名 symbol,区分语义变化、性能优化和 vendor backport。
本篇总结
- VLAN tag 可能在报文字节中,也可能只存在 skb metadata 中。
- 隧道 skb 同时保存 outer/inner header offset,offload 依赖这些元数据理解内层协议。
- 分片产生多个独立 skb,重组队列必须持有并在超时/错误时释放全部片段。
- 重组结果常用非线性布局或
frag_list,不能假设重新拼成一块连续内存。
sk_buff 的难点从来不只是字段多,而是同一个对象不断改变数据视图、元数据状态和持有者。只要把“布局、字段、所有权、释放”四条线同时画出来,绝大多数网络栈源码都会从零散函数变成可推导的生命周期。
系列目录
- Linux sk_buff 深度解析(一):数据结构、四指针与核心字段
- Linux sk_buff 深度解析(二):非线性数据、引用计数与生命周期
- Linux sk_buff 深度解析(三):从网卡 DMA 到 Socket 的 RX 路径
- Linux sk_buff 深度解析(四):从 sendmsg 到 TX completion
- Linux sk_buff 深度解析(五):GRO、GSO、Checksum 与多核流调度
- Linux sk_buff 深度解析(六):Netfilter、tc、eBPF 与 XDP
- Linux sk_buff 深度解析(七):VLAN、隧道、分片与重组(本文)
- Linux sk_buff 深度解析(八):Socket 内存、零拷贝、丢包与版本演进
