tcpip协议栈Linux源码分析五IPv6分⽚报⽂重组分析⼀做防⽕墙模块的时候遇到过IPv6分⽚报⽂处理的问题是,当时的问题是netfilter⽆法基于传输层的端⼝拦截IPv6分⽚报⽂,但是IPv4的分⽚报⽂可以。分析了内核源码得知是因为netfilter的连接跟踪模块重组了IPv4分⽚报⽂,但是对于IPv6的分⽚报⽂没有重归组导致,⾃3.10.x版本后的内核修改了这⼀块,在PRE_ROUTING前netfilter重组了IPv6分⽚报⽂。
之前写过⼏篇博客分析了IPv4分⽚报⽂的处理,接下来分析下IPv6分⽚报⽂的处理。IPv6分⽚报⽂重组原理基本上和IPv4类似,都需要维护⼀个分⽚链表,都有定时器处理垃圾回收等等,细节⽅⾯上略有不同
内核版本:3.4.39
IPv6模块启动的时候会去注册分⽚报⽂处理函数以及IPv6报⽂处理函数。
⽹卡驱动收到IPv6报⽂后查协议处理函数,即ipv6_rcv,这个函数简单处理后传递给pre-routing,如果没有意外就传递给
ip6_rcv_finish(),这个函数就是查路由,然后调⽤相应的处理函数,如果是发给本机的就调⽤ip6_input(),在这个函数⾥⾯就是让local-in链上的钩⼦函数处理⼀遍,没有意外的话就传递给ip6_input_finish(),在这个函数⾥⾯会遍历各个传输层协议处理函数,⽐如路由选项、⽬的地选项、分⽚选项、tcp协议或者udp协议等等,我们主要关注分⽚选项的处理。完整的流程图如下:
分⽚报⽂是调⽤ipv6_frag_rcv函数来处理,在看这个函数之前,先看下ipv6分⽚表的组织图:
收到分⽚报⽂后会根据报⽂的三元素(saddr, daddr, ip ID)结合⼀个随机数rnd计算⼀个哈希值,然后根据这个哈希值去查哈希数组,每个数组元素由⼀个链表组成,链表中挂着哈希值相同的分⽚队列,
收到报⽂则去查匹配的分⽚队列,匹配了则插⼊进去,如果所有分⽚报⽂都集齐了则开始重组。⽤于计算哈希值的随机数rnd不是固定的,内核会起⼀个定时器定期修改该值,然后重新分配分⽚队列,这样做是为了防⽌攻击。因为分⽚队列占⽤系统内存,如果⼀直都⽆法集齐的话,这段内存就会被垃圾回收定时器回收,回收分⽚内存的时候按照FIFO的原则,上图中的lru链表就是⽤来处理这个的。
看下分⽚选项处理函数ipv6_frag_rcv()的实现:
static int ipv6_frag_rcv(struct sk_buff *skb)
{
struct frag_hdr *fhdr;
struct frag_queue *fq;
const struct ipv6hdr *hdr = ipv6_hdr(skb);
struct net *net = dev_net(skb_dst(skb)->dev);
//防⽌分⽚嵌套,分⽚报⽂会在下⾯设置这个标志位,这⾥是防⽌存在多个分⽚选项头
if (IP6CB(skb)->flags & IP6SKB_FRAGMENTED)
goto fail_hdr;
//增加统计计数
IP6_INC_STATS_BH(net, ip6_dst_idev(skb_dst(skb)), IPSTATS_MIB_REASMREQDS);
/* Jumbo payload inhibits frag. header */
//jumbo 类型特⼤报⽂不允许分⽚
if (hdr->payload_len==0)
goto fail_hdr;
//设置分⽚选项头指针
if (!pskb_may_pull(skb, (skb_transport_offset(skb) +
sizeof(struct frag_hdr))))
goto fail_hdr;
hdr = ipv6_hdr(skb);
//获取分⽚头部指针
fhdr = (struct frag_hdr *)skb_transport_header(skb);
//检查⽚偏移和MF标志位是否合法,不合法则设置IP6SKB_FRAGMENTED标志位
//并返回1
if (!(fhdr->frag_off & htons(0xFFF9))) {
/* It is not a fragmented frame */
skb->transport_header += sizeof(struct frag_hdr);
IP6_INC_STATS_BH(net,
ip6_dst_idev(skb_dst(skb)), IPSTATS_MIB_REASMOKS);
IP6CB(skb)->nhoff = (u8 *)fhdr - skb_network_header(skb);
//设置⼀个标志位
IP6CB(skb)->flags |= IP6SKB_FRAGMENTED;
return 1;
}
//如果分⽚报⽂占⽤内存超过阈值,则调⽤ip6_evictor释放部分旧的分⽚报⽂
if (atomic_read(&net->) > net->ipv6.frags.high_thresh)
ip6_evictor(net, ip6_dst_idev(skb_dst(skb)));
//根据源地址,⽬的地址,IP ID去分⽚表中到相应的分⽚哈希队列,到则返回
//不到则新建⼀个,该函数返回失败的唯⼀可能性是创建失败。
fq = fq_find(net, fhdr->identification, &hdr->saddr, &hdr->daddr);
if (fq != NULL) {
int ret;
spin_lock(&fq->q.lock);
//到队列后则进⾏⼊队或重组操作
ret = ip6_frag_queue(fq, skb, fhdr, IP6CB(skb)->nhoff);
spin_unlock(&fq->q.lock);
fq_put(fq);
return ret;
}
IP6_INC_STATS_BH(net, ip6_dst_idev(skb_dst(skb)), IPSTATS_MIB_REASMFAILS);
kfree_skb(skb);
return -1;
fail_hdr:
IP6_INC_STATS(net, ip6_dst_idev(skb_dst(skb)), IPSTATS_MIB_INHDRERRORS);
icmpv6_param_prob(skb, ICMPV6_HDR_FIELD, skb_network_header_len(skb));
return -1;
}
到分⽚队列后则将该报⽂插⼊到队列⾥合适的位置,这个处理交给ip6_frag_queue函数完成:
static int ip6_frag_queue(struct frag_queue *fq, struct sk_buff *skb,
struct frag_hdr *fhdr, int nhoff)
{
struct sk_buff *prev, *next;
struct net_device *dev;
int offset, end;
struct net *net = dev_net(skb_dst(skb)->dev);
//重组完成或者队列被GC回收了会设置该标志位,这时候收到后续报⽂直接丢弃即可。
if (fq->q.last_in & INET_FRAG_COMPLETE)
goto err;
//⽚偏移都是8字节的整数倍
offset = ntohs(fhdr->frag_off) & ~0x7;
//获取可分⽚数据部分长度,⽤payload_len长度减去其它扩展选项头长度
end = offset + (ntohs(ipv6_hdr(skb)->payload_len) -
((u8 *)(fhdr + 1) - (u8 *)(ipv6_hdr(skb) + 1)));
((u8 *)(fhdr + 1) - (u8 *)(ipv6_hdr(skb) + 1)));
//分⽚报⽂最⼤长度不能超过IPV6_MAXPLEN(65535)
if ((unsigned int)end > IPV6_MAXPLEN) {
IP6_INC_STATS_BH(net, ip6_dst_idev(skb_dst(skb)),
IPSTATS_MIB_INHDRERRORS);
icmpv6_param_prob(skb, ICMPV6_HDR_FIELD,
((u8 *)&fhdr->frag_off -
skb_network_header(skb)));
return -1;
}
//重新计算校验和
if (skb->ip_summed == CHECKSUM_COMPLETE) {
const unsigned char *nh = skb_network_header(skb);
skb->csum = csum_sub(skb->csum,
csum_partial(nh, (u8 *)(fhdr + 1) - nh,
0));
}
/* Is this the final fragment? */
if (!(fhdr->frag_off & htons(IP6_MF))) {
/* If we already have some bits beyond end
* or have different end, the segment is corrupted.
*/
/
/已经收到最后的分⽚了,检查长度是否是否有异常,没有的话
//更新长度并设置标志位
if (end < fq->q.len ||
((fq->q.last_in & INET_FRAG_LAST_IN) && end != fq->q.len))
goto err;
fq->q.last_in |= INET_FRAG_LAST_IN;
fq->q.len = end;
} else {
/* Check if the fragment is rounded to 8 bytes.
* Required by the RFC.
*/
/
/如果不是最后⼀个分⽚报⽂则end必须是8字节整数倍,否则按照协议报错  if (end & 0x7) {
/* RFC2460 says always send parameter problem in
* this case. -DaveM
*/
IP6_INC_STATS_BH(net, ip6_dst_idev(skb_dst(skb)),
IPSTATS_MIB_INHDRERRORS);
icmpv6_param_prob(skb, ICMPV6_HDR_FIELD,
offsetof(struct ipv6hdr, payload_len));
return -1;
}
if (end > fq->q.len) {
/
* Some bits beyond end -> corruption. */
tcpip路由协议
//长度不匹配,丢弃该报⽂
if (fq->q.last_in & INET_FRAG_LAST_IN)
goto err;
//更新长度
fq->q.len = end;
}
}
//数据长度为0,这种情况直接丢弃该报⽂
if (end == offset)
goto err;
/
/将data指针指向数据部分
/* Point into the IP datagram 'data' part. */
if (!pskb_pull(skb, (u8 *) (fhdr + 1) - skb->data))
goto err;
goto err;
//将数据调整到线性缓存区
if (pskb_trim_rcsum(skb, end - offset))
goto err;
/* Find out which fragments are in front and at the back of us
* in the chain of fragments so far.  We must know where to put
* this fragment, right?
*/
prev = fq->q.fragments_tail;
if (!prev || FRAG6_CB(prev)->offset < offset) {
next = NULL;
goto found;
}
prev = NULL;
for(next = fq->q.fragments; next != NULL; next = next->next) {
if (FRAG6_CB(next)->offset >= offset)
break; /* bingo! */
prev = next;
}
found:
/* RFC5722, Section 4, amended by Errata ID : 3089
*                          When reassembling an IPv6 datagram, if
*  one or more its constituent fragments is determined to be an
*  overlapping fragment, the entire datagram (and any constituent
*  fragments) MUST be silently discarded.
*/
/* Check for overlap with preceding fragment. */
//根据RFC5722,如果分⽚报⽂数据部分有重叠的话则丢弃整个分⽚队列 if (prev &&
(FRAG6_CB(prev)->offset + prev->len) > offset)
goto discard_fq;
/* Look for overlap with succeeding segment. */
if (next && FRAG6_CB(next)->offset < end)
goto discard_fq;
FRAG6_CB(skb)->offset = offset;
/* Insert this fragment in the chain of fragments. */
skb->next = next;
if (!next)
fq->q.fragments_tail = skb;
if (prev)
prev->next = skb;
else
fq->q.fragments = skb;
dev = skb->dev;
if (dev) {
fq->iif = dev->ifindex;
skb->dev = NULL;
}
//更新分⽚队列时间戳和分⽚队列总长度
fq->q.stamp = skb->tstamp;
fq-&at += skb->len;
//增加分⽚占⽤的内存⼤⼩
atomic_add(skb->truesize, &fq->q->mem);
/* The first fragment.
* nhoffset is obtained from the first fragment, of course.
*/

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。