而每个将被重新组合的 IP 数据报都用一个 ipq 结构实例来表示。
struct ipq {
//用来将ipq_hash散列表链接成双向链表
struct hlist_node list;
struct list_head lru_list;
u32 user; //标识分片来源: 来自网络其他主机或是本地环回接口的分片、含有路由警告选项的IP分片
//以下四个字段的值都来源于ip首部,用来唯一确定分片来自哪个ip数据报
__be32 saddr;
__be32 daddr;
__be16 id;
u8 protocol;
u8 last_in;
#define COMPLETE 4 //所有分片已到达,可以进行组装;
#define FIRST_IN 2 // 第一个分片到达,其特殊之处在于只有第一个分片包含了所有ip选项;
#define LAST_IN 1 //最后一个分片已到达,最后一个分片带有原始数据包的长度信息
//用来链接已经接收到的分片
struct sk_buff *fragments;
int len;
//已接收到的所有分片总长度,因此可以用len和meat来判断一个ip数据报的所有分片时否已到齐
int meat;
//自旋锁,在smp环境下,处理ipq及分片链时需上锁
spinlock_t lock;
atomic_t refcnt; //引用计数
//组装超时定时器,组装分片非常消耗资源,防止无休止等待分片的到达
struct timer_list timer;
//记录最后一个分片的到达时间,在组装数据报时用该值作为时间戳
struct timeval stamp;
//接收最后一个分片的网络设备索引号。当分片组装失败时,用该设备发送组装失败icmp出错报文。
int iif;
//已接收到分片的计数器,可通过对端信息块peer中的分片计数器和该分片计数器来防止DoS攻击
unsigned int rid;
//记录发送方的一些信息
struct inet_peer *peer;
};
#define IPQ_HASHSZ 64
static struct hlist_head ipq_hash[IPQ_HASHSZ];
每个原始的 IP 数据报的所有分片以链表的形式保存在 ipq 结构 fragments 中。
在网络层中,会根据每个原始的 IP 数据报首部中的(saddr, daddr, id, protocol, ipfrag_hash_rnd)计算一个 hash 值,然后将 ipq 结构放到对应的 ipq_hash[hash] 散列表中。
因此,当一个新的分片 skb 到来时,根据(saddr, daddr, id, protocol, ipfrag_hash_rnd)计算出 hash 值,从 ipq_hash[hash] 散列表中找到 ipq 结构,然后把分片存放到 fragments 链表中。
当 IP 分片到达本地时,先调用 ip_defrag 进行重组。
//ip数据报输入到本地
int ip_local_deliver(struct sk_buff *skb)
{
if (skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {
skb = ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER);
if (!skb)
return 0;
}
return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
分片组装
分片组装的流程如下:
ip_defrag
--> ip_find 在ipq散列表中查找分片所属的ipq,若找不到则新建一个ipq
--> ip_frag_queue 将分片插入到ipq分片链表的适当位置
--> ip_frag_reasm 原始数据报的所有分片全部到达,组装分片
具体实现细节如下
ip_defrag
struct sk_buff *ip_defrag(struct sk_buff *skb, u32 user)
{
struct iphdr *iph = skb->nh.iph;
struct ipq *qp;
struct net_device *dev;
IP_INC_STATS_BH(IPSTATS_MIB_REASMREQDS);
//若iqp散列表消耗的内存大于指定的值,则ip_evictor()清理分片
if (atomic_read(&ip_frag_mem) > sysctl_ipfrag_high_thresh)
ip_evictor();
//获取接收数据报的网络设备指针
dev = skb->dev;
if ((qp = ip_find(iph, user)) != NULL) {
struct sk_buff *ret = NULL;
spin_lock(&qp->lock);
//将分片插入到ipq分片链表的适当位置
ip_frag_queue(qp, skb);
if (qp->last_in == (FIRST_IN|LAST_IN) &&
qp->meat == qp->len)
//调用ip_frag_reasm组装分片
ret = ip_frag_reasm(qp, dev);
spin_unlock(&qp->lock);
//删除iqp及其所有分片
ipq_put(qp, NULL);
return ret;
}
IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);
//释放分片
kfree_skb(skb);
return NULL;
}
当一个分片到达后,按照偏移量插入到 ipq 分片链表的适当位置,如下图:
ip_find
在 ipq 散列表中查找分片所属的 ipq 结构如下:
//根据分片的ip首部以及user标志在ipq散列表中找对应的ipq,若没找到,则为其创建新的ipq
static inline struct ipq *ip_find(struct iphdr *iph, u32 user)
{
__be16 id = iph->id;
__be32 saddr = iph->saddr;
__be32 daddr = iph->daddr;
__u8 protocol = iph->protocol;
unsigned int hash;
struct ipq *qp;
struct hlist_node *n;
read_lock(&ipfrag_lock);
//计算hash值
hash = ipqhashfn(id, saddr, daddr, protocol);
hlist_for_each_entry(qp, n, &ipq_hash[hash], list) {
if(qp->id == id &&
qp->saddr == saddr &&
qp->daddr == daddr &&
qp->protocol == protocol &&
qp->user == user) {
atomic_inc(&qp->refcnt);
read_unlock(&ipfrag_lock);
return qp;
}
}
read_unlock(&ipfrag_lock);
//返回新建的ipq
return ip_frag_create(iph, user);
}
ip_frag_create
创建 ipq 结构,并初始化其组装超时定时器。
static struct ipq *ip_frag_create(struct iphdr *iph, u32 user)
{
struct ipq *qp;
if ((qp = frag_alloc_queue()) == NULL)
goto out_nomem;
qp->protocol = iph->protocol;
qp->last_in = 0;
qp->id = iph->id;
qp->saddr = iph->saddr;
qp->daddr = iph->daddr;
qp->user = user;
qp->len = 0;
qp->meat = 0;
qp->fragments = NULL;
qp->iif = 0;
qp->peer = sysctl_ipfrag_max_dist ? inet_getpeer(iph->saddr, 1) : NULL;
init_timer(&qp->timer);
qp->timer.data = (unsigned long) qp;
qp->timer.function = ip_expire;
spin_lock_init(&qp->lock);
atomic_set(&qp->refcnt, 1);
return ip_frag_intern(qp);
out_nomem:
LIMIT_NETDEBUG(KERN_ERR "ip_frag_create: no memory left !\n");
return NULL;
}
ip_frag_intern
//将新建的ipq插入到ipq散列表中和ipq_lru_list中
static struct ipq *ip_frag_intern(struct ipq *qp_in)
{
struct ipq *qp;
unsigned int hash;
write_lock(&ipfrag_lock);
//计算hash值
hash = ipqhashfn(qp_in->id, qp_in->saddr, qp_in->daddr,
qp_in->protocol);
qp = qp_in;
if (!mod_timer(&qp->timer, jiffies + sysctl_ipfrag_time))
atomic_inc(&qp->refcnt);
//递增ipq的引用计数
atomic_inc(&qp->refcnt);
//将ipq插入到ipq散列表和lru_list中
hlist_add_head(&qp->list, &ipq_hash[hash]);
INIT_LIST_HEAD(&qp->lru_list);
list_add_tail(&qp->lru_list, &ipq_lru_list);
//对ipq的数量进行计数
ip_frag_nqueues++;
write_unlock(&ipfrag_lock);
return qp;
}
ip_frag_queue
static void ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
{
struct sk_buff *prev, *next;
int flags, offset;
int ihl, end;
//对分片已全部接收到的ipq,则释放该分片后返回
if (qp->last_in & COMPLETE)
goto err;
if (!(IPCB(skb)->flags & IPSKB_FRAG_COMPLETE) &&
unlikely(ip_frag_too_far(qp)) && unlikely(ip_frag_reinit(qp))) {
ipq_kill(qp);
goto err;
}
//取出ip首部中的标志位、片偏移及首部长度字段,并计算片偏移值和首部长度值
offset = ntohs(skb->nh.iph->frag_off);
flags = offset & ~IP_OFFSET;
//ip首部中的片偏移字段为13位,表示的是8字节的倍数
offset &= IP_OFFSET;
offset <<= 3;
ihl = skb->nh.iph->ihl * 4;
//计算分片末尾处在原始数据报中的位置
end = offset + skb->len - ihl;
if ((flags & IP_MF) == 0) {
if (end < qp->len ||
((qp->last_in & LAST_IN) && end != qp->len))
goto err;
//设置LAST_IN标志,将完整数据报长度存储在ipq的len字段中
qp->last_in |= LAST_IN;
qp->len = end;
} else {
//不是最后一个分片,其数据长度又不是8字节对齐,则将其截为8字节对齐
if (end&7) {
end &= ~7;
if (skb->ip_summed != CHECKSUM_UNNECESSARY)
skb->ip_summed = CHECKSUM_NONE;
}
if (end > qp->len) {
//若此数据报有异常,则直接丢弃
if (qp->last_in & LAST_IN)
goto err;
qp->len = end;
}
}
//若分片的数据区长度为0,则该分片异常,直接丢弃
if (end == offset)
goto err;
//调用pskb_pull去掉ip首部,只保留数据部分
if (pskb_pull(skb, ihl) == NULL)
goto err;
//将skb数据区长度调整为 end-offset, ip有效负载长度
if (pskb_trim_rcsum(skb, end-offset))
goto err;
prev = NULL;
for(next = qp->fragments; next != NULL; next = next->next) {
if (FRAG_CB(next)->offset >= offset)
break;
prev = next;
}
if (prev) {
int i = (FRAG_CB(prev)->offset + prev->len) - offset;
if (i > 0) {
offset += i;
if (end <= offset)
goto err;
if (!pskb_pull(skb, i))
goto err;
if (skb->ip_summed != CHECKSUM_UNNECESSARY)
skb->ip_summed = CHECKSUM_NONE;
}
}
while (next && FRAG_CB(next)->offset < end) {
int i = end - FRAG_CB(next)->offset;
if (i < next->len) {
if (!pskb_pull(next, i))
goto err;
FRAG_CB(next)->offset += i;
qp->meat -= i;
if (next->ip_summed != CHECKSUM_UNNECESSARY)
next->ip_summed = CHECKSUM_NONE;
break;
} else {
struct sk_buff *free_it = next;
next = next->next;
if (prev)
prev->next = next;
else
qp->fragments = next;
qp->meat -= free_it->len;
frag_kfree_skb(free_it, NULL);
}
}
//记录当前分片的偏移值
FRAG_CB(skb)->offset = offset;
//将当前的分片插入到ipq分片队列中的相应位置
skb->next = next;
if (prev)
prev->next = skb;
else
qp->fragments = skb;
if (skb->dev)
qp->iif = skb->dev->ifindex;
skb->dev = NULL;
//更新ipq的时间戳
skb_get_timestamp(skb, &qp->stamp);
//累计该分片已收到的分片总长度
qp->meat += skb->len;
//累计分片组装模块所占的内存
atomic_add(skb->truesize, &ip_frag_mem);
//若片偏移值为0,说明当前分片为第一个分片,设置FIRST_IN
if (offset == 0)
qp->last_in |= FIRST_IN;
write_lock(&ipfrag_lock);
//调整所属ipq在ipq_lru_list中的位置,这是为了在占用内存超过阈值时可以先释放最久未用的那些分片
list_move_tail(&qp->lru_list, &ipq_lru_list);
write_unlock(&ipfrag_lock);
return;
err:
kfree_skb(skb);
}
对于其中计算分片末尾处在原始数据报中的位置的地方。
//计算分片末尾处在原始数据报中的位置end = offset + skb->len - ihl;
有关ihl、offset、len 和 end 的关系,如下图:
ip_frag_reasm
static struct sk_buff *ip_frag_reasm(struct ipq *qp, struct net_device *dev)
{
struct iphdr *iph;
struct sk_buff *fp, *head = qp->fragments;
int len;
int ihlen;
ipq_kill(qp);
BUG_TRAP(head != NULL);
BUG_TRAP(FRAG_CB(head)->offset == 0);
//计算原始数据报包括ip首部的总长度,
ihlen = head->nh.iph->ihl*4;
len = ihlen + qp->len;
//若该长度值超过64k则丢弃
if(len > 65535)
goto out_oversize;
if (skb_cloned(head) && pskb_expand_head(head, 0, 0, GFP_ATOMIC))
goto out_nomem;
if (skb_shinfo(head)->frag_list) {
struct sk_buff *clone;
int i, plen = 0;
if ((clone = alloc_skb(0, GFP_ATOMIC)) == NULL)
goto out_nomem;
clone->next = head->next;
head->next = clone;
skb_shinfo(clone)->frag_list = skb_shinfo(head)->frag_list;
skb_shinfo(head)->frag_list = NULL;
for (i=0; i<skb_shinfo(head)->nr_frags; i++)
plen += skb_shinfo(head)->frags[i].size;
clone->len = clone->data_len = head->data_len - plen;
head->data_len -= clone->len;
head->len -= clone->len;
clone->csum = 0;
clone->ip_summed = head->ip_summed;
atomic_add(clone->truesize, &ip_frag_mem);
}
skb_shinfo(head)->frag_list = head->next;
skb_push(head, head->data - head->nh.raw);
atomic_sub(head->truesize, &ip_frag_mem);
for (fp=head->next; fp; fp = fp->next) {
head->data_len += fp->len;
head->len += fp->len;
if (head->ip_summed != fp->ip_summed)
head->ip_summed = CHECKSUM_NONE;
else if (head->ip_summed == CHECKSUM_COMPLETE)
head->csum = csum_add(head->csum, fp->csum);
head->truesize += fp->truesize;
atomic_sub(fp->truesize, &ip_frag_mem);
}
head->next = NULL;
head->dev = dev;
skb_set_timestamp(head, &qp->stamp);
//重置首部长度、片偏移、标志位和总长度
iph = head->nh.iph;
iph->frag_off = 0;
iph->tot_len = htons(len);
IP_INC_STATS_BH(IPSTATS_MIB_REASMOKS);
//既然各分片都已处理完,释放ipq的分片队列
qp->fragments = NULL;
return head;
out_nomem:
LIMIT_NETDEBUG(KERN_ERR "IP: queue_glue: no memory for gluing "
"queue %p\n", qp);
goto out_fail;
out_oversize:
if (net_ratelimit())
printk(KERN_INFO
"Oversized IP packet from %d.%d.%d.%d.\n",
NIPQUAD(qp->saddr));
out_fail:
IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);
return NULL;
}
组装的过程就是把所有分片组装起来,即将分片连接到第一个 skb 中的 frag_list 上,并返回组装后的数据报文。
ipq 散列表的重组
所有的分片重组都是通过 ipq 散列表进行的。随着后续 ipq 的添加或删除,使得散列表中的 ipq 的分布变得不均匀,处理性能会大大降低,因此需要定时对散列表进行重新组装。这样做同时也是为了防御 DoS 攻击。
散列表的定时重组是通过 ipfrag_secret_timer定时器实现的,在 ipfrag_init() 中对 ipfrag_secret_timer 定时器的初始化。在该函数中还初始化了 ipfrag_hash_rnd 变量,该变量主要用来与 IP 首部中的源地址、目的地址等构成 ipq 散列表的关键字。每次重组时都会将 ipfrag_hash_rnd 更新为一个新的随机值,并重新设置 ipfrag_secret_timer 定时器,时间跨度为 10 min。
ipfrag_init
void ipfrag_init(void)
{
ipfrag_hash_rnd = (u32) ((num_physpages ^ (num_physpages>>7)) ^
(jiffies ^ (jiffies >> 6)));
init_timer(&ipfrag_secret_timer);
ipfrag_secret_timer.function = ipfrag_secret_rebuild;
ipfrag_secret_timer.expires = jiffies + sysctl_ipfrag_secret_interval;
add_timer(&ipfrag_secret_timer);
}
ipfrag_secret_rebuild
//对全局的ipq散列表进行重组
static void ipfrag_secret_rebuild(unsigned long dummy)
{
unsigned long now = jiffies;
int i;
write_lock(&ipfrag_lock);
//重新获取ipfrag_hash_rnd随机值
get_random_bytes(&ipfrag_hash_rnd, sizeof(u32));
//遍历ipq散列表中所有ipq,根据新的ipfrag_hash_rnd值把这些ipq重新连接到散列表对应的桶中
for (i = 0; i < IPQ_HASHSZ; i++) {
struct ipq *q;
struct hlist_node *p, *n;
hlist_for_each_entry_safe(q, p, n, &ipq_hash[i], list) {
unsigned int hval = ipqhashfn(q->id, q->saddr,
q->daddr, q->protocol);
if (hval != i) {
hlist_del(&q->list);
hlist_add_head(&q->list, &ipq_hash[hval]);
}
}
}
write_unlock(&ipfrag_lock);
//重新设置重构定时器的下次到期时间
mod_timer(&ipfrag_secret_timer, now + sysctl_ipfrag_secret_interval);
}
清除超时的 IP 分片
在复杂的网络环境下,一个 IP 数据报的分片有可能不能全部抵达目的地址,而该数据报已到达的分片会占用大量的资源,此外也为了防止抵御 DoS 攻击,因此需要设置一个时钟,一旦超时,数据报的分片还未全部到达,则将其已到达的分片全部清除。
每当收到一个属于新的 IP 数据报分片时,在为其创建 ipq 时,会初始化其超时定时器 ip_expire
static struct ipq *ip_frag_create(struct iphdr *iph, u32 user)
{
...
qp->timer.function = ip_expire;
...
}
ip_expire
//组装超时定时器例程,当定时器激活时,清除在规定时间内没有完成组装的ipq及其所有分片
static void ip_expire(unsigned long arg)
{
struct ipq *qp = (struct ipq *) arg;
spin_lock(&qp->lock);
//当前已是COMPLETE状态,不做处理,直接跳到释放ipq及其所有的分片处
if (qp->last_in & COMPLETE)
goto out;
//将ipq从ipq散列表和ipq_lru_list链表中删除
ipq_kill(qp);
IP_INC_STATS_BH(IPSTATS_MIB_REASMTIMEOUT);
IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);
//若第一个分片已经到达,则发送分片组装超时ICMP出错报文
if ((qp->last_in&FIRST_IN) && qp->fragments != NULL) {
struct sk_buff *head = qp->fragments;
if ((head->dev = dev_get_by_index(qp->iif)) != NULL) {
icmp_send(head, ICMP_TIME_EXCEEDED, ICMP_EXC_FRAGTIME, 0);
dev_put(head->dev);
}
}
out:
spin_unlock(&qp->lock);
//释放ipq及其所有的ip分片
ipq_put(qp, NULL);
}
垃圾收集
为了控制 IP 组装所占用的内存,设置了两个阈值 ipfrag_high_thresh 和 ipfrag_low_thresh 。当前 ipq 散列表占用的内存量存储在全局变量 ip_frag_mem 中,当 ip_frag_mem 大于 ipfrag_high_thresh 时,需要调用 ip_evictor() 对散列表进行清理,直到 ip_frag_mem 降低到 ipfrag_low_thresh 。这两个阈值可以在系统运行时通过 proc 文件系统修改。
ip_evictor
该方法主要对ipq中的分片进行条件性的清理。在所有的ipq中,若分片没有到齐,则被删除。
static void ip_evictor(void)
{
struct ipq *qp;
struct list_head *tmp;
int work;
work = atomic_read(&ip_frag_mem) - sysctl_ipfrag_low_thresh;
if (work <= 0)
return;
while (work > 0) {
read_lock(&ipfrag_lock);
//若lru链表为空,解锁后返回
if (list_empty(&ipq_lru_list)) {
read_unlock(&ipfrag_lock);
return;
}
tmp = ipq_lru_list.next;
qp = list_entry(tmp, struct ipq, lru_list);
//递增ipq引用计数
atomic_inc(&qp->refcnt);
read_unlock(&ipfrag_lock);
//在删除分片前后要做同步保护
spin_lock(&qp->lock);
if (!(qp->last_in&COMPLETE))
ipq_kill(qp);
spin_unlock(&qp->lock);
//ipq_put真正删除ipq及其所有分片
ipq_put(qp, &work);
IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);
}
}