3、网络基础回顾:OSI与TCP/IP模型、以太网帧、IP、TCP/UDP、ICMP、ARP协议要点,以及它们在内核中的对应实现
在深入Linux内核网络协议栈的源码与数据路径之前,必须先建立一套完整的“协议—内核模块”映射认知。网络协议是分层的抽象约定,而内核协议栈则是这些约定的具体工程实现。理解OSI与TCP/IP模型的差异,掌握以太网帧、IP、TCP/UDP、ICMP、ARP的协议要点,并明确它们在Linux内核中对应的代码模块(如net/ipv4、net/ethernet、net/core),是后续分析收包路径、NAT、路由、连接跟踪等高级主题的基石。本章将梳理这些基础协议的核心字段与工作机制,并逐一对应到内核源码的关键文件与数据结构,帮助读者建立从“抓包报文”到“内核代码”的直觉。
上图展示了OSI模型、TCP/IP模型与Linux内核模块之间的粗略对应关系。在实际内核实现中,并不存在严格的“会话层”或“表示层”代码,这些功能由应用层库或系统调用接口承担。内核关注的焦点是从传输层到网络接口层的收发路径。理解这张映射图,是阅读内核网络源码的第一步。
3.1 核心原理:分层模型与内核模块的对应关系
OSI七层模型是理论参考,TCP/IP四层模型是事实标准,而Linux内核的实现则更贴近TCP/IP模型,但又有其独特的优化。在内核中,每个协议层并非一个独立的进程,而是一组操作函数指针和数据结构,它们通过struct sk_buff(套接字缓冲区)进行数据传递。
- 应用层:对应系统调用接口(如
sendto、recvfrom)和Socket抽象层(net/socket.c)。 - 传输层:对应
net/ipv4/tcp.c、net/ipv4/udp.c,实现端口寻址、流量控制、可靠传输(TCP)或尽力而为(UDP)。 - 网络层:对应
net/ipv4/ip_input.c、ip_output.c,处理IP地址、路由查找、分片与重组。 - 网络接口层:对应
net/ethernet/eth.c和net/core/dev.c,负责以太网帧的封装与解析,以及驱动接口(drivers/net/)。
内核通过struct packet_type注册表将网络层协议(如IP、ARP)与对应的处理函数绑定。当网卡驱动收到一个数据帧时,netif_receive_skb()会根据帧头中的以太网类型字段(EtherType)查找匹配的packet_type,从而分发到IP或ARP的接收例程。
关键数据结构:struct sk_buff 是贯穿整个协议栈的核心。它包含指向数据缓冲区的指针,以及一组“头指针”偏移量:mac_header(链路层头)、network_header(网络层头)、transport_header(传输层头)。这种设计避免了数据拷贝,各层只需移动指针即可解析或添加协议头。
3.2 以太网帧与内核实现
以太网帧是数据链路层的封装单位。标准以太网帧(Ethernet II)格式如下:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 目的MAC地址 | 6 | 接收方物理地址 |
| 源MAC地址 | 6 | 发送方物理地址 |
| EtherType | 2 | 上层协议类型(0x0800为IPv4,0x0806为ARP) |
| Payload | 46-1500 | 上层数据(IP包或ARP报文) |
| FCS | 4 | 帧校验序列(CRC32),通常由硬件计算 |
在Linux内核中,以太网帧的解析与构造主要在net/ethernet/eth.c中完成。关键函数包括eth_type_trans()(接收路径,用于识别EtherType并设置skb->protocol)和eth_header()(发送路径,用于填充MAC头)。
// 接收路径:识别协议类型
__be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev) {
struct ethhdr *eth = eth_hdr(skb);
// ... 检查VLAN标签 ...
skb->protocol = eth->h_proto; // 直接读取EtherType字段
return skb->protocol;
}
值得注意的是,内核并不校验FCS字段,因为该校验由网卡硬件完成。如果硬件检测到CRC错误,会丢弃该帧并递增rx_crc_errors统计计数,软件层通常看不到坏帧。
3.3 IP协议要点与内核路由
IP协议(IPv4)是网络层的核心,提供无连接、不可靠的尽力而为交付。IP头部关键字段包括:版本(IPv4)、首部长度(IHL)、总长度、标识符(用于分片)、标志位(DF/MF)、片偏移、TTL(生存时间)、协议号(TCP=6,UDP=17,ICMP=1)、源/目的IP地址。
内核中IP层的入口是ip_rcv()(接收)和ip_output()(发送)。核心逻辑包括:
- 路由查找:通过
ip_route_input()或ip_route_output_flow()查询FIB(转发信息库),决定是本地交付、转发还是丢弃。 - 分片与重组:发送时若报文超过MTU,调用
ip_fragment();接收时由ip_defrag()完成重组。 - TTL处理:每经过一个路由器TTL减1,若TTL为0则丢弃并发送ICMP超时消息。
经验提醒:在排查IP分片问题时,使用ping -s 3000命令可以强制产生分片。观察/proc/net/snmp中的ReasmFails和FragFails计数,可以快速判断是否因MTU不一致导致分片失败。
3.4 TCP与UDP协议要点及内核实现
传输层提供端到端的通信服务。TCP(传输控制协议)提供面向连接的可靠字节流服务,UDP(用户数据报协议)提供无连接的数据报服务。
| 特性 | TCP | UDP |
|---|---|---|
| 连接状态 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 确认、重传、排序 | 尽力而为,不保证送达 |
| 头部开销 | 20-60字节 | 8字节 |
| 内核模块 | net/ipv4/tcp.c, tcp_input.c, tcp_output.c | net/ipv4/udp.c |
| 主要数据结构 | struct tcp_sock | struct udp_sock |
TCP在内核中的实现极为复杂,涉及状态机(tcp_rcv_state_process())、拥塞控制(tcp_cong.c)、滑动窗口(tcp_output.c)等。UDP则相对简单,核心函数是udp_rcv()和udp_sendmsg(),通过四元组(源IP、源端口、目的IP、目的端口)查找对应的Socket。
// UDP接收路径简化逻辑
int udp_rcv(struct sk_buff *skb) {
struct udphdr *uh = udp_hdr(skb);
// 根据目的端口查找socket
struct sock *sk = udp_lookup(skb, uh->dest, ...);
if (sk == NULL) {
// 端口不可达,发送ICMP错误
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
return 0;
}
// 将数据放入socket接收队列
udp_queue_rcv_skb(sk, skb);
return 0;
}
3.5 ICMP与ARP协议要点
ICMP(互联网控制消息协议)用于在IP网络中传递错误报告和诊断信息。常见类型包括:回显请求/应答(Type 8/0,即ping)、目的不可达(Type 3)、超时(Type 11)。内核中ICMP的实现位于net/ipv4/icmp.c,核心函数是icmp_rcv()和icmp_send()。
ARP(地址解析协议)用于将IP地址解析为MAC地址。其报文格式包含硬件类型、协议类型、操作码(请求=1,应答=2)以及四组地址字段。内核中ARP的实现位于net/ipv4/arp.c,核心数据结构是ARP缓存(struct neighbour)。
风险提示:ARP协议无认证机制,极易受到ARP欺骗攻击。在内核中可以通过设置/proc/sys/net/ipv4/conf/all/arp_ignore和arp_announce参数来增强防御,但根本解决方案是部署动态ARP检测或使用IPsec。
3.6 案例拆解:一次完整的ping包收发流程
为了将上述协议要点串联起来,我们以执行ping 192.168.1.1为例,拆解数据包在内核中的完整旅程。该案例有助于理解各层协议如何协作,以及内核模块的调用顺序。
背景与目标:主机A(IP: 192.168.1.10, MAC: AA:BB:CC:DD:EE:01)需要探测网关(IP: 192.168.1.1)的连通性。初始状态:ARP缓存为空。
操作步骤与内核行为:
- 构造ICMP请求:ping程序调用
socket(AF_INET, SOCK_RAW, IPPROTO_ICMP),内核在icmp_send()中构造ICMP回显请求报文。 - IP层封装:IP层调用
ip_output(),添加IP头(TTL=64,协议=1,目的IP=192.168.1.1)。此时需要路由查找,发现目的IP在同一子网,下一跳为直连。 - ARP解析:在发送前,邻居子系统(
net/core/neighbour.c)查找ARP缓存,未命中。内核发送ARP请求(广播),等待网关应答。 - ARP应答:网关回复ARP应答(单播),内核更新ARP缓存,获得网关MAC地址。
- 以太网帧封装:
eth_header()填充目的MAC为网关MAC,源MAC为主机A MAC,EtherType=0x0800。 - 驱动发送:
dev_queue_xmit()将sk_buff传递给网卡驱动,通过DMA发送到物理链路。 - 接收应答:网关回复ICMP应答,网卡接收后,
eth_type_trans()识别EtherType,ip_rcv()处理IP头,icmp_rcv()匹配对应的ping进程,最终通过Socket返回给用户态。
结果与复盘:整个过程在微秒级完成。如果ARP缓存未命中,首次ping会有额外延迟。通过strace跟踪sendto和recvfrom系统调用,可以观察到内核态与用户态的数据交换边界。
复盘要点:该案例展示了sk_buff如何在不同层间传递。每层只修改自己的头部指针,不拷贝数据。理解这一流程后,再看tcpdump抓包结果,就能将每个报文与内核代码路径对应起来。
3.7 常见误区与总结
在学习网络协议栈时,初学者常陷入以下误区:
- 误区一:认为OSI模型与内核实现一一对应。实际上内核没有独立的会话层和表示层,这些功能由应用层库(如SSL库)实现。
- 误区二:混淆“校验和”的承担者。以太网FCS由硬件计算,IP头校验和由软件计算,TCP/UDP校验和由软件计算(但可卸载到硬件)。
- 误区三:忽视
sk_buff的头部指针。直接操作skb->data而不使用skb_push/skb_pull等API,会导致数据错乱。 - 误区四:认为ARP只在局域网使用。当数据包需要跨网关转发时,源主机的ARP解析的是网关MAC,而非最终目的主机的MAC。
总结而言,本章建立的协议—内核映射关系是后续所有分析的基础。读者应重点掌握sk_buff的运作方式,以及eth_type_trans、ip_rcv、tcp_v4_rcv、udp_rcv、arp_process这几个关键函数的调用位置。下一阶段的学习中,无论是分析Netfilter钩子、流量控制还是驱动开发,都需要基于本章的协议框架进行。