网络访问层仍受到传输介质的性质以及相关适配器的设备驱动程序的影响很大。网络层与网络适配器的硬件性质几乎完全分离。为什么说几乎?因为该层不仅负责发送和接收数据,还负责在彼此不直接连接的系统之间转发和路由分组。查找最佳路由并选择适当的网络设备来发送分组,也涉及对底层地址族的处理(如特定硬件MAC地址)。
网络访问层(数据链路 + 物理层)直接受限于传输介质(如光纤、双绞线)和硬件驱动程序(如网卡驱动)。
网络层理论上独立于硬件,但实际需要处理路由、地址解析等功能,这些功能间接依赖硬件地址(如 MAC 地址)。
路由决策需考虑硬件限制
- 例如:路由器选择路径时需考虑链路带宽、MTU(最大传输单元)等物理层属性。
地址解析依赖硬件地址
- 网络层使用 IP 地址,但实际传输需通过 ARP(地址解析协议)将 IP 映射到 MAC 地址(硬件地址)。
- 例子:当主机 A(IP: 192.168.1.1,MAC: 00:01)要向主机 B(IP: 192.168.1.2)发送数据时,需先通过 ARP 获取主机 B 的 MAC 地址。
特定硬件功能的利用
- 某些网络层协议(如 IPv6 邻居发现)依赖 ICMPv6 消息,而 ICMPv6 的传输仍需通过数据链路层。
1.ipv4的首部
其中各字段如下:
1. 版本(Version)
- 定义:4 位字段(之所以不是3位,是为了未来版本以及字段对齐),标识 IP 协议版本,目前常用值为
4
(IPv4)或6
(IPv6)。- 例子:家庭宽带路由器分配的设备 IP 地址若为
192.168.1.100
,属于 IPv4 协议,该字段值为4
;若设备使用 IPv6 地址2001:db8::1
,则版本字段值为6
。2. IP 首部长度(IHL,Internet Header Length)
- 定义:4 位字段,以 32 位(4 字节)为单位表示首部长度。无选项时,标准 IP 首部为 20 字节,对应 IHL 值为
5
(5×4 字节 = 20 字节);若有选项,需重新计算。ip首部最小20字节,最大60字节。- 例子:某 IP 分组首部含 20 字节固定部分 + 4 字节选项,总长度 24 字节,IHL 值为
6
(6×4 字节 = 24 字节)。3. 代码点 / 服务类型(DS,Differentiated Services)
- 定义:8 位字段,前 6 位为区分服务代码点(DSCP),用于标记流量优先级;后 2 位保留。例如视频会议、语音通话等实时业务可设置高优先级。
- 例子:视频直播流量为确保流畅,网络设备可将其 DS 字段设置为
46
(二进制101110
),标记为 “快速转发” 类服务。4. 总长度(Total Length)
- 定义:16 位字段,标识 IP 分组整体长度(首部 + 数据部分),单位为字节,最大值 65535 字节。
- 例子:IP 首部 20 字节,携带 1000 字节网页数据,总长度字段值为
1020
(20 + 1000 = 1020)。5. 分片标识(Identification)
- 定义:16 位字段,发送方为每个 IP 分组分配唯一标识,用于分片重组。同一原始分组的所有分片共享相同标识。
- 例子:发送一个 3000 字节的文件,因链路 MTU 限制需分片。所有分片的 “分片标识” 字段均为发送方生成的同一值(如
12345
),接收方通过该标识识别属于同一分组的分片。6. 标志(Flags)
- 定义:3 位字段,常用标志位:
- 第 1 位(保留);
- 第 2 位(DF,Don’t Fragment):置 1 表示禁止分片;
- 第 3 位(MF,More Fragments):置 1 表示后续还有分片。
- 例子:某视频流数据要求不被分片,发送方将标志字段设为
010
(二进制),其中 DF 位为 1,网络设备若无法传输则丢弃该分组并反馈错误。7. 分片偏移量(Fragment Offset)
- 定义:13 位字段,标识分片在原始分组中的位置,单位为 8 字节块。接收方通过该值重组分片。
- 例子:某分片携带的数据从原始分组第 1000 字节开始,分片偏移量为
125
(1000 ÷ 8 = 125),表示该分片在重组时应放在 125×8 = 1000 字节的位置。8. TTL(生存时间,Time to Live)
- 定义:8 位字段,限制 IP 分组转发跳数。每经一个路由器,TTL 减 1;减至 0 时,分组被丢弃。
- 例子:主机发送 TTL 为
64
的分组,经过 10 个路由器后,TTL 变为54
;若第 64 跳仍未到达目标,分组会被第 64 个路由器丢弃。9. 协议(Protocol)
- 定义:8 位字段,标识 IP 分组承载的上层协议。常见值:
6
(TCP)、17
(UDP)、1
(ICMP)。- 例子:访问网页时,浏览器通过 TCP 协议传输数据,IP 首部协议字段值为
6
;使用ping
命令时,基于 ICMP 协议,协议字段值为1
。10. 首部检验和(Header Checksum)
- 定义:16 位字段,通过计算 IP 首部二进制反码和校验首部传输错误,不校验数据部分。
- 例子:发送方计算首部字节的反码和,存入该字段;接收方重新计算,若结果与字段值不符,说明首部在传输中出错,丢弃该分组。
11. 源地址、目标地址
- 定义:各 32 位字段,分别标识发送方和接收方的 IP 地址。
- 例子:用户电脑(IP:
192.168.1.100
)向服务器(IP:114.114.114.114
)发送请求,IP 首部源地址填192.168.1.100
,目标地址填114.114.114.114
。12. 选项(Options)
- 定义:可变长字段(最长 40 字节),用于扩展功能,如记录路由、时间戳、源路由等。
- 例子:网络调试时,使用 “记录路由” 选项,让经过的路由器记录自身 IP,帮助分析路径。
13. 填充(Padding)
- 定义:若干 0 比特,确保 IP 首部总长度是 32 位(4 字节)的整数倍,满足首部长度对齐要求。
- 例子:若选项长度使首部总长度为 22 字节,需添加 2 字节填充(补 0),使首部长度变为 24 字节(6×4 字节)。
2.在Linux源码中ip首部由iphdr数据结构进行实现
version
:对应 IP 首部的 “版本” 字段,标识 IPv4 协议(值固定为 4)。ihl
:对应 “IP 首部长度(IHL)”,以 4 字节为单位表示首部长度。tos
:对应 “代码点 / 服务类型”,用于标记流量优先级等服务类型。tot_len
:对应 “总长度”,表示 IP 分组(首部 + 数据)的总字节数。id
:对应 “分片标识”,用于分片后重组数据。frag_off
:融合 “标志” 和 “分片偏移量” 功能,既包含分片标志(如是否允许分片),也记录分片偏移位置。ttl
:对应 “生存时间”,限制 IP 分组的转发跳数。protocol
:对应 “协议” 字段,标识上层协议(如 TCP、UDP)。check
:对应 “首部检验和”,用于校验 IP 首部传输是否出错。saddr
/daddr
:分别对应 “源地址” 和 “目标地址”,存储 32 位 IPv4 地址。
3.分组穿过网络层的路线
ip_rcv是网络层的入口点,分组向上穿过内核的过程如下:
一、分组接收路径
底层接收
分组通过物理网络接口(如以太网)进入主机,借助 轮询机制(检测是否有数据到达),首先进入网络层的分组接收函数ip_rcv
,这是网络层处理接收分组的入口。预处理钩子(NF_IP_PRE_ROUTING)
分组进入ip_rcv
后,触发 Netfilter 的 NF_IP_PRE_ROUTING 钩子。此处可对分组进行过滤、修改(如防火墙规则检查),是网络层接收分组后的第一处自定义处理点。路由决策
经过预处理的分组进入 路由模块,核心逻辑是判断分组的目标地址:
- 本地交付:若目标地址是本机 IP,分组进入
ip_local_deliver
,将分组提交给 传输层(TCP/UDP)处理。- 转发:若目标地址非本机 IP,分组需转发。此时触发 Netfilter 的 NF_IP_FORWARD 钩子,再次进行过滤(如检查是否允许转发),然后进入转发流程。
二、分组发送路径
传输层到网络层的发送
传输层(TCP/UDP)生成的分组进入网络层发送函数ip_queue_xmit
,这里负责管理发送队列,为分组添加 IP 首部等操作。输出预处理钩子(NF_IP_LOCAL_OUT)
ip_queue_xmit
触发 Netfilter 的 NF_IP_LOCAL_OUT 钩子,用于本地生成的分组(非转发分组)的过滤与修改(如标记特殊流量)。路由与输出处理
- 分组经过 路由模块,确定从哪个网络接口发送(路由表查询)。
- 进入
ip_output
,完成分组的最终封装(如分片处理),准备发送。底层发送
最终,分组通过dev_queue_xmit
交给物理网络接口(如以太网),完成从网络层到数据链路层的传递,实现物理发送。
完整流程示例:本地主机发送HTTP请求
-
应用层:浏览器调用
write()
发送HTTP请求。 -
传输层:内核TCP模块封装数据,生成TCP段。
-
LOCAL_OUT钩子:iptables检查OUTPUT链规则。
-
路由决策:查询路由表确定出口网卡(如eth0)。
-
POST_ROUTING钩子:应用SNAT规则,修改源IP为公网地址。
-
网络接口层:添加以太网头部,通过
dev_queue_xmit
发送到eth0。 -
物理链路:网卡将数据帧转换为电信号发送到路由器。
一、接收分组以及分组转发
1.接收分组
在分组转发到 ip_rcv 之后,必须检查接收到的信息确保它是正确的。主要检查计算的校验和与首部中存储的校验和是否一致。其他的检查包括分组是否达到了 IP 首部的最小长度,分组的协议是否确实是 IPv4。
在进行检查之后,内核并不立即继续对分组的处理,而是调用一个 netfilter 挂钩,使得用户空间可以对分组数据进行操作。netfilter 挂钩插入到内核源代码中定义好的各个位置,使得分组能够被外部动态操作。
ip_rcv()函数入口---->调用一个netfilter挂钩---->ip_route_input()负责选择路由(判断路由的结果是选择一个函数进行下一步分组处理)---->选择可用的函数ip_local_deliver()和ip_forward()。具体选择哪个函数,取决于分组时交付到本地计算机下一个更高协议例程还是转发到网络中的另一个主机或网络设备。
数据报在Linux内核中netfilter处理过程,有不同HOOK点(挂钩):
1. 本地产生数据且目标为本地的流程
- 来源与目标:数据由本地应用层产生(如本地运行的网络调试工具发送测试数据),目标地址为本机 IP。
- 处理逻辑:
- 应用层 → 传输层:应用层数据交给传输层(TCP 或 UDP),添加传输层首部(如端口号)。
- 传输层 → 网络层:传输层数据传递到网络层,网络层添加 IP 首部(源 IP 为本机,目标 IP 也为本机),生成 IP 数据报。
- 网络层处理:
- 触发 NF_IP_LOCAL_OUT 钩子(如标记本地流量)。
- 路由选择判定为本地交付,直接将数据报传递给上层协议(传输层),无需经过 NF_IP_POST_ROUTING 钩子。
2. 本地产生数据但需转发出去的流程
- 来源与目标:数据由本地应用层产生(如本地代理工具转发外部请求),但目标地址非本机 IP,需转发到其他网络设备。
- 处理逻辑:
- 应用层 → 传输层:应用层数据交给传输层(TCP 或 UDP),添加传输层首部。
- 传输层 → 网络层:传输层数据传递到网络层,网络层添加 IP 首部(源 IP 为本机,目标 IP 为外部地址),生成 IP 数据报。
- 网络层处理:
- 触发 NF_IP_LOCAL_OUT 钩子(自定义处理,如流量标记)。
- 经路由选择确定为转发,触发 NF_IP_POST_ROUTING 钩子,将数据报发送到数据链路层,经物理网络转发到下一跳设备。
3. 外部进来需转发出去的流程
- 来源与目标:数据报来自外部网络,经网络接口进入本机,目标地址非本机 IP。
- 处理逻辑:
- 数据报进入内核后,先经过 NF_IP_PRE_ROUTING 钩子(预处理,如过滤)。
- 路由模块判定为转发,触发 NF_IP_FORWARD 钩子(检查转发规则)。
- 最后通过 NF_IP_POST_ROUTING 钩子,发送到下一跳网络设备。
4. 外部进来且目标为本机的流程
- 来源与目标:数据报来自外部网络,经网络接口进入本机,目标地址为本机 IP。
- 处理逻辑:
- 数据报进入内核后,先进行 IP 检验(验证首部有效性)。
- 触发 NF_IP_PRE_ROUTING 钩子(如防火墙规则检查)。
- 路由模块判定为本地交付,触发 NF_IP_LOCAL_IN 钩子(用户空间自定义操作,如安全检查)。
- 最终将数据报传递给上层协议(传输层),由对应协议(如 TCP、UDP)处理后续逻辑。
2.ip_rcv函数整体调用栈
当收到ip数据报的时候,调用ip_rcv函数进行处理,检查ip数据报并通过Netfilter钩子处理之后交给ip_rcv_finish进行后续的处理。ip_rcv_finish
函数主要完成 IP 数据包接收后的一些收尾处理,包括调用 l3mdev_ip_rcv
处理可能存在的 L3 主设备情况,然后调用 ip_rcv_finish_core
进行核心的路由和统计更新等操作,最后根据处理结果调用 dst_input。
在 ip_rcv_finish
函数中,如果 ip_rcv_finish_core
的返回值不是 NET_RX_DROP
,就会调用 dst_input(skb)
。dst_input
是一个通用的数据包输入处理函数,它会根据 skb
中的目的信息(存储在 skb_dst(skb)
中)调用相应的输入函数。以下是这个处理函数被设置的时机:当 arp_rcv 函数调用 arp_process 处理 ARP 数据包时,arp_process 会通过 ip_route_input_noref () 触发路由选择子系统执行查找。该过程会判断数据包目的地址属性:若为本地地址,最终通过 skb_dst_set (skb, &rt->dst) 完成 skb 与路由结果 dst 的关联,此时 dst 的 input 函数指针会被设置为 ip_local_deliver,用于本地协议栈交付;若目的地址非本地(需转发),该 input 函数指针则会被设置为 ip_forward,为后续数据包转发处理做准备。这一机制不仅适用于 ARP 场景,更是 IP 层处理所有数据包路由走向(本地接收或转发)的通用逻辑。
当 skb_dst(skb)->input
为 ip_local_deliver
时,就会调用 ip_local_deliver
函数,将数据包递交给本地协议栈进行进一步处理 。ip_local_deliver
函数主要处理 IP 分片重组,并再次调用 Netfilter 的 NF_INET_LOCAL_IN
钩子函数。如果数据包通过了这些钩子函数的检查,最终会调用 ip_local_deliver_finish。
调用流程总结
下面是从
ip_rcv_finish
到ip_local_deliver
再到ip_local_deliver_finish
的完整调用流程:
ip_rcv_finish
调用ip_rcv_finish_core
完成核心的路由和统计更新等操作。- 如果
ip_rcv_finish_core
处理成功,ip_rcv_finish
调用dst_input(skb)
。dst_input(skb)
根据skb_dst(skb)->input
调用ip_local_deliver
。ip_local_deliver
处理 IP 分片重组并调用 Netfilter 的NF_INET_LOCAL_IN
钩子函数。- 如果数据包通过 Netfilter 检查,
NF_HOOK
调用ip_local_deliver_finish
完成最终的本地交付处理。
更加详细的流程请参考以下文章:
内核arp_rcv函数到ip_local_deliver_finish的具体调用流程-CSDN博客
3.具体ip_rcv(...)函数分析
/** IP receive entry point*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,struct net_device *orig_dev)
{//从网络设备 dev 中提取其所属的 网络命名空间(Network Namespace)。//Linux内核通过命名空间实现容器化网络隔离,每个命名空间有独立的网络栈(IP地址、路由表等)struct net *net = dev_net(dev);/*对IP数据包进行初步处理,包括:长度检查:确保数据包长度不小于IP头部长度。版本检查:验证 iph->version 是否为IPv4(值为 4)。校验和验证:计算IP头部校验和,若错误则丢弃。处理IP选项:若IP头包含选项(如记录路由、时间戳),解析并处理。数据包规范化:可能对数据包进行重新封装或调整(如移除填充字节)。*/skb = ip_rcv_core(skb, net);if (skb == NULL)return NET_RX_DROP;/*NFPROTO_IPV4:指定协议族为 IPv4。NF_INET_PRE_ROUTING:指定钩子点为 PRE_ROUTING,即在路由决策之前处理数据包。net:网络命名空间。skb:指向数据包的 sk_buff 指针。dev:接收数据包的网络设备。ip_rcv_finish:如果 Netfilter 钩子处理通过,数据包将被传递给 ip_rcv_finish 函数进行后续的 处理,如路由决策等。*/return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,net, NULL, skb, dev, NULL,ip_rcv_finish);
}
参数说明:
struct sk_buff *skb
:sk_buff
是 Linux 内核中用于存储网络数据包的核心数据结构,也被称为套接字缓冲区。它包含了数据包的实际内容以及相关的控制信息,如源地址、目的地址、协议类型等。skb
指针指向接收到的数据包。struct net_device *dev
:net_device
结构体表示网络设备,如以太网网卡、无线网卡等。dev
指向接收该数据包的网络设备。struct packet_type *pt
:packet_type
结构体用于描述网络数据包的类型信息,包括协议类型、处理函数等。pt
指向与该数据包类型相关的结构体。struct net_device *orig_dev
:表示原始的网络设备,通常在数据包经过桥接等处理时使用,这里用于标识数据包最初进入系统的网络设备。
函数功能:
ip_rcv()
是IPv4协议的数据包接收入口函数,主要完成以下任务:
-
协议分发入口:
从网络接口层(如以太网)接收数据包后,根据packet_type
确认是IPv4协议数据包,进入IP层处理。 -
初步处理:
调用ip_rcv_core()
进行IP头部合法性检查、校验和验证、选项解析等。 -
Netfilter拦截:
通过NF_HOOK
触发NF_INET_PRE_ROUTING
钩子(如iptables的PREROUTING规则),决定是否继续处理或丢弃数据包。 -
路由决策:
最终调用ip_rcv_finish()
完成路由选择(转发或本地处理)。
4.具体ip_rcv_core(...)代码讲解
函数参数
skb
:指向struct sk_buff
类型的指针,sk_buff
是 Linux 内核中用于表示网络数据包的结构体,它包含了数据包的各种信息,如数据、头部信息、网络设备信息等。net
:指向struct net
类型的指针,net
结构体代表一个网络命名空间,用于隔离不同的网络环境。
函数功能
ip_rcv_core
是 Linux 内核中处理 IP 数据包接收的核心函数。它负责对接收到的 IP 数据包进行基本的合法性检查,如数据包类型、IP 头部长度、版本号、校验和等,若检查通过则对数据包进行一些必要的处理,如调整数据包长度、设置传输层头部指针等,最后返回处理后的 sk_buff
结构体指针;若检查不通过,则丢弃该数据包并返回 NULL
。
内部重要逻辑部分
1. 检查数据包类型
if (skb->pkt_type == PACKET_OTHERHOST)goto drop;
当网络接口处于混杂模式时,会接收到不属于本机的数据包。若数据包类型为
PACKET_OTHERHOST
,表示该数据包是发给其他主机的,直接跳转到drop
标签处丢弃该数据包。2. 增加接收统计信
__IP_UPD_PO_STATS(net, IPSTATS_MIB_IN, skb->len);
更新网络命名空间
net
中的接收统计信息,记录接收到的数据包的总长度。3. 检查数据包共享情况
skb = skb_share_check(skb, GFP_ATOMIC); if (!skb) {__IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);goto out; }
skb_share_check
函数用于检查skb
是否被共享,若共享则复制一份。若复制失败,增加接收丢弃统计信息并跳转到out
标签处返回NULL
。4. 检查 IP 头部长度
if (!pskb_may_pull(skb, sizeof(struct iphdr)))goto inhdr_error; iph = ip_hdr(skb);
pskb_may_pull
函数用于检查skb
中是否有足够的数据来容纳一个 IP 头部。若不足,跳转到inhdr_error
标签处增加头部错误统计信息并丢弃数据包。若足够,则获取 IP 头部指针iph
。5. 检查 IP 头部基本信息
if (iph->ihl < 5 || iph->version != 4)goto inhdr_error;
检查 IP 头部长度字段
ihl
是否至少为 5(表示 IP 头部至少有 20 字节),以及 IP 版本号是否为 4(IPv4)。若不满足条件,跳转到inhdr_error
标签处处理。6. 增加 ECN 统计信
BUILD_BUG_ON(IPSTATS_MIB_ECT1PKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_ECT_1); BUILD_BUG_ON(IPSTATS_MIB_ECT0PKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_ECT_0); BUILD_BUG_ON(IPSTATS_MIB_CEPKTS != IPSTATS_MIB_NOECTPKTS + INET_ECN_CE); __IP_ADD_STATS(net,IPSTATS_MIB_NOECTPKTS + (iph->tos & INET_ECN_MASK),max_t(unsigned short, 1, skb_shinfo(skb)->gso_segs));
根据 IP 头部的服务类型字段
tos
中的显式拥塞通知(ECN)信息,更新网络命名空间net
中的相应统计信息。7. 再次检查 IP 头部长度
if (!pskb_may_pull(skb, iph->ihl*4))goto inhdr_error; iph = ip_hdr(skb);
再次检查
skb
中是否有足够的数据来容纳完整的 IP 头部(ihl * 4
字节),若不足则跳转到inhdr_error
标签处处理。8. 检查 IP 头部校验和
if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))goto csum_error;
ip_fast_csum
函数用于快速计算 IP 头部的校验和。若校验和不通过,跳转到csum_error
标签处增加校验和错误统计信息并丢弃数据包。9. 检查数据包总长度
len = ntohs(iph->tot_len); if (skb->len < len) {__IP_INC_STATS(net, IPSTATS_MIB_INTRUNCATEDPKTS);goto drop; } else if (len < (iph->ihl*4))goto inhdr_error;
将 IP 头部中的总长度字段
tot_len
从网络字节序转换为主机字节序。若skb
的实际长度小于总长度,说明数据包被截断,增加截断数据包统计信息并丢弃该数据包;若总长度小于 IP 头部长度,跳转到inhdr_error
标签处处理。10. 调整数据包长度
if (pskb_trim_rcsum(skb, len)) {__IP_INC_STATS(net, IPSTATS_MIB_INDISCARDS);goto drop; }
pskb_trim_rcsum
函数用于将skb
的长度调整为 IP 头部指定的总长度,并更新校验和。若调整失败,增加接收丢弃统计信息并丢弃该数据包。11. 设置传输层头部指针
iph = ip_hdr(skb); skb->transport_header = skb->network_header + iph->ihl*4;
更新 IP 头部指针,并设置传输层头部指针为 IP 头部之后的位置。
12. 清空套接字控制块信息
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm)); IPCB(skb)->iif = skb->skb_iif;
清空
skb
中的套接字控制块信息,并设置输入接口信息。13. 孤儿化
skb
skb_orphan(skb);
由于 TProxy 的原因,将
skb
从当前套接字中分离出来。孤儿化的含义
skb_orphan
函数的作用是将skb
从当前关联的套接字中分离出来,使其成为一个 “孤儿” 数据包。在 Linux 内核网络子系统中,一个skb
通常会与某个套接字(struct sock
)相关联,这种关联用于管理数据包的发送和接收。与 TProxy 的关系
TProxy(Transparent Proxy)是一种透明代理技术,它允许在不修改客户端和服务器之间的通信的情况下对数据包进行拦截和转发。在 TProxy 场景下,数据包可能需要被重新路由或者转发到其他地方,此时就需要将数据包从原来的套接字关联中分离出来,以避免干扰后续的处理流程。通过调用
skb_orphan
函数,可以确保skb
不再与原来的套接字有任何关联,从而可以独立地进行后续的处理,例如重新路由到代理服务器或者进行其他形式的转发。14. 返回处理后的
skb
return skb;
若所有检查和处理都通过,返回处理后的
skb
结构体指针。错误处理
csum_error:__IP_INC_STATS(net, IPSTATS_MIB_CSUMERRORS); inhdr_error:__IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS); drop:kfree_skb(skb); out:return NULL;
在不同的错误情况下,增加相应的错误统计信息,释放
skb
占用的内存,并返回NULL
。
IP 分组可能如上述交付给本地计算机处理,也可能离开互联网络层,转发到另一台计算机,而不牵涉本地计算机的高层协议实例。分组的目标地址分为以下两类:
(1)目标计算机在某个本地网络中,发送计算机与该网络有连接。
(2)目标计算机在地理上属于远程计算机,不连接到本地网络,只能通过网关访问。
第二种场景更复杂,首先需找到剩余路由中的第一个站点,将分组转发到该站点(这是向最终目标地址的第一步传输)。因此,不仅需要计算机所属本地网络结构的相关信息,还需要相邻网络结构和相关外出路径的信息。
1.
ip_rcv
(IP 层接收入口)
- 作用:接收网络设备驱动层传来的 IP 分组,进行校验和、版本等基础检查。
- 关键逻辑:判断分组是否属于本地(通过查找本地 IP 地址),若不属于本地,则进入转发流程,调用
ip_forward
。2.
ip_forward
(IP 转发主函数)
- 作用:处理非本地接收的 IP 分组,执行转发策略检查(如转发是否允许)、TTL 递减等操作。
- 关键逻辑:通过路由查找函数
fib_lookup
确定转发路径,若找到合适路由,调用ip_forward_finish
继续处理。3.
fib_lookup
(路由查找)
- 作用:根据目标 IP 地址,在路由表(FIB,转发信息库)中查找转发路径,确定下一跳网关和出接口。
- 关键逻辑:匹配路由表项,获取下一跳地址和输出网络设备。
4.
ip_forward_finish
(转发收尾处理)
- 作用:完成分组转发前的最后处理,如更新 IP 头部校验和、设置链路层头部等。
- 关键逻辑:调用
__ip_forward_skbuff
处理分组,最终通过dev_queue_xmit
将分组发送到输出网络设备。5.
dev_queue_xmit
(设备层发送)
- 作用:将处理好的分组交给网络设备驱动,完成物理层发送。
- 关键逻辑:触发设备驱动的发送函数,将分组通过物理链路发送到下一跳。
5.ip_forward(...)
通过网关访问:相邻网络结构和外出路径信息,该信息由路由表提供,路由表由内核通过多种数据结构实现并管理,在接收分组时调用ip_route_input函数充当路由实现的接口,一方面因为该函数能够识别出分组时交付到本地还是转发出去,另一方面因为该函数能够找到通向目标地址的路由。我们所说的目标地址存储在套接字缓冲区的dst字段地址。
ip_forward代码流程如下:
其主要功能:根据报文信息得到路由、ipset安全监测、转发的基本逻辑,ip层提交本地处理流程等,根据ip地址决定是提交给本地处理(ip_local_deliver),还是报文转发(ip_forward)。
二、发送分组
1.ip_queue_xmit(...)
内核提供几个通过网络层发送数据的函数,可以由较高协议层使用。其中ip_queue_xmit是最常使用的,代码流程如下:
ip_queue_xmit()
是 IP 层的通用数据包发送函数,负责所有 IP 数据包的封装、分片和发送,与上层协议是否面向连接无关。其核心逻辑如下:
路由缓存优化:
- 无论套接字是否面向连接(如 TCP/UDP),内核会缓存最近使用的路由信息(
dst_cache
)。- 对于已连接的 TCP 套接字,目的地址固定,可直接复用缓存路由,减少路由查询开销。
- 对于未连接的 UDP 套接字(未调用
connect
),每次发送需实时查询路由(除非缓存命中)。IP 层处理流程:
- 构建 IP 头部(源 / 目的 IP、协议号、TTL 等)。
- 调用 Netfilter 钩子(如
local_out
、post_routing
)进行包过滤。- 若数据包超过 MTU,调用
ip_fragment
分片。- 将数据包传递给链路层发送(
dev_queue_xmit
)。协议无关性:
- TCP:通过
tcp_transmit_skb()
调用ip_queue_xmit()
,依赖路由缓存提升效率。- UDP:通过
udp_sendmsg()
→ip_send_skb()
调用ip_queue_xmit()
,分片逻辑与 TCP 一致,但无连接状态管理。
调用完ip_queue_xmit
函数后,通常会按以下流程继续处理:
- netfilter 钩子处理:
ip_queue_xmit
完成其工作(比如构建 IP 报头)后,会调用netfilter
的local_out
钩子进行本地输出的数据包过滤。之后还会调用netfilter
的post_routing
钩子 ,进一步对即将离开主机的数据包进行过滤和处理,只有通过这些钩子检查的数据包才会继续后续流程。 - 路由操作:进行输出路由操作,确定数据包的下一跳地址和出接口等信息。
- 数据包分段(分片):调用
ip_output
函数,在此函数中会判断数据包是否需要分片(相对的重组的函数是ip_rcv中的ip_defrag)。如果数据包大小超过了下一跳链路的最大传输单元(MTU),就会调用ip_fragment
函数进行分片处理 ,将大数据包拆分成多个小的分片以便传输。 - 链路层处理:调用链路层发送函数(如
dev_queue_xmit
),将处理好的数据包(可能是原包,也可能是分片后的包)传递给数据链路层。数据链路层会进行帧封装等操作,然后通过网络设备驱动程序将数据发送到物理链路。 - 队列处理与发送:数据进入设备的发送队列(
qdisc
,队列调度器),如default_qdisc
,然后根据队列算法进行处理。设备驱动程序会将数据包排入硬件的发送缓冲区(如ringbuffer tx
) 。 - 硬件传输:网络接口卡(NIC)从内存中获取数据包(通过直接内存访问,DMA),并将其发送到物理网络上。传输完成后,NIC 会发出硬件中断信号表示传输结束,驱动程序处理该中断,并进行后续的资源管理和处理操作 。
用户层的tcp的api函数send 函数与 ip_queue_xmit 的关系:
用户层的
send
函数是应用层 API,其底层实现会通过系统调用(如write
或sendto
)进入内核空间。以 TCP 为例:
- 传输层处理:
send
会调用 TCP 层的发送函数(如tcp_sendmsg
),处理 TCP 协议相关逻辑(如分段、序号管理、滑动窗口等)。- IP 层调用:TCP 层处理完数据后,会调用 IP 层函数(如
ip_queue_xmit
),由 IP 层负责构建 IP 包头、路由查找、分片等操作。
2.ip_output(...)
1. Netfilter 钩子处理
- 调用
netfilter
框架的NF_IP_POST_ROUTING
钩子,允许防火墙、流量监控等模块对即将离开主机的 IP 数据包进行过滤、修改等操作,实现网络安全策略或流量控制。2. 分片决策与处理
- 判断数据包是否需要分片:
- 若数据包大小超过输出接口的最大传输单元(MTU),调用
ip_fragment
对数据包进行分片,确保分片后的数据包符合链路层传输要求。- 若无需分片,直接进入后续流程。
3. 数据包发送准备
- 调用
ip_finish_output
和ip_finish_output2
函数,完成 IP 层对数据包的最终处理,包括:
- 检查数据包是否有足够空间容纳链路层硬件首部(如以太网首部),若不足,通过
skp_realloc_headroom
调整缓冲区空间。- 最终通过
dst->neighbour->output
调用邻居子系统(Neighbor Subsystem)的发送函数,将数据包交付给数据链路层,由链路层完成帧封装并通过物理网络发送。4. 衔接网络层与链路层
作为 IP 层向网络访问层(链路层)的过渡函数,
ip_output
整合了 IP 层的路由、分片逻辑,最终将处理好的数据包移交链路层,实现从网络层到物理传输的关键跳转。
3.路由
在任何 IP 实现中,不仅在转发外部分组时需要,而且也用于发送本地计算机产生的分组。
每个接收到的分组属于 3 个类别之一:其目标是本地主机;其目标是当前主机直接连接的计算机;其目标是远程计算机,只能经由中间系统到达。
路由结果关联到一个套接字缓冲区,套接字缓冲区的 dst 成员指向一个 dest_entry 结构的实例,此实例的内容是在路由查找期间填充的,具体内核代码如下:
struct neighbour成员存储计算机在本地网络中的IP和硬件地址,这样就可以通过网络访问层直接到达。neighbour实例由内核中实现ARP的ARP层创建,ARP协议负责将IP地址转为硬件地址(MAC地址).
一、
dst_entry
结构体使用案例场景:本地主机通过以太网发送 UDP 数据包到远程服务器(
192.168.1.100
),路由查找后dst_entry
结构体的典型应用流程如下:
应用层触发发送
用户空间调用sendto
发送 UDP 数据,内核通过系统调用进入udp_sendmsg
,开始构建数据包。路由查找填充
dst_entry
- 调用路由查找函数(如
ip_route_output_ports
),根据目标 IP(192.168.1.100
)查找路由表(1)。- 找到匹配路由规则后,创建或获取
dst_entry
实例:struct dst_entry *dst; // 路由查找示例(简化逻辑) dst = ip_route_output_ports(net, sk, saddr, daddr, tos, dev, NULL, NULL, OIF);
- 填充
dst_entry
成员:
dev
:指定输出网络设备(如eth0
)。ops
:关联操作函数集(如inet_ops
,包含output
函数指针,指向ip_output
)。
- 基于
dst_entry
发送数据包
- 调用
dst->ops->output
(即ip_output
),将数据包交给 IP 层处理:dst->ops->output(net, sk, skb);
- 后续流程:IP 层分片、调用 Netfilter 钩子,最终通过
dst->dev
(eth0
)发送数据。
二、
dst_entry
与其他结构体的交互逻辑
- 与
net_device
的交互
dst_entry->dev
指向输出网络设备(如eth0
),决定数据包从哪个物理接口发送。- 示例:发送数据包前,通过
dst->dev
获取接口 MTU,判断是否需要分片。
- 与
dst_ops
的交互
dst_entry->ops
是操作函数集,定义关键行为(如output
函数)。- 交互示例:
通过struct dst_ops inet_ops = { .output = ip_output, // 关联 IP 层发送函数 // 其他函数(如释放资源) }; dst->ops = &inet_ops;
dst->ops->output
触发 IP 层处理流程。
- 与
neighbour
结构体的交互
- 目标:解析链路层地址(如 MAC 地址)。
- 流程:
dst_entry
关联的路由确定下一跳 IP 后,通过邻居子系统(neighbour
)解析下一跳的 MAC 地址。- 例如,ARP 模块创建
neighbour
实例,查询 ARP 表获取 MAC 地址,填充到链路层首部,最终通过dst->dev
发送数据。
上述内容补充:
(1)这个路由表查询的是什么?
1. 典型路由表条目数据
假设系统路由表中有如下条目:
字段 示例数据 说明 目的网络 192.168.1.0
目标网络地址(子网) 子网掩码 255.255.255.0
用于判断目标 IP 是否属于该子网 下一跳 IP 192.168.1.1
数据包转发的下一跳地址(若目标在子网外,通常为网关) 出接口 eth0
数据包从该网络接口发送 路由优先级(管理距离) 100
多路由匹配时,优先级高的规则优先使用 2. 匹配过程
- 子网匹配:
目标 IP192.168.1.100
与子网掩码255.255.255.0
按位与,得到网络地址192.168.1.0
,与路由表中目的网络192.168.1.0
匹配。- 结果输出:
匹配成功后,路由查找函数获取该条目中的 下一跳 IP(192.168.1.1
) 和 出接口(eth0
),填充到dst_entry
结构体,用于后续数据包转发。
(2)当通过路由表确定下一条地址的时候,下一步要查询arp表,如果不存在,要发送arp请求,那么这个arp请求是广播吗?然后一定跟本机在同一子网吗?
当通过路由表确定下一跳地址后,若 ARP 表中没有对应的映射条目,通常会发送 ARP 请求,这个 ARP 请求一般是广播形式 ,但也有特殊情况;而下一跳地址不一定与本机在同一子网,具体说明如下:
ARP 请求是否为广播
- 通常为广播形式:在常见的网络环境中,当设备需要解析某个 IP 地址对应的 MAC 地址,且 ARP 表中没有相关记录时,会以广播的形式发送 ARP 请求。广播的目的 MAC 地址是全 F(例如在以太网中为 FF - FF - FF - FF - FF - FF),这样同一个广播域内的所有设备都会收到该请求。只有目标 IP 地址匹配的设备会回复 ARP 响应,告知自身的 MAC 地址。
- 特殊情况:也存在一些特殊情况不是以广播形式发送 ARP 请求。例如在支持 IPV6 的网络中,使用邻居发现协议(NDP)替代 ARP,NDP 使用组播地址代替广播;另外在一些启用了代理 ARP 功能的网络场景中,ARP 请求的发送和处理方式会有所变化,代理 ARP 设备会代替目标设备响应 ARP 请求,减少广播流量。
下一跳地址与本机是否在同一子网
- 可能在同一子网:当目标 IP 地址与本机在同一子网时,通过路由表确定的下一跳地址就是目标主机的 IP 地址,此时下一跳地址与本机在同一子网。例如,在一个小型局域网中,两台主机进行通信,它们的 IP 地址都在 192.168.1.0/24 网段内,数据可以直接在子网内传输,下一跳就是目标主机 ,和本机处于同一子网。
- 可能不在同一子网:若目标 IP 地址与本机不在同一子网,此时通过路由表确定的下一跳地址通常是网关(路由器)的 IP 地址,用于将数据包转发到其他子网或网络。这种情况下,下一跳地址(网关)和本机不一定在同一子网。比如,主机 A 的 IP 地址是 192.168.1.10,要访问的目标服务器 IP 地址是 192.168.2.50,它们属于不同子网,主机 A 会将数据包发送给网关(如 192.168.1.1) ,由网关进行后续转发,该网关就和主机 A 不在同一个目标子网内。
(3) 当目标与本机不在同一子网,还需要arp协议找到本地网关的MAC地址吗?
ARP 协议的作用
ARP(Address Resolution Protocol)即地址解析协议,其主要功能是将 IP 地址解析为对应的 MAC 地址。在以太网等局域网环境中,数据链路层的帧传输需要知道目标设备的 MAC 地址,而网络层的通信使用的是 IP 地址,因此需要通过 ARP 协议来完成这两种地址之间的映射。
通信流程及 ARP 协议的使用
当主机 A(IP 地址为 192.168.1.10)要访问目标服务器(IP 地址为 192.168.2.50)时,由于它们处于不同子网,主机 A 会根据路由表确定下一跳地址为网关(如 192.168.1.1)。接下来的步骤如下:
- ARP 表检查:主机 A 首先会检查自己的 ARP 表,看是否已经存在网关(192.168.1.1)对应的 MAC 地址记录。
- ARP 请求发送:如果 ARP 表中没有该记录,主机 A 会以广播的形式发送一个 ARP 请求。这个 ARP 请求的目标 IP 地址是网关的 IP 地址(192.168.1.1),请求内容是询问拥有该 IP 地址的设备的 MAC 地址是什么。
- ARP 响应接收:网关设备接收到这个 ARP 请求后,会识别出目标 IP 地址是自己的,于是会发送一个 ARP 响应给主机 A,响应中包含了网关自己的 MAC 地址。
- ARP 表更新:主机 A 接收到网关的 ARP 响应后,会将网关的 IP 地址和对应的 MAC 地址记录到自己的 ARP 表中,以便后续通信使用。
- 数据帧封装与发送:主机 A 在知道了网关的 MAC 地址后,会将数据封装成以太网帧,其中目的 MAC 地址设置为网关的 MAC 地址,源 MAC 地址设置为自己的 MAC 地址,然后将该帧通过物理网络发送给网关。网关接收到帧后,会根据自身的路由表将数据包转发到目标服务器所在的子网。
必要性分析
网络层的数据包最终要通过数据链路层进行传输,而数据链路层的帧必须明确目标设备的 MAC 地址才能正确发送。虽然主机 A 知道了下一跳的 IP 地址(网关的 IP 地址),但在数据链路层通信时需要对应的 MAC 地址,因此必须使用 ARP 协议来解析网关的 IP 地址对应的 MAC 地址,这样才能保证数据包能够正确地从主机 A 传输到网关,进而由网关转发到目标服务器。