0%

Linux网络内核源码分析|网络层之IP层处理

关于IPv4

IP简述

IP包通过路由器向正确的方向跳转,以达到转发数据的效果。为了完成正确的效果,数据包必须包含源地址和目的地址。

IP支持数据分片。如果数据过长,则将其分成一片一片的分片再发送。分片到达目的地的顺序可能与发送顺序不同,因此IP需要将分片重新组成原来的数据段。

IPv4数据包

一个数IPv4数据包由头部和数据构成,数据中记录了传输层信息内容。

IPv4模块处理流程

概述

从传输层到IP层发送数据包:

  • UDP模块调用函数ip_push_pending_frames()进入IP层。
  • TCP模块调用函数ip_queue_xmit()进入IP层。

这两个函数都为数据包封装了IP协议信息,把对应的头部加到套接字缓冲区内。

ip_queue_xmit()会调用ip_route_output_flow()查找路由表:为套接字缓冲区设置路由出口信息。如果已有TCP连接,则套接字缓冲区保存了路由信息,则不需要这一步。

两条路下来,都调用dst_output()进入IP层发送流程:

dst_output()→ip_output()→ip_finish_output()→ip_finish_output2()→邻居子系统调用接口函数dev_queue_xmit()→底层网络设备驱动程序

如果是接收数据包:

网络设备驱动会通过接口netif_rx()把数据包递交给ip_rcv(),进入IP层。接着调用:ip_rcv_finish()作数据包处理→ip_route_input为数据包查询路由信息,确定是转发/交付本地。如果是需要转发的,则不用再次查询路由。接着,ip_rcv_finish()→dst_input()分情况处理数据包:转发/交付本地

对于交付本地的数据包:

dst_input()→ip_local_deliver()对要交付的数据包进一步处理→ip_local_deliver_finish()把数据包交给传输层

对于要转发的数据包:

dst_input()→ip_forward()→ip_forward_finish(),之后就按照发送数据包的方式发送,路由信息早已设置好。

发送数据包

从UDP发:ip_push_pending_frames(sk) ip_output.c

大致的功能:封装IP头部信息,进入IP层的发送流程。

  1. 通过sk中的struct inet_opt结构获得路由表项信息。sk中的inet->cork.rt是udp_sendmsg调用ip_append_data时指定的,记录了ip_route_output_flow查找路由表所返回的路由表项信息。此处将信息记录在struct rtable *rt中。
    1. rt的rt->u.dst决定了套接字缓冲区skb数据如何进一步发送。
  2. 检查套接字发送队列是否为空,并返回队首的套接字缓冲区,存为skb。
  3. 遍历套接字发送队列,调整数据长度。
  4. 获得分片标志:df。
  5. 获取TTL:ttl。
  6. 将套接字缓冲区中的data所指区域强制转换成struct iphdr结构。
    1. 通过设置iph的信息:TOS、总长度、标志号等IP头部信息。
    2. 计算校验和并设置IP头部的校验和字段,ip_send_check()
  7. skb->dst = dst_clone(&rt->u.dst);为套接字缓冲区指定了路由表项信息。从传输层到IP层的至关重要的一步。
    1. 即,为数据包进入IP发送流程设置了具体方法(skb->dst->output = ip_output)。
    2. 此时,程序已经为dst_output配置完skb处理信息。
  8. err = NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, skb->dst->dev, dst_output)。内核将从这里转到dst_output函数,通过ip_output函数进入IP发送流程。

从TCP发:ip_queue_xmit(skb,ipfragok) ip_output.c

TCP模块调用该函数处理数据包,该函数与ip_push_pending_frames()相似:封装IP头部信息,进入IP层的发送流程。

  1. 得到套接字缓冲区曾经记录的路由表项信息,写入变量rt中。
  2. 确认套接字缓存的路由是否有效,并返回给变量rt,__sk_dst_check(sk,0)
  3. 建立TCP连接后,路由表项信息被保存在套接字缓冲区中。
    1. 如果没有缓存的路由表项,则查找路由表ip_route_output_flow(),获取路由表项信息,把路由信息返回给rt。
  4. 重要步骤:为数据包进入IP发送流程设置具体方法:skb->dst = dst_clone(&rt->u.dst)
  5. 在套接字缓冲区中为IP头部留下IP头部空间,skb_push()
  6. 设置IP头部协议信息。
  7. 构建IP选项信息:给iph的各项元素赋值。
  8. 设置IP包的头部标识符;设置IP校验和ip_send_check()
  9. 此时,已经配置了必要的skb信息,为发送了足够的准备。
  10. NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,dst_output);内核将从这里转到dst_output函数,通过ip_output函数进入IP层处理。

dst_output(skb) /net/dst.h

该函数的任务是把套接字缓冲区(skb)中的数据发送出去,具体去向由路由表项的出口信息决定。

dst_output通过struct dst_entry的output指针进入具体的IP发送流程。所以此前要指定具体函数,这一步在UDP的udp_sendmsg()完成或TCP的ip_queue_xmit()完成。

此时skb->dst->output已经指向ip_output()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Output packet to network from transport.  */
static inline int dst_output(struct sk_buff *skb)
{
int err;
for (;;) {
// 这里实际调用了ip_output函数
err = skb->dst->output(&skb);

if (likely(err == 0))
return err;
if (unlikely(err != NET_XMIT_BYPASS))
return err;
}
}

ip_output(pskb) ip_output.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int ip_output(struct sk_buff **pskb)
{
struct sk_buff *skb = *pskb;

IP_INC_STATS(IPSTATS_MIB_OUTREQUESTS);

// 如果数据包太大而不能再网络设备上传输,则将其分成一些小分片
if ((skb->len > dst_pmtu(skb->dst) || skb_shinfo(skb)->frag_list) &&
!skb_shinfo(skb)->tso_size)
return ip_fragment(skb, ip_finish_output);
else
return ip_finish_output(skb);
// 进入ip_finish_output()
}

ip_finish_output(skb) ip_output.c

1
2
3
4
5
6
7
8
9
10
11
12
13
int ip_finish_output(struct sk_buff *skb)
{
struct net_device *dev = skb->dst->dev;

// 设置套接字缓冲区数据的发送设备
skb->dev = dev;
// 设置所用的协议为IP协议
skb->protocol = htons(ETH_P_IP);

// 通过过滤器进入ip_finish_output2
return NF_HOOK(PF_INET, NF_IP_POST_ROUTING, skb, NULL, dev,
ip_finish_output2);
}

ip_finish_output2(skb) ip_output.c

  1. 通过套接字缓冲区的struct dst_entry指针访问邻居子系统:dst->neighbour
  2. 如果有缓存指针hh,则通过hh->output发送数据:hh_output指向一个发送数据的接口函数,比如dev_queue_xmit。
  3. 如果缓存指针hh为空,则通过dst->neighbour->output发送数据:output指向一个发送数据的接口函数,比如dev_queue_xmit。

IP包的本地接收

流程概述

ip_rcv()→ ip_rcv_finish() →确定要交付本地dst_input() →ip_local_deliver()→ip_local_deliver_finish()向上层协议下注册的套接字递交该数据包

ip_rcv(skb,dev,pt) net/ipv4/ip_input.c

该函数作一些格式化检查工作:判断数据包是不是发给本地的、检查头部长度是否正确、检查校验和、去掉填充字段等。然后调用ip_rcv_finish()

  1. 丢弃发给其他主机的数据包。
  2. 检查数据包长度是否为IP头部的长度:pskb_may_pull(skb,sizeof(iphdr))
  3. 得到IP包的包头起始位置iph。
  4. 检查数据包是否为IPv4格式包:ipv4->ihl/version。
  5. 检查套接字缓冲区是否组偶容纳数据包指定长度的报头。
  6. 检查校验和:ip_fast_csum(iph,iph->ihl)。如果错误则跳转到对应错误处理。
  7. 检查IP包的总长度是否正确。
  8. 去掉填充字段:__pskb_trim(skb,len)。
  9. 由过滤器调用ip_rcv_finish函数,进一步处理数据包。return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,ip_rcv_finish);

ip_rcv_finish(skb) net/ipv4/ip_input.c

  1. 如果套接字缓冲区skb还未记录路由表项信息,则需要用ip_route_input查找路由表,为skb设置路由表项信息。
  2. 如果IP报头长度大于20字节,则报头携带了IP选项信息,调用函数skb_cow来确定skb是否被共享而可写入,以确保缓冲修改的安全性。
    1. 函数ip_options_compile(NULL,skb)从数据包收集选项信息。
    2. 检查数据包是否设置了源路由选项(opt->srr):
      1. 调用ip_options_rcv_srr(skb)处理IP选项。
    3. 通过接口dst_input确定对数据包的处理:递交给上层协议模块/转发。

dst_input(skb) include/net/dst.h

该函数递交套接字缓冲区中的数据,具体去往何处由skb->dst->input指针确定。

如果要转发数据包,则通过该指针调用函数ip_forward

如果要递交数据包,则通过该指针调用函数ip_local_deliver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Input packet from network to transport.  */
static inline int dst_input(struct sk_buff *skb)
{
int err;
for (;;) {
// 这里决定去向:
err = skb->dst->input(skb);

if (likely(err == 0))
return err;
/* Oh, Jamal... Seems, I will not forgive you this mess. :-) */
if (unlikely(err != NET_XMIT_BYPASS))
return err;
}
}

下面先看交付本地的情况:ip_local_deliver。

ip_local_deliver(skb) ip_input.c

重组IP包后存于skb,ip_defrag(skb)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*Deliver IP Packets to the higher protocol layers.*/ 
int ip_local_deliver(struct sk_buff *skb)
{
/*Reassemble IP fragments.*/
// 如果IP包在发送时被分成多个小片,则调用ip_defrag重组IP包
// 完成重组的IP包被存放在套接字缓冲区中
if (skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {
skb = ip_defrag(skb);
if (!skb)
return 0;
}
// 递交给ip_local_deliver_finish做进一步处理
return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,ip_local_deliver_finish);
}

ip_local_deliver_finish(skb) ip_input.c

该函数确定原始套接字列表raw_v4_htable中是否有接收数据包的套接字。

如果曾经注册过专门的原始套接字来接收某协议的数据包,则调用raw_v4_input接收该数据。

  1. 得到IP报头长度ihl之后,调用__skb_pull(skb,ihl)把套接字缓冲区的数据从IP包取出。
  2. 得到IP报头封装的传输层协议类型值:protocol
  3. 生成与协议变量protocol对应的查表索引值:hash
  4. 判断与protocol对应的协议是否通过原始套接字接收数据,如果是,则返回对应原始套接字,并通过raw_v4_input(skb,iph,hash)来接收套接字缓冲区中的数据内容。
  5. 从表inet_protos[hash]得到管理protocol协议接受方法的struct net_protocol类型变量 inet_protos[hash],并保存在ippprot中;通过ipprot的handler指针调用上层协议的接收函数:
    1. protocol指示为UDP协议,则ipprot = &udp_protocol;调用handler指向的函数udp_rcv
    2. protocol指示为ICMP协议,则ipprot = &icmp_protocol;调用handler指向的函数icmp_rcv
  6. 如果上层模块没有接收缓冲区数据的套接字(ipprot = inet_protos[hash]) == NULL,则发送一个目的不可达的ICMP报文通知对方。

一些说明:

向raw_v4_htable表注册某原始套接字。如果创建了原始套接字,则hash函数把sk记录到raw_v4_htable表中。变量raw_prot管理SOCK_RAW类型套接字,它为hash指定了函数raw_v4_hash,该函数的功能就是把当前的套接字加入raw_v4_htable表中。

inet_init()向inet_protos表注册UDP协议——struct net_protocol udp_protocol,其中的成员变量handler指向udp_rcv()

IP包的转发

流程概述

ip_rcv()→ ip_rcv_finish() →确定要转发dst_input() →ip_forward()→ip_forward_finish()→数据包交给ip_output()进入IP发送流程

这里从ip_forward()函数开始讲起。

ip_forward() ip_forward.c

  1. xfrm策略检查,skb的包类型检查。
  2. 如果设置了router_alert的选项,则必须调用ip_call_ra_chain处理数据包,ip_call_ra_chain会将数据包交给所有原始套接字。
  3. 获得IP报头iph,检查iph->ttl是否减为0,如果减为0,则goto too_many_hops,发送ICMP数据包报告数据包无效。
  4. 检查是否同时设置了严格路由标志和rt_uses_gateway标志。
    1. 如果都使用了,则不能用严格路由选择,则goto sr_failed,发回一条“严格路由失败”的ICMP“目的不可达”消息。
  5. 即将要修改数据包信息(ttl、checksum)则先拷贝一份skb。
  6. 给iph->ttl字段减一。
  7. 如果目的地址不可达,则通知发送数据的源端,ip_rt_send_redirect(skb)
  8. 假设没有NF_IP_FORWARD钩子,则调用ip_forward_finish完成转发的最后操作。
  9. sr_failed:返回一个ICMP包,报告目的地不可达,icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
  10. too_many_hops:返回一个ICMP包,报告数据包已失效,icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);

ip_forward_finish() ip_forward.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline int ip_forward_finish(struct sk_buff *skb)
{
// 更新统计信息
struct ip_options * opt = &(IPCB(skb)->opt);

IP_INC_STATS_BH(IPSTATS_MIB_OUTFORWDATAGRAMS);

// 检查是否包含IP选项
if (unlikely(opt->optlen))
// 包含:
ip_forward_options(skb);
// 否则:
return dst_output(skb);// 转入IP层的发送流程
}