Linux内核构造数据包并发送(Netfilter方式)

RayDon 2011-03-03

一、构造数据包简析

这里并不详细介绍如何在内核中构造数据包,下文如有需要会在适当的位置进行分析。这里简单的分析讲一下内核态基于Netfilter框架构造数据包的方式。

内核中可以用到的构造数据包的方式,个人认为可以分为两种。

其一,我们直接用alloc_skb申请一个skb结构体,然后根据实际的应用填充不同的成员,或者基于当前数据包的skb,调用skb_copy_expand()函数等新申请一个nskb,并且拷贝skb的内容。

其二,也是个人比较常用的,就是直接在先前接收到的数据包skb上作修改,主要有源IP、目IP,如果是TCP/UDP协议的话,还有源端口目的端口号。总之,就是根据自己的需求去调整数据包的相关成员即可。

通常,这两种方式最终可能都要涉及到重新计算各个部分的校验和,这也是必须的。

二、如何发送构造的数据包

承接上文,数据包已经构造完毕,下一步关键就是如何发送数据包了。个人这里总结的有两种方法。

方法一,就是让数据包接着按照Netfilter的流程进行传输。因为数据包的一些内容已经被更改,尤其是当源IP和目的IP被更改,主要是交换的情况下,是需要确保有路由可查的。

NF框架中查路由的位置一是在PREROUTING之后,而是在LOCALOUT之后。又由于这里是需要将数据包从本地发送出去。因此,可以考虑让修改后的数据包从LOCALOUT点发出。

       内核代码中有这种方式的典型体现。本文涉及的相关内核代码的版本都是2.6.18.3。源文件为ipt_REJECT.c,函数send_reset用于往当前接收到数据包的源IP上发送RST包,整个函数涉及了数据包的构造和发送,这里一起做个简单分析。 
/* Send RST reply */  
  
static void send_reset(struct sk_buff *oldskb, int hook)  
  
{  
  
    struct sk_buff *nskb;  
  
    struct iphdr *iph = oldskb->nh.iph;  
  
    struct tcphdr _otcph, *oth, *tcph;  
  
    struct rtable *rt;  
  
    u_int16_t tmp_port;  
  
    u_int32_t tmp_addr;  
  
    int needs_ack;  
  
    int hh_len;  
    /* 判断是否是分片包*/  
  
    if (oldskb->nh.iph->frag_off & htons(IP_OFFSET))  
  
        return;  
/*得到TCP头部指针*/  
  
    oth = skb_header_pointer(oldskb, oldskb->nh.iph->ihl * 4,  
  
                 sizeof(_otcph), &_otcph);  
  
    if (oth == NULL)  
  
        return;  
    /* 当期收到的包就是RST包,就不用再发送RST包了*/  
  
    if (oth->rst)  
  
        return;  
    /*检查数据包的校验和是否正确*/  
    if (nf_ip_checksum(oldskb, hook, iph->ihl * 4, IPPROTO_TCP))  
        return;  
    /*这一步比较关键,做的就是更新路由的工作。该函数的主要工作就是将当前数据包的源IP当做路由的目的IP,同时考虑数据包的目的IP,得到去往该源IP的路由*/  
  
    if ((rt = route_reverse(oldskb, oth, hook)) == NULL)  
  
        return;  
    hh_len = LL_RESERVED_SPACE(rt->u.dst.dev);  
    /* 拷贝当前的oldskb,包括skb结构体和数据部分。这就是我们上面提到的构造数据包的第一种方式*/  
  
    nskb = skb_copy_expand(oldskb, hh_len, skb_tailroom(oldskb),  
  
                   GFP_ATOMIC);  
  
    if (!nskb) {  
  
        dst_release(&rt->u.dst);  
  
        return;  
  
    }  
    /*因为是拷贝的oldskb,这里不需要再引用了,因此释放对该路由项的引用*/  
    dst_release(nskb->dst);  
    /*将新构造数据包引用的路由指向上面由route_reverse函数返回的新的路由项 */  
    nskb->dst = &rt->u.dst;  
    /* 清除nskb中拷贝过来的oldskb中链接跟踪相关的内容*/  
  
    nf_reset(nskb);  
  
    nskb->nfmark = 0;  
  
   skb_init_secmark(nskb);  
    /*以下就是构造数据包的实际数据部分。如果我们将这里不为nskb新申请缓冲区,而直接指向oldskb的缓冲区,就使我们上面提到的第二种构造数据包的方法。*/  
  
    /*获取nskb的tcp header*/  
  
    tcph = (struct tcphdr *)((u_int32_t*)nskb->nh.iph + nskb->nh.iph->ihl);  
  
    /*交换源和目的IP */  
  
    tmp_addr = nskb->nh.iph->saddr;  
  
    nskb->nh.iph->saddr = nskb->nh.iph->daddr;  
  
    nskb->nh.iph->daddr = tmp_addr;  
  
    /*交换源和目的端口 */  
  
    tmp_port = tcph->source;  
  
    tcph->source = tcph->dest;  
  
    tcph->dest = tmp_port;  
  
  
    /*重置TCP头部的长度,并修改IP头部中记录的数据包的总长度。因为这里是发送RST报文,只需要有TCP的头部,不需要TCP的数据部分*/  
  
    tcph->doff = sizeof(struct tcphdr)/4;  
  
    skb_trim(nskb, nskb->nh.iph->ihl*4 + sizeof(struct tcphdr));  
  
    nskb->nh.iph->tot_len = htons(nskb->len);  
  
    /*重新设置 seq, ack_seq,分两种情况(TCP/IP详解有描述)*/  
  
    if (tcph->ack) { /*原始数据包中ACK标记位置位的情况*/  
  
        needs_ack = 0;  
  
        tcph->seq = oth->ack_seq; /*原始数据包的ack_seq作为nskb的seq*/  
  
        tcph->ack_seq = 0;  
  
    } else { /*原始数据包中ACK标记位没有置位的情况,初始连接SYN或者结束连接FIN等*/  
  
        needs_ack = 1;  
  
        /*这种情况应该是SYN或者FIN包,由于SYN和FIN包都占用1个字节的长度。因此ack_seq应该等于旧包的seq+1即可。这里之所以这样表示,可能是还存在其他情况的数据包。*/  
  
        tcph->ack_seq = htonl(ntohl(oth->seq) + oth->syn + oth->fin  
  
                      + oldskb->len - oldskb->nh.iph->ihl*4  
  
                      - (oth->doff<<2));  
  
        tcph->seq = 0;  
  
    }  
  
    /* RST标记位置1*/  
  
    ((u_int8_t *)tcph)[13] = 0;  
  
    tcph->rst = 1;  
  
    tcph->ack = needs_ack;  
  
     tcph->window = 0;  
  
    tcph->urg_ptr = 0;  
  
    /*重新计算TCP校验和*/  
  
    tcph->check = 0;  
  
    tcph->check = tcp_v4_check(tcph, sizeof(struct tcphdr),  
  
                   nskb->nh.iph->saddr,  
  
                   nskb->nh.iph->daddr,  
  
                   csum_partial((char *)tcph,  
  
                        sizeof(struct tcphdr), 0));  
  
  
    /* 修改IP包的TTL,并且设置禁止分片*/  
  
    nskb->nh.iph->ttl = dst_metric(nskb->dst, RTAX_HOPLIMIT);  
  
    /* Set DF, id = 0 */  
  
    nskb->nh.iph->frag_off = htons(IP_DF);  
  
    nskb->nh.iph->id = 0;  
  
  
    /*重新计算IP数据包头部校验和*/  
  
    nskb->nh.iph->check = 0;  
  
    nskb->nh.iph->check = ip_fast_csum((unsigned char *)nskb->nh.iph,  
  
                       nskb->nh.iph->ihl);  
  
  
    /* "Never happens" */  
  
    if (nskb->len > dst_mtu(nskb->dst))  
  
        goto free_nskb;  
  
/*使nskb和oldskb的链接记录关联*/  
  
    nf_ct_attach(nskb, oldskb);  
/*这里就是最终发送数据包的方式,具体方法就是让新数据包经过LOACLOUT的hook点,然后查路由,最后经由PREROUTING点,将数据包发送出去。 
 
其实这里我还是有1个疑问:(1)为什么不可以直接查找路由,而必须先经过LOCALOUT点;*/  
  
    NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, nskb, NULL, nskb->dst->dev,  
  
        dst_output);  
  
    return;  
free_nskb:  
  
    kfree_skb(nskb);  
}
 其实,这不是丢到了高层,而是和ip_queue_xmit()发送过程意义一样。 

对这包进行重新路由后,封装了头部,之后,放到了NF_IP_LOCAL_IN之前而已。 

其实,这里面只要修改了中途修改了ip地址,肯定是需要手动重新路由的。

这就涉及到一些比较复杂的route cache的查找,如果没有就去查找route tables;之后,进行路由结构和neighbour结构的关联,就涉及到邻居子系统的相关操作;接着就涉及到arp cache的查找,如果没有,进行一些操作,arp的过程等等,才找到了相关的ip对应的mac信息。

转自:http://blog.chinaunix.net/u/33048/showart_2043789.html,作者:Godbach 

相关推荐