文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Linux 内核网络之网络层接收消息:分片组装

2024-11-30 16:13

关注

而每个将被重新组合的 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);
}
}
来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯