IP分片报文的接收与重组

article/2025/9/13 6:24:01

对于长度超过接口MTU的数据包,需要进行分片处理,IP报头中与分片相关的字段有如下几个:

Identification       -  用来确认不同的分片是否属于同一个IP报文;
Flags                  -  其中IP_MF表示还有分片,此分片为中间分片;
Fragment Offset -  表示此分片在整个报文中的偏移地址。

了解了这几个字段之后,来看一下内核中的实现。

分片接收队列

内核将接收到的分片报文暂存在一个ipq结构的队列中。由ipq结构的定义与查找匹配函数(ip4_frag_match)可知,以下几个值唯一确定一个分片队列:

user         重组报文的用户
saddr       源IP地址
daddr       目的IP地址
id              IP报文ID标识
protocol    传输层协议号
vif             L3 master device index

user
重组报文的用户,也就是重组之后报文的使用者。例如在ip_local_deliver中调用重组函数(ip_defrag),user参数使用的是IP_DEFRAG_LOCAL_DELIVER值,即此处的数据包重组是为了要传给本机的上层应用程序使用。另外,netfilter的透明代理(tproxy)和socket匹配也需要将所有分片进行重组,如是此目的,user参数使用IP_DEFRAG_CONNTRACK_IN。user参数的引入可使内核对同一组数据分片同时进行不同目的的重组,完整的user值参见内核代码ip_defrag_users枚举类型定义。

saddr#daddr#id#protocl
四个IP报头的字段。

vif
L3mdev设备索引。l3mdev用于实现VRF(Virtual Forwarding and Routing)功能,不同的VRF之间是三层相互隔离的,在两个VRF中可存在其它几个参数相同的数据包,此时需要vif索引加以区分。

struct ipq {struct inet_frag_queue q;u32         user;__be32      saddr;__be32      daddr;__be16      id;u8          protocol;int         vif;   /* L3 master device index */
};

实际的分片保存在结构体ipq中的成员q(inet_frag_queue)内,其中fragments指向分片队列的头,fragments_tail指向分片的队列尾。

struct inet_frag_queue {struct hlist_node   list;struct sk_buff      *fragments;struct sk_buff      *fragments_tail;
};

 

分片存储结构

IP分片在内核中分两级存储。其一,根据IP报头的4个字段计算得到一个hash值,数据包按照此hash值散列于相应的bucket中。此hash数组大小为1024。所以,此处的查找非常简单,只需要将计算得到的hash值作为索引(ip4_frags.hash[hash])即可得到相应的bucket。全局变量ip4_frags保存有所有ipv4相关的分片信息。

struct inet_frags {
    struct inet_frag_bucket hash[INETFRAGS_HASHSZ];
};
static struct inet_frags ip4_frags;

hash = ipqhashfn(iph->id, iph->saddr, iph->daddr, iph->protocol);

其二,在inet_frag_bucket结构中,chain链表用于将所有已接收到的分片通过分片队列结构(inet_frag_queue)中的list成员链接起来。但是上文提到的ipq结构又在什么地方呢?实际内核代码中inet_frag_queue属于ipq的一部分,为ipq的第一个成员,内核并不单独分配inet_frag_queue结构,而是通过分配一个ipq将其一并创建出来。所以chain链表上也可以说是链接的ipq结构。

struct inet_frag_bucket {
    struct hlist_head   chain;
    spinlock_t      chain_lock;
};

#define INETFRAGS_MAXDEPTH  128

以chain开头的链表长度最大为128,即内核最大能接收具有128个分片的数据包。

分片队列查找

内核在接收到一个分片时,首先查找是否已接收过同一个报文的其它分片。查找过程在分片存储的二级结构中进行,第一级通过IP报头的id、saddr、daddr和 protocol字段找到相应bucket。

第二级遍历bucket的chain链表,找到正确的ipq分片队列,匹配函数见ip4_frag_match实现。

static bool ip4_frag_match(const struct inet_frag_queue *q, const void *a)
{return  qp->id == arg->iph->id &&qp->saddr == arg->iph->saddr &&qp->daddr == arg->iph->daddr &&qp->protocol == arg->iph->protocol &&qp->user == arg->user &&qp->vif == arg->vif;
}

如果是数据包的第一个分片没有ipq,此时如果chain链表的长度还没有超出INETFRAGS_MAXDEPTH(128),并且分片队列所占内存没有超出高阈值,分配一个新的ipq队列结构,添加到chain链表上。

 

分片的插入

现在找到了当前接收的分片所需放入的队列ipq(ipq->inet_frag_queue),需要考虑插入的位置了。在结构inet_frag_queue中,成员fragments(struct sk_buff)指向第一个分片,fragments_tail指向最后一个。分片之间通过sk_buff的next成员组成一个单向链表,分片按照IP头部OFFSET字段的有小到大依次排列。来看插入处理函数ip_frag_queue。

a)正常情况下顺序接收到分片数据包,当前接收到的分片的OFFSET就会大于已接收的最后一个分片的OFFSET,或者如果是接收到第一个分片报文,分片链表末尾fragments_tail为空,此两种情况下,当前接收的分片都需要添加到sk_buff链表末尾,仅需要获得前一个sk_buff(prev)指针。

    prev = qp->q.fragments_tail;if (!prev || FRAG_CB(prev)->offset < offset) {next = NULL;goto found;}

b)接收到乱序分片。需要遍历sk_buff分片链表查找合适插入位置,获取前一个(prev)与后一个(next)分片的sk_buff指针。

    for (next = qp->q.fragments; next != NULL; next = next->next) {if (FRAG_CB(next)->offset >= offset)break;  /* bingo! */prev = next;}

c)丢弃不合法分片。正常情况下,每接收一个分片就将队列的接收计数加一,同时将相应的对端系统的接收计数加一,二者一致。但是,内核有可能在此期间接收到相同的源地址设备发送的另外一组需要分片的数据流,其会对应到另外一个分片队列,将会导致内核的对端系统(peer->rid)接收计数增加。此后,再此接收到前一个队列的分片时,分片队列ipq的rid就会小于对端系统的rid,如果二者的差值大于64时,内核认为是非法的分片,将会丢弃整个分片队列。

static int ip_frag_too_far(struct ipq *qp)
{struct inet_peer *peer = qp->peer;unsigned int max = qp->q.net->max_dist;start = qp->rid;end = atomic_inc_return(&peer->rid);qp->rid = end;
}
static int __net_init ipv4_frags_init_net(struct net *net)
{   net->ipv4.frags.max_dist = 64;
}

至此,我们也获得了当前分片的插入位置(prev和next),将分片链接到prev之后,next之前。

    /* Insert this fragment in the chain of fragments. */skb->next = next;if (!next)qp->q.fragments_tail = skb;if (prev)prev->next = skb;elseqp->q.fragments = skb;

当前接收的分片数据包可能与前一个或者后一个已有分片存在重叠部分,需要进行合并。如果与前一个分片(prev)重叠,采用增加当前分片的OFFSET值的方法避开重叠部分;如果是与后一个分片(next)重叠,一种情况是与后一分片的以部分重叠,采用增加后一分片的OFFSET的值来避开重叠部分;另外一种情况是重叠的部分包含整个后一分片,此时就可以free是否掉后一分片,继续检查是否与后后的分片重叠,循环进行处理,直到解决重叠问题为止。

 

分片重组

 

重组的前提是接收到所有的分片。内核判断一个队列是否接收到了所有分片需要满足三个条件:

a)INET_FRAG_FIRST_IN  - 在接收到OFFSET为0值的数据包时设置此标志;
b)INET_FRAG_LAST_IN   - 接收到IP报头中More Fragmentation(IP_MF)标志等于0的分片时,设置此标志位;
c)inet_frag_queue中meat等于len - meat在每次成功插入一个分片后增加此分片的长度值,len值由最后一个分片的OFFSET值加上其长度获得。

    if (qp->q.flags == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) &&qp->q.meat == qp->q.len)err = ip_frag_reasm(qp, prev, dev);

来看重组函数ip_frag_reasm,首先检查最近插入的分片报文是否为数据包的第一个分片,如果不是必定存在前一个分片(prev不为空),此时,如下的代码将使用最近插入的这个分片结构(sk_buff)作为分片链表的头。

首先克隆clone一份最近接收的这一分片,将克隆之后的分片重新链接到分片链表中,替换掉之前的分片。之后将链表头分片(fragments)克隆到最近接收的这一分片中,释放位于链表头的分片,将最近接收分片设置为链表头。

    if (prev) {head = prev->next;fp = skb_clone(head, GFP_ATOMIC);if (!fp)goto out_nomem;fp->next = head->next;if (!fp->next)qp->q.fragments_tail = fp;prev->next = fp;skb_morph(head, qp->q.fragments);head->next = qp->q.fragments->next;consume_skb(qp->q.fragments);qp->q.fragments = head;}

其次检查所有分片的总长度是否超过65535,超出65535的数据包不做重组,函数直接返回。

    ihlen = ip_hdrlen(head);len = ihlen + qp->q.len;if (len > 65535) goto out_oversize;

对于分片链表的头一个分片(head),如果其自身包括分片,需要做一些特殊处理。为其分片数据单独创建一个sk_buff,将其链接在链表头head之后。head仅包含数据区与页面数据存储区。修改相应的长度信息。

    if (skb_has_frag_list(head)) {struct sk_buff *clone;clone = alloc_skb(0, GFP_ATOMIC);clone->next = head->next;head->next = clone;skb_shinfo(clone)->frag_list = skb_shinfo(head)->frag_list;skb_frag_list_init(head);for (i = 0; i < skb_shinfo(head)->nr_frags; i++)plen += skb_frag_size(&skb_shinfo(head)->frags[i]);clone->len = clone->data_len = head->data_len - plen;head->data_len -= clone->len;head->len -= clone->len;}

真正的重组操作,其实很简单。涉及到需要修改的为长度信息,包括data_len、len和truesize。head为最终重组完成后的sk_buff结构。

    for (fp=head->next; fp; fp = fp->next) {head->data_len += fp->len;head->len += fp->len;head->truesize += fp->truesize;}

分片生存时间

现实网络环境中,有可能接收不到一个数据包的所有分片,无法重组数据包将导致这些分片一直驻留在分片队列中。内核采用超时机制处理这些分片。在接收到第一个数据包分片,创建分片队列后,内核随即启动超时计时器,超时时间从网络命名空间结构中获取(timeout), 默认情况下超时时间为30秒钟(IP_FRAG_TIME)。

static struct inet_frag_queue *inet_frag_alloc(struct netns_frags *nf, struct inet_frags *f, ...)
{timer_setup(&q->timer, f->frag_expire, 0);
}
static struct inet_frag_queue *inet_frag_intern(struct netns_frags *nf, ...)
{if (!mod_timer(&qp->timer, jiffies + nf->timeout))refcount_inc(&qp->refcnt);
}#define IP_FRAG_TIME    (30 * HZ)       /* fragment lifetime    */static int __net_init ipv4_frags_init_net(struct net *net)
{   net->ipv4.frags.timeout = IP_FRAG_TIME;
}
void __init ipfrag_init(void)
{   ip4_frags.frag_expire = ip_expire;
}

超时定时器在到期之后,使用ip_expire函数释放分片队列(ipq_put)。如果本机就是这些分片报文的目的主机,回复ICMP的分片重组超时消息(type=ICMP_TIME_EXCEEDED(11), code=ICMP_EXC_FRAGTIME(1))。

 

分片重组内存管理

分片重组系统在初始化时,限定其内存使用不超过4M字节(high_thresh)的内存(基于网络命名空间),如果超过high_thresh,内核会释放一部分分片,将内存使用见底到3M字节(low_thresh)。

每个分片占用的内存使用其sk_buff的truesize值统计(其包括sk_buff结构体占用内存、skb_shared_info结构体占用内存与数据包占用内存的总和)。对于第一个分片,还要分配分片队列(ipq),也要计入到内存占用中。当分片重组或者超时删除之后,减低内容占用统计。

当接收到一个分片报文,查找是否已存在相应的分片队列时,检查当前网络命名空间中分片占用内存是否大于low_thresh,如大于,调唤醒初始化时注册的工作队列函数(inet_frag_worker)释放部分分片占用的内存。
在分配新的分片队列(inet_frag_queue)时,首先检查当前占用内存是否超过high_thresh,如超过不再分片新的分片队列。

struct inet_frag_queue *inet_frag_find(...)
{if (frag_mem_limit(nf) > nf->low_thresh)inet_frag_schedule_worker(f);
}
static struct inet_frag_queue *inet_frag_alloc(...)
{if (!nf->high_thresh || frag_mem_limit(nf) > nf->high_thresh) {inet_frag_schedule_worker(f);return NULL;}
}

由于内存超限,inet_frag_worker函数每次执行最多扫描128个bucket中的分片队列,每次最多释放512个分片队列(inet_frag_queue)。执行完成之后记录最后扫描的bucket索引,下次被唤醒时,由此索引开始继续扫描。

#define INETFRAGS_EVICT_BUCKETS   128
#define INETFRAGS_EVICT_MAX   512

 

分片队列重建

分片bucket中的链表chain深度超过128时(系统中同时进行处理重组数据包的用户(IP_DEFRAG_LOCAL_DELIVER)或者l3mdev过多),将不能够再创建出新的分片队列(ipq)。此时也需要唤醒工作队列(inet_frag_worker)释放部分分片,并且在重建间隔大于5秒钟时,重建分片结构。

#define INETFRAGS_MAXDEPTH  128
#define INETFRAGS_MIN_REBUILD_INTERVAL (5 * HZ)

重建主要是改变了分片的hash值,rebuild重建函数修改了hash函数的参数ip4_frags.rnd,从而导致ip4_frags中以hash值为索引存储在bucket结构中的ipq链表出现索引与hash值不一致的情况。重建就是将二者调整一致。

static void inet_frag_secret_rebuild(struct inet_frags *f)
{get_random_bytes(&f->rnd, sizeof(u32));
}
static unsigned int ipqhashfn(__be16 id, __be32 saddr, __be32 daddr, u8 prot)
{net_get_random_once(&ip4_frags.rnd, sizeof(ip4_frags.rnd));return jhash_3words((__force u32)id << 16 | prot, (__force u32)saddr, (__force u32)daddr,ip4_frags.rnd);
}
struct inet_frags {struct inet_frag_bucket hash[INETFRAGS_HASHSZ];
}

 

重组参数控制

PROC系统中的控制文件(/proc/sys/net/ipv4):

ipfrag_high_thresh    -  分片占用内存的高阈值
ipfrag_low_thresh      -  分片占用内存的低阈值
ipfrag_max_dist         -  分片有效的最长间隔距离,表示允许的分片队列中接收计数(ipq->rid)与对端系统(inet_peer->rid)中的接收计数的最大差值。 从同一个源地址接收到的分片
ipfrag_secret_interval  -  目前没有在使用。今后可能用于控制分片重组系统重建时间间隔
ipfrag_time                    -  分片超时时间

 

内核版本

Linux-4.15

 


http://chatgpt.dhexx.cn/article/O7as0caH.shtml

相关文章

数据 分片

背景 传统的将数据集中存储至单一节点的解决方案&#xff0c;在性能、可用性和运维成本这三方面已经难于满足海量数据的场景。 从性能方面来说&#xff0c;由于关系型数据库大多采用 B 树类型的索引&#xff0c;在数据量超过阈值的情况下&#xff0c;索引深度的增加也将使得磁…

MongoDB:海量存储基础-分片架构

文章目录 1.分片架构2.分片策略3.读写策略4.数据迁移 1.分片架构 分片是数据库中常用的提升存储容量的方式&#xff0c;它基于水平拓展的思想&#xff0c;将大量的数据按一定规则分配到不同的服务器中存储&#xff0c;比如MySQL集群可以借助Sharding-JDBC等外部框架对数据进行…

SpringBoot文件分片上传

背景 最近好几个项目在运行过程中客户都提出文件上传大小的限制能否设置的大一些&#xff0c;用户经常需要上传好几个G的资料文件&#xff0c;如图纸&#xff0c;视频等&#xff0c;并且需要在上传大文件过程中进行优化实时展现进度条&#xff0c;进行技术评估后针对框架文件上…

芯片的设计流程和流片成本

每天都在用&#xff0c;但你知道芯片的设计流程和流片成本吗&#xff1f; 2017-05-10 06:10 来源&#xff1a;半导行业观察 芯片&#xff0c;是无数设计工程师们烧死很多脑细胞后产生的作品&#xff0c;完全可以称得上是当代的艺术品。无论是电工们&#xff0c;还是科技小白…

华为防火墙分片缓存

分片缓存 分片缓存功能用来缓存先于首片分片报文到达的后续分片报文&#xff0c;避免分片报文被防火墙丢弃。 网络设备在传输报文时&#xff0c;如果设备上配置的MTU&#xff08;Maximum Transfer Unit&#xff09;小于报文长度&#xff0c;则会将报文分片后继续发送。 我们知道…

二三层报头及IP分片详解

一、报文数据格式 二、二层数据格式 二层以太帧格式 DMAC&#xff08;6字节&#xff09;SMAC&#xff08;6字节&#xff09;type&#xff08;2字节&#xff09;DATA&#xff08;46-1500字节&#xff09;CRC&#xff08;4字节&#xff09; 字段长度含义DMAC6字节目的MAC地址&am…

哈希分片总结

主要参考&#xff1a; 《大数据日知录》 https://zhuanlan.zhihu.com/p/34985026 什么是哈希分片 后台随着数据规约的越来越大&#xff0c;单机明显无法存储着庞大的数据量&#xff0c;只能依靠大规模集群在对数据进行存储和处理&#xff0c;所以系统的可扩展性也成为了很重要…

MongoDB 分片

分片 sharding 分片是指将数据拆分,将起分散到不同服务器的过程.,将数据分散到不同的服务器上,可以存储更多的数据,处理更大的负载 MongoDB分片机制允许你创建一个包含许多服务器(分片)的集群,将子数据分散在集群中.每个分片维护着一个数据集合的子集,相比单服务器和副本集&…

LaTeX制作幻灯片

LaTeX \LaTeX LATE​X幻灯片制作采用的是beamer类。beamer类默认使用sans serif family&#xff0c;和article不同&#xff0c;article默认是roman font family。所以如果我要实现幻灯片的标题和正文字体的设置可以通过下面的命令&#xff1a; \setsansfont{TeX Gyre Termes} …

MySQL的分片

前言 从开发人员的角度来说&#xff0c;为什么要了解和掌握MySQL分片&#xff1f; 第一&#xff0c;了解MySQL分片可以更合理地定制分片策略&#xff0c;选分片字段是要讲科学的。 第二&#xff0c;了解MySQL分片以后如果出现故障报错&#xff0c;也有助于问题的排查。 第三…

外汇天眼:新手如何模拟炒外汇?模拟炒外汇的一点心得分享

模拟炒外汇通常是在没有进行外汇交易之前利用外汇交易软件的模拟账户进行无风险的外汇交易。是刚入门的投资者为熟悉外汇交易软件和外汇市场而经常使用的方法。 新手如何模拟炒外汇&#xff1f; 外汇市场的火热让不少投资者纷纷加入&#xff0c;很多高手也都是从新手走过来的…

外汇套利原理及策略EA

外汇套利&#xff0c;通俗解释就是当相同货币对或相关货币对之间的价差偏离正常范围时进行买卖&#xff0c;当价差回归正常范围时获利平仓的一种交易策略。 外汇套利分为三类 单品种的跨平台套利两相关品种的对冲套利三相关品种的三角套利 如何开发三角套利策略 “三角” 是…

外汇平台怎么选择?玩外汇又有那些优势呢?

外汇平台怎么选择&#xff1f;玩外汇又有那些优势呢? 怎么选择好的外汇平台呢? 第一个 是优先选择大炒外汇的交易平台&#xff0c;虽然也不排除一些小平台是优质的&#xff0c;不过对于新手投资者来说在判断上是很难下决断的。选大平台的主要原因是可以搜索相关的口碑&…

创建模拟外汇网站

1.创建网站模板 2.创建网站服务器 1.创建网站模板 打开pycharm&#xff0c;File→New Project,创建项目文件夹project1 单击项目文件project1&#xff0c;右键新建一个名称为rates.csv的文件。 输入以下内容&#xff1a; 交易币,交易币单位,现汇卖出价,现钞卖出价,现汇买入价…

量化交易之平台搭建

该篇主要是是用来展示量化交易的效果&#xff0c;不构成任何投资建议&#xff0c;仅供参考 搭建的环境: ​ 系统 linux-centos7 python环境&#xff1a; ​ python3.7.4 先安装好我们的库: cd demo pip install -r requirements.txt -i https://pypi.douban.com/simple安…

外汇交易平台怎么选择?

近年来&#xff0c;外汇业发展可谓是生机勃勃&#xff0c;形形色色外汇交易商如雨后春笋般涌出&#xff0c;铺天盖地的外汇广告不停袭击着你的眼球&#xff0c;一夜暴富的神话天天都在上演……外汇市场一片繁荣。市场繁荣说明大众投资理财意识正日渐成熟&#xff0c;但是这个杂…

五步看平台,选好安全的MT4交易外汇平台

在选择MT4外汇交易平台时&#xff0c;如何避免不正规的平台?以下是5招教你远离不正规MT4交易外汇平台。 1. 查看平台是否取得合法许可证 首要考虑的是该平台是否取得了合法的许可证。许可证是证明一个交易平台合法的最重要证据&#xff0c;因此务必在选择时核查该平台的许可证…

结算平台的搭建

写在前头的话&#xff1a; 希望这一系列可以多多帮到一些人——想要做后端产品经理的人、正在做后端产品经理的人、已经放弃了后端产品的人。博主产品经理的资历不深&#xff0c;是一个”复盘“派。 因为涉及部分商业信息&#xff0c;所以时隔了好几年后&#xff0c;才在这里…

如何搭建自己的外汇平台,教你如何搭建外汇平台,如何省钱

外汇市场从世纪之初进入中国,到如今有十几个年头。从起初耳熟能详的几个平台商到现在如雨后春笋般出现,中国的外汇市场越来越开放,价格成本也越来越透明。很多外汇代理商不断发展壮大,对搭建自己的平台有了需求。 开外汇平台赚钱,是一个普遍流传的说法。但是开平台到底有…

泰坦科技MT4平台搭建提供一站式白标方案 快速开启外汇经纪业务

众所周知&#xff0c;外汇市场是全球最大的金融市场&#xff0c;而利用白标技术成为外汇服务提供商是很多人选择运营外汇业务的第一步。相对于外汇主标需要购买整套系统和独立服务器这样的高额成本&#xff0c;搭建一个白标平台要容易的多&#xff0c;也可以相对较快地开展外汇…