截获数据包的几种方式

在实际应用中需要服务端调用libpcap模拟接收TCP连接而不实际进行accept,会存在客户端发了SYN请求,TCP协议栈直接回复了RST导致客户端直接关闭了连接的情况。(一开始是准备使用TCPreplay进行TCP数据重放的,后面发现TCPreplay中的TCPliveplay只是对PF_PACKET类型的数据包进行简单的send(),不过可以基于它提供的框架完成TCP、IP校验和的计算)
libpcap的相关文档介绍如下:
Programming with pcap

通过查阅相关资料,得知想要截获并修改数据包内容,可以使用netfilter/iptables 技术,即设置自己的hook函数,通过hook函数来对相应的数据包执行具体的操作。
在数据包流经内核协议栈的整个过程中,在一些已预定义的关键点上PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT和POST_ROUTING会根据数据包的协议簇PF_INET到这些关键点去查找是否注册有钩子函数。如果没有,则直接返回okfn函数指针所指向的函数继续走协议栈;如果有,则调用nf_hook_slow函数,从而进入到Netfilter框架中去进一步调用已注册在该过滤点下的钩子函数,再根据其返回值来确定是否继续执行由函数指针okfn所指向的函数。
netfilter相关的文档介绍如下:

  • 数据包在内核态得捕获、修改和转发
  • Linux netfilter源码分析
  • Linux下使用netfilter进行IP包解析
  • Netfilter源码分析
  • Linux Netfilter实现机制和扩展技术
  • 洞悉linux下的Netfilter&iptables:什么是Netfilter?

我因为只需要过滤特定IP的回复,所以代码很简单

#include 
#include 
#include 
#include /*PF_INET*/
#include /*NF_IP_PRE_FIRST*/
#include 
#include 
#include  /*in_aton()*/
#include 
#include 
 
#define ETHALEN 14

#define NIPQUAD(addr) \  
  ((unsigned char *)&addr)[0], \   
  ((unsigned char *)&addr)[1], \   
  ((unsigned char *)&addr)[2], \   
  ((unsigned char *)&addr)[3]

  
static struct nf_hook_ops nfho;
 
unsigned int checksum(unsigned int hooknum,
                      struct sk_buff *__skb,
                      const struct net_device *in,
                      const struct net_device *out,
                      int (*okfn)(struct sk_buff *)) 
{
    struct sk_buff *skb;
    struct net_device *dev;
    struct iphdr *iph;
    struct tcphdr *tcph;
    int tot_len;
    int iph_len;
    int tcph_len;
    int ret;
 
    skb = __skb;
    if (skb == NULL)
        return NF_ACCEPT;
 
    iph = ip_hdr(skb);
    if (iph == NULL)
        return NF_ACCEPT;
 
    tot_len = ntohs(iph->tot_len);

    __be32 sip,dip;  
 if(skb){  
   struct sk_buff *sb = NULL;  
   sb = skb;  
   struct iphdr *iph;  
   iph  = ip_hdr(sb);  
   sip = iph->saddr;  
   dip = iph->daddr;  
   //printk("My Packet for source address: %d.%d.%d.%d\n\
   // My destination address: %d.%d.%d.%d\n ", NIPQUAD(sip), NIPQUAD(dip));  
    }   
    if (iph->daddr == in_aton("10.1.65.180"))
    {
        __be32 myip;
        myip = iph->daddr;
        printk("before drop packet\n");
        printk("drop packet, address : %d.%d.%d.%d\n ", NIPQUAD(myip));
        return NF_DROP;
        }
      return NF_ACCEPT;
out:
    dev_put(dev);

    return NF_DROP;
}
static int __init filter_init(void)
{
    printk(KERN_INFO "hello, world!\n");
    int ret;
    nfho.hook = checksum;
    nfho.pf = PF_INET;
    //nfho.hooknum = NF_INET_POST_ROUTING;
    nfho.hooknum = NF_INET_LOCAL_OUT;
    nfho.priority = NF_IP_PRI_FIRST;

    ret = nf_register_hook(&nfho);
    if (ret < 0)
    {
        printk("%s\n", "can't modify skb hook!");
        return ret;
    }

    return 0;
}

static void filter_fini(void)
{
    printk(KERN_INFO "goodbye, cruel world\n");
    nf_unregister_hook(&nfho);
}

module_init(filter_init);
module_exit(filter_fini);

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("myslq");
MODULE_DESCRIPTION("A simple Hello World Module");

这样需要自己实现一个内核模块,编译完成后需要使用insmod hello.ko 加载模块,使用tail -f /vag/log/messags查看printk打印的日志,使用rmmod hello移除内核模块。
内核模块相关的文档如下:
linux内核驱动模块开发步骤及实例入门介绍

Makefile文件也是使用网上通用的版本

ifneq ($(KERNELRELEASE),)
 #kbuild syntax. dependency relationshsip of files and target modules are listed here.
 mymodule-objs := hello.o
 obj-m := hello.o   
else
PWD  := $(shell pwd)
KVER ?= $(shell uname -r)
KDIR := /lib/modules/$(KVER)/build
all:
	$(MAKE) -C $(KDIR) M=$(PWD)
clean:
	rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions
endif

注意,开发内核模块的话,必须有对应内核版本的dev开发包。

[root@host-10-33-43-250 hook_tcp]# rpm -qa|grep kernel
kernel-devel-3.10.0-327.el7.x86_64
kernel-3.10.0-957.1.3.el7.x86_64
kernel-tools-libs-3.10.0-327.el7.x86_64
kernel-headers-3.10.0-327.el7.x86_64
kernel-3.10.0-327.el7.x86_64
kernel-tools-3.10.0-327.el7.x86_64
kernel-devel-3.10.0-862.14.4.el7.x86_64

再后来发现这样真的是杀鸡用牛刀,可以直接使用iptables过滤掉相应的数据包,这样就过滤了指定IP的RST连接,客户端可以调用connect()而不用担心接收到RST导致连接被关闭。
第一条添加规则,第二条在调用完成后删除规则

iptables -t filter -A OUTPUT  -p tcp --tcp-flags RST RST --dst 10.1.65.180 -j DROP
iptables -t filter -D OUTPUT  -p tcp --tcp-flags RST RST --dst 10.1.65.180 -j DROP

iptables的命令行介绍
iptables工具_过滤包—命令

觉得使用system调用iptables命令不美观的话,可以安装iptables-devel库,通过调用libiptc库的应用程序接口设置相应的参数。

  • Linux防火墙netfilter的编程接口libiptc简介
  • Querying libiptc HOWTO
  • Linux下使用libiptc库编程下发规则

Linux c/c++编程使用 libiptc库下发规则可以代替system调用 iptables 命令方法,可以满足程序中批量地创建连接所需的批量增加、删除iptables规则;
使用步骤确实比较繁琐,得注意iptables版本的问题,编译相关库需要“-lip4tc” “-lxtables”,头文件需要“libiptc/libiptc.h” “net/netfilter/nf_nat.h” “iptables.h”;

然而实验结果表明,当多进程高频率得操作iptables时,system 和 libiptc 均会出现 “Resource temporarily unavailable” 错误,建议进行返回值判定和多进程的加锁保护;

因为官方提醒ibiptc不会提供稳定的对外接口,并不建议开发者使用。

  • Programmatically managing iptables rules in C: IPTC
  • Is there an C/C++ API for adding/removing rules?

建议直接使用popen方式输入命令执行。

sprintf(cmd_append_rst_restrict, "iptables -t filter -A OUTPUT  -p tcp --tcp-flags RST RST --dst %s -j DROP 2>/dev/null", input_dip);

#include 
#include 
//#include 
#include 

int execute_cmd(char *cmd)
{
        FILE *fp;

        if((fp = popen(cmd, "r")) == NULL)
        {
                perror("failed to popen\n");
                exit(1);
        }

do{
        char* buf2 = (char *)malloc(PIPE_MAX_SIZE);
        memset(buf2, 0, PIPE_MAX_SIZE);

        while(fgets(buf2, PIPE_MAX_SIZE, fp) != NULL)
        {
                printf("%s", buf2);
        }
        printf("result: %s", buf2);
}while(0);
        pclose(fp);
        return 0;
}

IP报文头部信息

	struct ip_addr{
	    unsigned char byte1;
	    unsigned char byte2;
	    unsigned char byte3;
	    unsigned char byte4;
	};
	
	
	typedef struct ipv4_hdr ipv4_hdr;
	struct ipv4_hdr{
	    u_int8_t ip_hl:4;
	    u_int8_t ip_v:4;
	
	    u_int8_t ip_tos;
	    u_int16_t ip_len;
	    u_int16_t ip_id;
	    u_int16_t ip_off;
	    u_int8_t ip_ttl;
	    u_int8_t ip_p;
	    u_int16_t ip_sum;
	    input_addr ip_src, ip_dst;
	};

在实际中进行TCP报文的重放中,发现存在IP包中设置了DF(don’t fragment )标记位,但是IP包总长度大于MTU的情况,如下图。(可能和抓包是从KVM虚拟化环境虚拟机中抓取的有关,虽然虚拟机以太网卡MTU也是1500,但是不会对大于MTU的包进行丢弃。)
截获数据包的几种方式_第1张图片
此种情况下,需要手动对包含以太帧的数据进行IP分片。分片具体代码如下,date为指向以太帧的const u_char *类型指向, size_t len 为前述数据的长度:

if(!exceed_mtu_flag)
        {
                retcode = (int)send(sp->handle.fd, (void *)data, len, 0);
        }
        else
        {
        tcp_hdr *tcphdr;
        ipv4_hdr *iphdr;
        unsigned int size_ip;
        unsigned int size_tcp;
        unsigned int size_ip_payload;

                u_int16_t ip_header_id = my_rand();
                u_int16_t tcp_bytes_offset = 0;// ip_off 只有低13位表示偏移长度,但是长度不超过1500,don't worry.
                u_int16_t frage_off = 0;//分片
                unsigned int ip_payload_sent_size = 0;
           //while(left_tcp_data_len > 1460)

                u_int16_t frage_max_off = 0;
                        //tcp_data(max 1460) + tcp_header 20 + ip_header 20 <= mtu 1500, the len also have eth_header 14.
                        //printf("warning, the length of data is %d, exceed the MTU and would be dropped", len);
                        //ipv4_hdr *iphdr;
                        iphdr = (ipv4_hdr *)(data + SIZE_ETHERNET);

                        size_ip = iphdr->ip_hl << 2;
                        if (size_ip < 20) {
                                printf("ERROR: Invalid IP header length: %u bytes\n", size_ip);
                                return 0;
                        }
                        //tcphdr = (tcp_hdr *)(data + SIZE_ETHERNET + size_ip);

                        //th_off is the offset of tcp header before payload.And the value should multiple 4.
                        //size_tcp = tcphdr->th_off*4;
                        //if (size_tcp < 20) {
                        //      printf("ERROR: Invalid TCP header length: %u bytes\n", size_tcp);
                        //return 0;
                        //}
                        /* payload = (u_char *)(sched[i].packet_ptr + SIZE_ETHERNET + size_ip + size_tcp); */
                        size_ip_payload = ntohs(iphdr->ip_len) - (size_ip);

                        if(size_ip_payload != len - SIZE_ETHERNET - size_ip)
                        {
                                printf("ERROR: Invalid length %d, real length %d\n", size_ip_payload, len - SIZE_ETHERNET - size_ip);
                        }
						frage_max_off = size_ip_payload / 8;
                        //left_ip_payload_size = size_ip_payload;

                        //iphdr->ip_len > len;  
                        //iphdr->ip_len = htons(1500);//IP 报文中的长度为除以太帧首部的其负载长度

                //uint32_t tcpr_random(uint32_t *seed);
                //The cast is unnecessary. Assignment, initialization, parameter passing, and return statements can assign values between any numeric types without a cast. The value is converted implicitly:rvalue type to lvalue value.But for easy understanding, I would write it explicitly. convert int to short int 
                        //iphdr->ip_id = (u_int16_t)my_rand();  
                        iphdr->ip_id = ip_header_id;


                        char *send_buf = (char *) malloc(len + 1);
                         if(send_buf == NULL) return FALSE;
                        memset(send_buf, 0, len + 1);
                        while(ip_payload_sent_size != size_ip_payload)
                        {
                                if(size_ip_payload - ip_payload_sent_size > 1514 - SIZE_ETHERNET - size_ip)
                                {
                                        iphdr->ip_len = htons(1514 - SIZE_ETHERNET);
                                        //iphdr->ip_off = 0;//以免影响后续的设置
                                        iphdr->ip_off = frage_off;
                                        iphdr->ip_off = htons(iphdr->ip_off | IP_MF);//#define IP_MF 0x2000U

                                        //ip_payload_sent_size += 1514 - SIZE_ETHERNET - size_ip;
                                        do_checksum_ip_header((u_char *) iphdr);
                                        memcpy(send_buf, data, SIZE_ETHERNET + size_ip);//copy the head
                                        memcpy(send_buf + SIZE_ETHERNET + size_ip, data + ip_payload_sent_size + SIZE_ETHERNET + size_ip, 1514 - SIZE_ETHERNET - size_ip);

                                        //ip_payload_sent_size += 1514 - SIZE_ETHERNET - size_ip;

                                        //send data
                                        retcode = (int)send(sp->handle.fd, (void *)send_buf, 1514, 0);

                                        ip_payload_sent_size += 1514 - SIZE_ETHERNET - size_ip;
                                }
                                else
                                {
                                        iphdr->ip_len = htons(size_ip_payload - ip_payload_sent_size + size_ip);
                                        iphdr->ip_off = htons(frage_off);

                                        //ip_payload_sent_size = size_ip_payload;

                                        do_checksum_ip_header((u_char *) iphdr);

                                        memcpy(send_buf, data, SIZE_ETHERNET + size_ip);//copy the head
                                        memcpy(send_buf + SIZE_ETHERNET + size_ip, data + ip_payload_sent_size + SIZE_ETHERNET + size_ip, size_ip_payload - ip_payload_sent_size);
                                        retcode = (int)send(sp->handle.fd, (void *)send_buf, size_ip_payload - ip_payload_sent_size + SIZE_ETHERNET + size_ip, 0);


                                        ip_payload_sent_size = size_ip_payload;

                                }
                                //fix IP header checksum
                        //iphdr->ip_off = tcp_bytes_offset / 8;

                        //retcode = (int)send(sp->handle.fd, (void *)data, len, 0);     
                                frage_off += (1514 - SIZE_ETHERNET - size_ip)/8;
                        //left_tcp_data_len -= 1460;    
                                memset(send_buf, 0, len + 1);

                        }
                        free(send_buf);
                        send_buf = NULL;

        }
                //retcode = (int)send(sp->handle.fd, (void *)data, len, 0);

                printf("retcode %d, desc %s, no %d", retcode, strerror(errno), errno);

其中使用到的一些自定义函数如下。

/**
 * Implementation of rand_r that is consistent across all platforms
 * This algorithm is mentioned in the ISO C standard, here extended
 * for 32 bits.
 * @param: seed
 * @return: random number
 */
uint32_t tcpr_random(uint32_t *seed)
{
  unsigned int next = *seed;
  int result;

  next *= 1103515245;
  next += 12345;
  result = (unsigned int) (next / 65536) % 2048;

  next *= 1103515245;
  next += 12345;
  result <<= 10;
  result ^= (unsigned int) (next / 65536) % 1024;

  next *= 1103515245;
  next += 12345;
  result <<= 10;
  result ^= (unsigned int) (next / 65536) % 1024;

  *seed = next;

  return result;
}

static int my_rand(void)
{
    struct timeval tv;

    unsigned int my_seed = 0;
    {
        gettimeofday(&tv, ((void *)0));
        my_seed = (unsigned int)tv.tv_sec ^ (unsigned int)tv.tv_usec;
    }

    return tcpr_random(&my_seed);
}

static int do_checksum_math_liveplay(u_int16_t *data, int len)
{
    int sum = 0;
    union {
        u_int16_t s;
        u_int8_t b[2];
    } pad;

    while (len > 1) {
        sum += *data++;
        len -= 2;
    }

    if (len == 1) {
        pad.b[0] = *(u_int8_t *)data;
        pad.b[1] = 0;
        sum += pad.s;
    }

    return (sum);
}

static int do_checksum_ip_header(u_int8_t *data)
{
    ipv4_hdr *ipv4;
    int ip_hl;
    volatile int sum = 0;
    ipv4 = NULL;

    ipv4 = (ipv4_hdr *)data;
    ip_hl = ipv4->ip_hl << 2;

        ipv4->ip_sum = 0;
        sum = do_checksum_math_liveplay((u_int16_t *)data, ip_hl);
        ipv4->ip_sum = CHECKSUM_CARRY(sum);

        return 0;
}

参考
https://www.cnblogs.com/x_wukong/p/5923767.html

你可能感兴趣的:(计算机网络)