关于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层的发送流程。
- 通过sk中的struct inet_opt结构获得路由表项信息。sk中的inet->cork.rt是udp_sendmsg调用ip_append_data时指定的,记录了ip_route_output_flow查找路由表所返回的路由表项信息。此处将信息记录在struct rtable *rt中。
- rt的rt->u.dst决定了套接字缓冲区skb数据如何进一步发送。
- 检查套接字发送队列是否为空,并返回队首的套接字缓冲区,存为skb。
- 遍历套接字发送队列,调整数据长度。
- 获得分片标志:df。
- 获取TTL:ttl。
- 将套接字缓冲区中的data所指区域强制转换成struct iphdr结构。
- 通过设置iph的信息:TOS、总长度、标志号等IP头部信息。
- 计算校验和并设置IP头部的校验和字段,
ip_send_check()
。
skb->dst = dst_clone(&rt->u.dst);
为套接字缓冲区指定了路由表项信息。从传输层到IP层的至关重要的一步。- 即,为数据包进入IP发送流程设置了具体方法(skb->dst->output = ip_output)。
- 此时,程序已经为dst_output配置完skb处理信息。
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层的发送流程。
- 得到套接字缓冲区曾经记录的路由表项信息,写入变量rt中。
- 确认套接字缓存的路由是否有效,并返回给变量rt,
__sk_dst_check(sk,0)
。 - 建立TCP连接后,路由表项信息被保存在套接字缓冲区中。
- 如果没有缓存的路由表项,则查找路由表
ip_route_output_flow()
,获取路由表项信息,把路由信息返回给rt。
- 如果没有缓存的路由表项,则查找路由表
- 重要步骤:为数据包进入IP发送流程设置具体方法:
skb->dst = dst_clone(&rt->u.dst)
。 - 在套接字缓冲区中为IP头部留下IP头部空间,
skb_push()
。 - 设置IP头部协议信息。
- 构建IP选项信息:给iph的各项元素赋值。
- 设置IP包的头部标识符;设置IP校验和
ip_send_check()
。 - 此时,已经配置了必要的skb信息,为发送了足够的准备。
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 | /* Output packet to network from transport. */ |
ip_output(pskb) ip_output.c
1 | int ip_output(struct sk_buff **pskb) |
ip_finish_output(skb) ip_output.c
1 | int ip_finish_output(struct sk_buff *skb) |
ip_finish_output2(skb) ip_output.c
- 通过套接字缓冲区的struct dst_entry指针访问邻居子系统:
dst->neighbour
。 - 如果有缓存指针hh,则通过hh->output发送数据:hh_output指向一个发送数据的接口函数,比如dev_queue_xmit。
- 如果缓存指针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()
。
- 丢弃发给其他主机的数据包。
- 检查数据包长度是否为IP头部的长度:
pskb_may_pull(skb,sizeof(iphdr))
。 - 得到IP包的包头起始位置iph。
- 检查数据包是否为IPv4格式包:ipv4->ihl/version。
- 检查套接字缓冲区是否组偶容纳数据包指定长度的报头。
- 检查校验和:
ip_fast_csum(iph,iph->ihl)
。如果错误则跳转到对应错误处理。 - 检查IP包的总长度是否正确。
- 去掉填充字段:__pskb_trim(skb,len)。
- 由过滤器调用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
- 如果套接字缓冲区skb还未记录路由表项信息,则需要用ip_route_input查找路由表,为skb设置路由表项信息。
- 如果IP报头长度大于20字节,则报头携带了IP选项信息,调用函数skb_cow来确定skb是否被共享而可写入,以确保缓冲修改的安全性。
- 函数
ip_options_compile(NULL,skb)
从数据包收集选项信息。 - 检查数据包是否设置了源路由选项(opt->srr):
- 调用
ip_options_rcv_srr(skb)
处理IP选项。
- 调用
- 通过接口dst_input确定对数据包的处理:递交给上层协议模块/转发。
- 函数
dst_input(skb) include/net/dst.h
该函数递交套接字缓冲区中的数据,具体去往何处由skb->dst->input指针确定。
如果要转发数据包,则通过该指针调用函数ip_forward
。
如果要递交数据包,则通过该指针调用函数ip_local_deliver
。
1 | /* Input packet from network to transport. */ |
下面先看交付本地的情况:ip_local_deliver。
ip_local_deliver(skb) ip_input.c
重组IP包后存于skb,ip_defrag(skb)
。
1 | /*Deliver IP Packets to the higher protocol layers.*/ |
ip_local_deliver_finish(skb) ip_input.c
该函数确定原始套接字列表raw_v4_htable中是否有接收数据包的套接字。
如果曾经注册过专门的原始套接字来接收某协议的数据包,则调用raw_v4_input接收该数据。
- 得到IP报头长度ihl之后,调用
__skb_pull(skb,ihl)
把套接字缓冲区的数据从IP包取出。 - 得到IP报头封装的传输层协议类型值:protocol。
- 生成与协议变量protocol对应的查表索引值:hash。
- 判断与protocol对应的协议是否通过原始套接字接收数据,如果是,则返回对应原始套接字,并通过
raw_v4_input(skb,iph,hash)
来接收套接字缓冲区中的数据内容。 - 从表inet_protos[hash]得到管理protocol协议接受方法的struct net_protocol类型变量
inet_protos[hash]
,并保存在ippprot中;通过ipprot的handler指针调用上层协议的接收函数:- protocol指示为UDP协议,则ipprot = &udp_protocol;调用handler指向的函数
udp_rcv
; - protocol指示为ICMP协议,则ipprot = &icmp_protocol;调用handler指向的函数
icmp_rcv
;
- protocol指示为UDP协议,则ipprot = &udp_protocol;调用handler指向的函数
- 如果上层模块没有接收缓冲区数据的套接字
(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
- xfrm策略检查,skb的包类型检查。
- 如果设置了router_alert的选项,则必须调用
ip_call_ra_chain
处理数据包,ip_call_ra_chain
会将数据包交给所有原始套接字。 - 获得IP报头iph,检查iph->ttl是否减为0,如果减为0,则goto too_many_hops,发送ICMP数据包报告数据包无效。
- 检查是否同时设置了严格路由标志和rt_uses_gateway标志。
- 如果都使用了,则不能用严格路由选择,则goto sr_failed,发回一条“严格路由失败”的ICMP“目的不可达”消息。
- 即将要修改数据包信息(ttl、checksum)则先拷贝一份skb。
- 给iph->ttl字段减一。
- 如果目的地址不可达,则通知发送数据的源端,
ip_rt_send_redirect(skb)
。 - 假设没有NF_IP_FORWARD钩子,则调用
ip_forward_finish
完成转发的最后操作。 - sr_failed:返回一个ICMP包,报告目的地不可达,
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
- too_many_hops:返回一个ICMP包,报告数据包已失效,
icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
ip_forward_finish() ip_forward.c
1 | static inline int ip_forward_finish(struct sk_buff *skb) |