本文分析ip_queue的内核态源码。文中如有任何疏漏和差错,欢迎各位朋友指正。
由于本文内容较多,本人将其分为上、中、下三篇。其中上篇和中篇的链接如下:
上篇:http://blog.chinaunix.net/u/33048/showart_2139488.html
中篇:http://blog.chinaunix.net/u/33048/showart_2139494.html
本文欢迎自由转载,但请标明出处,并保证本文的完整性。
作者:Godbach
Blog:http://Godbach.cublog.cn
日期:2010/01/04
接收用户空间的数据包
ip_queue模块加载的代码中有这样一行代码:
ipqnl = netlink_kernel_create(NETLINK_FIREWALL, 0, ipq_rcv_sk,
THIS_MODULE);
上文也已经分析了,该行代码就是注册一个用于接收用户空间配置数据的函数ipq_rcv_sk。因此,接收和处理用户空间的配置应该从这个函数开始分析。
ipq_rcv_sk的代码如下:
static void
ipq_rcv_sk(struct sock *sk, int len)
{
struct sk_buff *skb;
unsigned int qlen;
mutex_lock(&ipqnl_mutex);
for (qlen = skb_queue_len(&sk->sk_receive_queue); qlen; qlen--) {
skb = skb_dequeue(&sk->sk_receive_queue);
ipq_rcv_skb(skb);
kfree_skb(skb);
}
mutex_unlock(&ipqnl_mutex);
}
该函数的功能非常明确,就从当前套接字的接收数据包队列中取出数据包,然后交给ipq_rcv_skb进一步分析和处理,然后就释放掉该数据包的缓存。
下面接着分析ipq_rcv_skb函数,其源码如下:
#define RCV_SKB_FAIL(err) do { netlink_ack(skb, nlh, (err)); return; } while (0)
static inline void
ipq_rcv_skb(struct sk_buff *skb)
{
int status, type, pid, flags, nlmsglen, skblen;
struct nlmsghdr *nlh;
/*获取并检查数据包的长度*/
skblen = skb->len;
if (skblen < sizeof(*nlh))
return;
/*获取netlink消息头部的结构体,并检查头部中保存的数据长度字段*/
nlh = (struct nlmsghdr *)skb->data;
nlmsglen = nlh->nlmsg_len;
if (nlmsglen < sizeof(*nlh) || skblen < nlmsglen)
return;
/*取到用户态的进程ID,以及用户空间配置的标记为*/
pid = nlh->nlmsg_pid;
flags = nlh->nlmsg_flags;
/*判断用户进程的ID是否合法,以及该数据包是否是netlink的请求包*/
if(pid <= 0 || !(flags & NLM_F_REQUEST) || flags & NLM_F_MULTI)
RCV_SKB_FAIL(-EINVAL);
if (flags & MSG_TRUNC)
RCV_SKB_FAIL(-ECOMM);
/*获取消息的类型并检查*/
type = nlh->nlmsg_type;
if (type < NLMSG_NOOP || type >= IPQM_MAX)
RCV_SKB_FAIL(-EINVAL);
/*如果不是ip_queue消息,则返回*/
if (type <= IPQM_BASE)
return;
if (security_netlink_recv(skb, CAP_NET_ADMIN))
RCV_SKB_FAIL(-EPERM);
write_lock_bh(&queue_lock);
/*如果内核态记录的用户空间进程ID非0,且当前数据包的进程ID并不等于内核态记录的值,则意味着接收数据包失败*/
if (peer_pid) {
if (peer_pid != pid) {
write_unlock_bh(&queue_lock);
RCV_SKB_FAIL(-EBUSY);
}
} else {
net_enable_timestamp();
/*如果内核态记录的peer_pid 为0,则将当前消息中的进程ID记录到全局变量peer_pid 中。此种情形应该是用户态初次向内核发送ip_queue的消息*/
peer_pid = pid;
}
write_unlock_bh(&queue_lock);
/*处理用户空间下发的ip_queue配置消息*/
status = ipq_receive_peer(NLMSG_DATA(nlh), type,
nlmsglen - NLMSG_LENGTH(0));
if (status < 0)
RCV_SKB_FAIL(status);
if (flags & NLM_F_ACK)
netlink_ack(skb, nlh, 0);
return;
}
该函数对于接收到的用户空间下发的消息,检查其多个参数的合法性,并判断是否是ip_queue的消息。如果是,则交由ipq_receive_peer函数进行消息的解析和处理。
下面我们分析一下ipq_receive_peer函数的实现,代码如下:
static int
ipq_receive_peer(struct ipq_peer_msg *pmsg,
unsigned char type, unsigned int len)
{
int status = 0;
if (len < sizeof(*pmsg))
return -EINVAL;
/*根据用户态的消息类型进行处理*/
switch (type) {
case IPQM_MODE:
status = ipq_set_mode(pmsg->msg.mode.value,
pmsg->msg.mode.range);
break;
case IPQM_VERDICT:
if (pmsg->msg.verdict.value > NF_MAX_VERDICT)
status = -EINVAL;
else
status = ipq_set_verdict(&pmsg->msg.verdict,
len - sizeof(*pmsg));
break;
default:
status = -EINVAL;
}
return status;
}
该函数主要是根据用户态设置的消息的类型,进行不同的处理。我们在IP Queue分析的第一篇文章《Linux内核IP Queue机制的分析(一)——用户态接收数据包》(以下简称文一)中已经提到,用户态下发到内核态的消息分为“模式设置消息(IPQM_MODE)”和“断言消息(IPQM_VERDICT)”两个子类。
这里我们先分析消息类型为IPQM_MODE即模式设置消息时内核态的处理。对于断言消息IPQM_VERDICT的处理部分,我们将在后面第六节中分析。
模式设置消息对应的处理函数为ipq_set_mode,其代码如下:
static int
ipq_set_mode(unsigned char mode, unsigned int range)
{
int status;
write_lock_bh(&queue_lock);
status = __ipq_set_mode(mode, range);
write_unlock_bh(&queue_lock);
return status;
}
该函数主要就是调用__ipq_set_mode函数,其代码如下:
static inline int
__ipq_set_mode(unsigned char mode, unsigned int range)
{
int status = 0;
switch(mode) {
case IPQ_COPY_NONE:
case IPQ_COPY_META:
copy_mode = mode;
copy_range = 0;
break;
case IPQ_COPY_PACKET:
copy_mode = mode;
copy_range = range;
if (copy_range > 0xFFFF)
copy_range = 0xFFFF;
break;
default:
status = -EINVAL;
}
return status;
}
同样,我们在文一中分析过了模式设置消息的三种请求模式:IPQ_COPY_NONE、IPQ_COPY_META和IPQ_COPY_PACKET。参考文一中的具体解释,可以很轻松的理解该函数的实现:
(1)当请求模式为IPQ_COPY_NONE或IPQ_COPY_META时,记录下所设置的模式保存到全局变量copy_mode,并且将全局变量copy_range置0。因为这两种模式都不会要求读取数据包的原始内容;
(2)当请求模式为IPQ_COPY_PACKET时,记录下所设置的模式保存到全局变量copy_mode,并将用户空间设置的读取数据包的长度值保存到全局变量copy_range中。如果用户设置的长度大于0xFFFF,则将copy_range设置为0xFFFF。
这里copy_mode和copy_range,以及前面提到的peer_pid都使用全局变量类型,因为内核态发送相关消息到用户空间时要读取这几个参数,并根据各参数值的不同,发送不同类型的消息到用户空间。
通过模式设置消息,用户空间成功的告诉内核态,我需要数据包的哪些部分,只是摘要信息,还是连数据包的原始内容也要。如果需要数据包的原始内容,长度是多少等等。而内核态也会根据用户态下发的需求,将数据包的相关信息打包发给用户空间的接收进程。
IP Queue机制提供了一种将数据包交给用户态处理的方法。那么,最后最关键的一步就是对数据包的处理。经过我们上面的分析,用户空间对数据包的处理,就是通过下发IP Queue消息,告诉内核态数据包是丢弃还是接受等。而内核态会根据具体的消息对数据包做真正的丢弃或接受等处理。
那么,我们就从用户态下发的消息开始,一直分析到数据包最终被处理。
我们上面分析了用户态下发的消息最终由__ipq_set_mode函数处理。该函数会根据消息类型调用对应的处理函数。用户态下发的数据包处理意见的消息,我们称之为“断言消息”,对应消息类型的宏定义为IPQM_VERDICT。该消息对应的处理函数为ipq_set_verdict。该函数代码如下:
static int
ipq_set_verdict(struct ipq_verdict_msg *vmsg, unsigned int len)
{
struct ipq_queue_entry *entry;
/*检查对数据包处理意见的值的合法性*/
if (vmsg->value > NF_MAX_VERDICT)
return -EINVAL;
/*根据断言消息中保存的id找到对应的数据包queue管理结构体*/
entry = ipq_find_dequeue_entry(id_cmp, vmsg->id);
if (entry == NULL)
return -ENOENT;
else {
/*记录断言消息的处理结果*/
int verdict = vmsg->value;
/*如果用户空间拷贝了数据包的原始内容,则要根据数据包被修改的情况调用函数ipq_mangle_ipv4对queue管理结构体中保存的原始skb进行修改*/
if (vmsg->data_len && vmsg->data_len == len)
if (ipq_mangle_ipv4(vmsg, entry) < 0)
verdict = NF_DROP;
/*将数据包重新注入NF框架*/
ipq_issue_verdict(entry, verdict);
return 0;
}
}
该函数首先根据断言消息的id,从全局链表queue_list中找到当前消息对应的queue管理结构体。然后,根据用户空间对数据包的修改和处理意见,对当前queue管理结构体中的skb进行修改。最后,调用函数ipq_issue_verdict将数据包重新注入NF框架。
下面将逐一分析上面三个阶段的代码。
1. 获取断言消息对应的queue管理结构体
ipq_set_verdict函数中实现代码如下:
entry = ipq_find_dequeue_entry(id_cmp, vmsg->id);
可见这里调用了ipq_find_dequeue_entry函数获取queue管理结构体的。传进去参数一个是函数指针id_cmp, 一个是断言消息的id。我们在文一中已经提到过,断言消息的id即内核态发送给用户空间消息中的packet_id。通过ipq_build_packet_message函数我们可以知道,packet_id就是当前数据包对应的queue管理结构体的地址。因此,找到queue管理结构体的方法,就是遍历queue_list全局链表,找到地址等于vmsg->id的queue管理结构体。
因此,不难想象,id_cmp函数的实现代码:
static inline int
id_cmp(struct ipq_queue_entry *e, unsigned long id)
{
return (id == (unsigned long )e);
}
那么,函数ipq_find_dequeue_entry的代码实现的方法也就很明确了:
static struct ipq_queue_entry *
ipq_find_dequeue_entry(ipq_cmpfn cmpfn, unsigned long data)
{
struct ipq_queue_entry *entry;
write_lock_bh(&queue_lock);
entry = __ipq_find_dequeue_entry(cmpfn, data);
write_unlock_bh(&queue_lock);
return entry;
}
该函数进一步调用函数__ipq_find_dequeue_entry,代码如下:
static inline struct ipq_queue_entry *
__ipq_find_dequeue_entry(ipq_cmpfn cmpfn, unsigned long data)
{
struct ipq_queue_entry *entry;
entry = __ipq_find_entry(cmpfn, data);
if (entry == NULL)
return NULL;
__ipq_dequeue_entry(entry);
return entry;
}
该函数首先调用__ipq_find_entry,遍历全局链表queue_list,找到当前断言消息对应数据包的queue管理结构体:
/*
* Find and return a queued entry matched by cmpfn, or return the last
* entry if cmpfn is NULL.
*/
static inline struct ipq_queue_entry *
__ipq_find_entry(ipq_cmpfn cmpfn, unsigned long data)
{
struct list_head *p;
list_for_each_prev(p, &queue_list) {
struct ipq_queue_entry *entry = (struct ipq_queue_entry *)p;
if (!cmpfn || cmpfn(entry, data))
return entry;
}
return NULL;
}
然后将调用函数__ipq_dequeue_entry将找到的queue管理结构体从全局链表queue_list中删除,并减小被queue的数据包的统计计数。
static inline void
__ipq_dequeue_entry(struct ipq_queue_entry *entry)
{
list_del(&entry->list);
queue_total--;
}
至此,我们已经找到了断言消息对应数据包的queue管理结构体。
2. 根据用户的配置修改数据包
ipq_set_verdict函数中实现代码如下:
int verdict = vmsg->value;
if (vmsg->data_len && vmsg->data_len == len)
if (ipq_mangle_ipv4(vmsg, entry) < 0)
verdict = NF_DROP;
这里,首先记录下对数据包的处理结果vmsg->value。然后,比较关键的一步,就是如果断言消息中数据长度部分不为0,并且消息中记录的载荷长度等于当前计算出来的载荷长度(如果正常进行设置和通信的话,两者应该是相等的),则调用ipq_mangle_ipv4对原始的数据包进行处理。
函数ipq_mangle_ipv4的代码如下:
static int
ipq_mangle_ipv4(ipq_verdict_msg_t *v, struct ipq_queue_entry *e)
{
int diff;
/*获得用户空间处理后的数据包的ip头部*/
struct iphdr *user_iph = (struct iphdr *)v->payload;
/*判断消息中记录的载荷长度是否小于头部长度。如果是,则就不再修改queue中保存的skb*/
if (v->data_len < sizeof(*user_iph))
return 0;
/*判断数据包的长度是否发生变化,即获取用户修改后的数据包和原始数据包的长度的关系*/
diff = v->data_len - e->skb->len;
/*用户减小了数据包的长度*/
if (diff < 0)
skb_trim(e->skb, v->data_len);
/*用户增大了数据包的长度*/
else if (diff > 0) {
/*非法的数据包长度值*/
if (v->data_len > 0xFFFF)
return -EINVAL;
/*数据包被扩充的长度大于了skb的tailroom,即skb的tail:end之间的长度已经不能容纳数据包增加的数据,这是需要新分配一个skb*/
if (diff > skb_tailroom(e->skb)) {
struct sk_buff *newskb;
/*将原先的skb进行扩充,并将原始skb的数据包拷贝到新的skb中*/
newskb = skb_copy_expand(e->skb,
skb_headroom(e->skb),
diff,
GFP_ATOMIC);
if (newskb == NULL) {
printk(KERN_WARNING "ip_queue: OOM "
"in mangle, dropping packet\n");
return -ENOMEM;
}
if (e->skb->sk)
skb_set_owner_w(newskb, e->skb->sk);
kfree_skb(e->skb);
e->skb = newskb;
}
/*将skb的tail指针增加tail+diff*/
skb_put(e->skb, diff);
}
if (!skb_make_writable(&e->skb, v->data_len))
return -ENOMEM;
/*将断言消息中全部的载荷拷贝到skb对应的数据区中。*/
memcpy(e->skb->data, v->payload, v->data_len);
e->skb->ip_summed = CHECKSUM_NONE;
return 0;
}
该函数主要就是断言消息中的对数据包做的任何修改都反应到原始的skb中。
3. 将修改后的数据包重新注入NF框架
ipq_set_verdict函数实现的代码如下:
ipq_issue_verdict(entry, verdict);
即调用ipq_issue_verdict函数将数据包重新注入NF中。该函数的代码如下:
static void
ipq_issue_verdict(struct ipq_queue_entry *entry, int verdict)
{
/* TCP input path (and probably other bits) assume to be called
* from softirq context, not from syscall, like ipq_issue_verdict is
* called. TCP input path deadlocks with locks taken from timer
* softirq, e.g. We therefore emulate this by local_bh_disable() */
local_bh_disable();
nf_reinject(entry->skb, entry->info, verdict);
local_bh_enable();
kfree(entry);
}
该函数将queue管理结构体的两个个成员skb、info,以及用户对数据包的处理结果verdict作为入参,直接调用nf_reinject函数。nf_reinject函数会根据三个参数所保存的相关信息,对数据包进行真正的处理,比入交给下一个hook函数处理(NF_ACCEPT),或者丢弃(NF_DROP),或者重新交给当前再次交给当前的hook函数处理(NF_REPEAT)。
至此,整个ip_queue模块的核心代码已经分析完毕。