关于UDP
UDP数据包
source/dest:长16位,取值范围1~65535。
len:有效负载和UDP数据报头的总长度,单位为字节。
UDP初始化
UDP和其他核心协议都是在启动阶段通过方法inet_init()
来初始化的。
定义对象udp_protocol,用inet_add_protocol()
添加。
内核还提供了UDP传输层操作集:struct proto udp_prot{...}
,用proto_register()
注册。
UDP模块处理流程
概述
回顾套接字发送数据的流程,见套接字之收发数据、监听、连接、绑定:
send()|write()|sendto() → sys_send() → sys_sendto() → sock_sendmsg() → __sock_sendmsg() → sock->prot->sendmsg() → sk->sk_prot->sendmsg()
UDP采用SOCK_DGRAM类型的INET套接字,所以查询函数集后,最后两步具体调用的函数是:
→ inet_sendmsg() → udp_sendmsg()
自此,网络数据包从应用程序的发送函数开始,已经过套接字接口,应用程序把应用层数据内容置入内核,并通知内核的UDP协议模块继续发送数据。下面主要关注UDP协议模块中的处理流程,直到UDP协议模块将数据包交给IP层协议模块。
如果在应用层调用发送函数,若采用SOCK_DGRAM套接字,会触发内核udp_sendmsg
调用,查找路由信息后,调用ip_append_data
对数据做分片处理,然后调用udp_push_pending_frames
进行UDP包封装,并把数据包交给IP层处理。
如果在应用层调用接收函数,IP层取出数据内容后,函数ip_local_deliver_finish
会调用UDP模块的udp_rcv
函数。数据包会被加入套接字队列,再由函数udp_recvmsg
取出,并通知应用程序函数读取数据。
UDP包的发送
应用层采用SOCK_DGRAM套接字发送数据,会触发内核udp_sendmsg
调用。
udp_sendmsg(iocb, sk, msg, len) udp.c
- 设置corkreq:用于指出是否应该使用缓冲区机制,如果没有则数据包立即被发送,如果有则数据包会交给
udp_sendmsg()
累积,直到取消该选项——最后一个数据包到达。 - 检查长度len是否越界。
- 检查msg中是否有MSG_OOB(指定flag为MSG_MORE时,corkreq=1,等于使用了缓冲区机制)。
- 通过检查msg->msg_name字段,判断目的地址是否合法:
- 目的地址不为空:检查是否目的地址是否合法,协议族是否正确,有错误则return -EINVAL;
- 目的地址为空:若套接字处于TCP_ESTABLISHED状态(套接字已连接)仍认为目的地址合法,允许继续传送数据;若套接字不处于TCP_ESTABLISHED状态,则return -EDESTADDRREQ;
- 检查msg->msg_controllen,即:如果是控制报文(不为0),则通过
ip_cmsg_send()
处理控制报文,该方法分析指定的msghdr对象,并创建一个icmp_cookie对象(包含可供处理数据包时使用的信息)。 - 确定是否需要路由信息,是否广播。
- 若套接字已连接,则不需要查询路由,从套接字管理信息中返回路由表信息,并记录到rtable中。
- 若套接字无路由信息(rt==NULL),则调用
ip_route_output_flow()
查询路由表。 - cork the socket: inet->cork.fl的赋值。
ip_append_data
():对UDP数据包进行分片处理,为IP层分片处理做好准备- 若设置了corkreq,则需要调用
lock_sock()
获取套接字锁,之后再发送数据包,ip_append_data()
会将数据加入缓冲区,不立即传输,之后第调用udp_push_pending_frames(sk, up)
来完成传输。 - 出错则:
udp_flush_pending_frames(sk)→ip_flush_pending_frames()
,清空所有等待传输的SKB; - 没有出错且corkreq=0(不使用缓冲机制)则:
udp_push_pending_frames(sk, up)
; - 一些结束处理。标签out和do_confirm。
ip_append_data ip_output.c
创建套接字缓冲区sk_buff,为IP层数据分片做好准备;该函数根据路由查询得到的接口MTU,把超过MTU长度的数据分片保存在多个套接字缓冲区中,并插入套接字的发送队列sk_write_queue中(对于较大的数据包,该函数可能循环多次)。
具体过程:
- 一些变量的声明。其中:
- 从sk结构中获取inet结构:
struct inet_opt *inet = inet_sk(sk)
;inet结构保存套接字选项,inet->cork成员存储了与分片有关的控制信息。
- 从sk结构中获取inet结构:
- 判断套接字发送队列sk->sk_write_queue是否为空。
- 队列为空,则对inet->cork初始化,为分片做准备;
- ip选项不为空,则获取一些分片用的信息设置inet->cork;
- 初始化分片位置信息:设置sk的指针指向分片首地址、下一分片的存放位置;
- 队列不为空/不是第一个分片,则套接字缓冲区的data内容中没有头部格式信息。
- 队列为空,则对inet->cork初始化,为分片做准备;
- 从路由表项中得到网络设备的硬件头部信息。
- 分片首部长度:${fragheaderlen}={ip头部}+{ip选项长度(如果有)}$;获取分片最大长度(maxfraglen)。
- 累计分片数据的总长度,由inet->cork.length记录。
- 如果是空队列/是第一个分片,需要分配一个新的套接字缓冲区。
- 把尚未插入队列的新数据插入到套接字发送队列中。若$length>0$则说明数据还有剩余,需要继续分片并插入队列。
- 如果当前套接字缓冲区中没有空间装剩下的数据,则要分配新套接字缓冲区给剩下的数据;
- 分片长度:$fraglen={fragheaderlen}+{数据长度}$;
- 为最后一个碎片分配更多空间;
- 分配套接字缓冲区;
- 设置IP数据包的校验和模式(csummode),并初始化校验和(csum);
- 在套接字缓冲区中预留容纳硬件头头部的空间
skb_reserve
; - 为套接字缓冲区设置数据存放的起始位置:设置skb;
- 计算实际需要复制的数据长度(copy),把数据复制到套接字缓冲区中;
- 计算分片偏移位置,计算尚未分配套接字缓冲区的数据长度;
- 把已分配得到的套接字缓冲区插入套接字发送队列中。
- 判断网络设备是否设置了scatter/gather,如果有,则按照scatter/gather设置分片处理。
- 如果当前套接字缓冲区中没有空间装剩下的数据,则要分配新套接字缓冲区给剩下的数据;
udp_push_pending_frames(sk,*up) udp.c
传入参数*up为struct udp_opt型。
1 | struct udp_opt { |
该函数具体执行:
- 一些变量的声明。其中:
- 从sk结构中获取inet结构:
struct inet_opt *inet = inet_sk(sk)
;inet结构保存套接字选项,inet->cork成员存储了与分片有关的控制信息。 - 声明一个struct sk_buff *skb用于存套接字队列中的一个套接字缓冲区。
- 声明一个struct udphdr *uh用于保存udp头部信息。
- 从sk结构中获取inet结构:
- 从套接字发送队列(&sk->sk_write_queue)中得到一个套接字缓冲区,
skb_peek
。 - 为数据包设置udp头部信息:源端口、目的端口、长度等,存于uh。
- 如果不需要校验和计算,则直接去发送该数据包
goto send
。 - 校验和处理。
- send:准备IP协议处理,
ip_push_pending_frames(sk)
。
UDP包的接收
发现接收了一个UDP包,IP层协议调用udp_rcv把数据包递交到UDP协议模块。
通过调用udp_queue_rcv_skb和sock_queue_rcv_skb,UDP协议模块把数据包插入套接字的接收队列中,等待udp_recvmsg从队列中取出数据包,递交到应用程序。
udp_rcv(skb) udp.c
被注册为接收UDP数据包的方法。
UDP初始化时,内核通过struct net_protocol udp_protocol注册接收UDP数据包的方法。
1 | static struct net_protocol udp_protocol = { |
udp_rcv函数具体内容:
- 判断套接字缓冲区中是否存在一个UDP头部长度的存储区。
- 如果不存在则goto no_header处理。
- 获取UDP包的起始位置uh,获取UDP包的长度ulen。
- 得到UDP数据包的长度,
pskb_trim()
。 - 是否组播?是:
udp_v4_mcast_deliver()
;否:继续。 - 根据套接字信息(源IP地址、源端口、目的IP地址、目的端口),查找端口上是否有一个打开的套接字?
sk=udp_v4_lookup()
。- 如果还有,则把套接字缓冲区插入套接字sk的接收队列中,
udp_queue_rcv_skb()
,返回。 - sk==NULL:继续。
- 如果还有,则把套接字缓冲区插入套接字sk的接收队列中,
- 检查校验和,校验和正确则继续。如果校验和错误,则丢掉该数据包,
goto csum_error
。 - 返回一个ICMP包,通知对方目的地/端口不可达,
icmp_send()
。 - 丢弃数据包。
参考:《Linux网络内核分析与开发》,肖宇峰、李昕、时岩编著。
udp_queue_rcv_skb(sk,skb) udp.c
被udp_rcv
所调用,通过sock_queue_rcv_skb
把收到的套接字缓冲区skb插入套接字sk的接收队列中。
IPsec与XFRM框架
涉及到IPsec——Internet协议安全子系统:是一组协议,是大多数IPVPN技术的标准配置。这组协议对通信会话中的每个数据包进行身份验证和加密,以确保IP流量的安全。
IPsec是由XFRM框架实现的,XFRM读作’transfrom’。XFRM框架是独立于协议簇的,旨在提供适用于生产环境的IPv6和IPsec协议栈。
XFRM策略(xfrm_policy)和XFRM状态(xfrm_state)是XFRM框架中的基本数据结构。
XFRM策略:告诉IPsec是否要对特定流进行处理的规则,策略包含一个选择器(xfrm_selector),用于指定要将策略应用于哪些流。
XFRM状态:表示IPsec安全关联,包含加密密钥、标志、请求ID、统计信息、重放参数等信息。内核将IPsec安全关联存储在安全关联数据库中。
函数具体内容
- XFRM安全策略检查,
xfrm4_policy_check(sk,XFRM_POLICY_IN,skb)
。 - 对IPsec封装包进行分析处理。
- 检查sk、skb,如果需要校验,则计算校验和。
- 调用函数
sock_queue_rcv_skb(sk,skb)
将套接字缓冲区skb插入套接字sk的接收队列中。
sock_queue_rcv_skb(sk,skb) sock.h
对套接字缓冲区skb做简单处理后,函数sock_queue_rcv_skb
把套接字缓冲区插入套接字接收队列sk->sk_receive_queue
的尾部。
- 套接字的包过滤处理,
sk_filter()
。出错则结束函数,返回错误信息。 - 设置套接字缓冲区skb的一些数据
- skb->dev置空;
- skb->sk指针指向当前套接字结构sk,
skb_set_owner_r(skb,sk)
。
- 插入套接字接收队列的尾部,
skb_queue_tail(&sk->sk_receive_queue, skb);
。 - 通知被阻塞的操作:套接字接收队列中已有数据就绪,
sock_flag()
。
udp_recvmsg() udp.c
应用程序通过套接字调用接收函数时,会调用内核函数udp_recvmsg()
。该函数从套接字队列中取出数据,并通过struct msghdr结构把其中的数据和地址信息复制给用户程序。
- 检查地址长度;检查队列中是否有错误信息。
- 从套接字sk的接收队列中取出套接字缓冲区skb,
skb_recv_datagram()
- 准备复制数据:需要复制的数据(copied)不包括UDP头部;如果缓冲区长度不够(copied>len),则设置缓冲区长度,并作截断标志。
- 校验判断:
- 如果不需要校验(skb->ip_summed==CHECKSUM_UNNECESSRY),则把套接字缓冲区skb数据复制到msg->msg_iov结构中,以便应用程序从接收缓冲区中读取数据,
skb_copy_datagram_iovec()
。 - 如果需要校验,则先校验,再把套接字缓冲区skb数据复制到msg->msg_iov结构,
skb_copy_datagram_iovec()
。 - 其他情况:复制套接字缓冲区的内容并进行校验,
skb_copy_and_csum_datagram_iovec()
。
- 如果不需要校验(skb->ip_summed==CHECKSUM_UNNECESSRY),则把套接字缓冲区skb数据复制到msg->msg_iov结构中,以便应用程序从接收缓冲区中读取数据,
- 记录接收时间,
sock_recv_timestamp(msg,sk,skb)
。 - 复制地址信息:sin <- skb。
- 处理IP选项,
ip_cmsg_recv(msg,skb)
。 - 如果用户程序接收数据采用MSG_PEEK标志,则读出数据,但不删除套接字队列中的缓冲。