article icon indicating copy to clipboard operation
article copied to clipboard

连接跟踪的一种非常暴力的获取方式

Open ICKelin opened this issue 6 years ago • 0 comments

在项目当中有时需要连接跟踪相关的信息,netfilter官网提供了一个libnetfilter_conntrack的用户态库,通过netlink的方式与内核对应的模块通信操作conntrack,但是会有性能瓶颈,还有就是问题不好定位,本文提供一种用户层获取连接跟踪的实现思路,在继续阅读之前,请确保具备以下基本知识:

  • 了解netfilter
  • 能够开发基于netfilter的简单的包过滤防火墙
  • 对skb有一定的了解

解决思路很清晰,在内核层面netfilter钩子当中hook数据包,然后强行在L4和payload之间封装一层协议,有点类似运营商植入广告,将连接跟踪的信息通过这层协议传递给用户态,用户态先解这层协议。再读取数据。

比如说,用户层比较关心DNAT之前的目的地址(origin_ip)和目的端口(origin_port),那么可以将origin_ip和origin_port再加上payload的长度三个字段作为一层协议,插入到tcp和payload之间。这样用户层的应用只需要正常读数据,读取协议头,再读取payload即可。

以TCP为例,部分示例代码如下:

// tcp
static int handle_tcp(struct sk_buff *skb){
	struct iphdr *ip = ip_hdr(skb);
	struct tcphdr *tcp = tcp_hdr(skb);
	
	// conntrack
	enum ip_conntrack_info conntrack_info;	
	struct nf_conntrack_tuple *origin_tuple = NULL;
	struct nf_conn *ct = NULL;

	// inject buffer
	char insert_data[8];
	char *ptr = &insert_data[0];

	// origin payload
	char *origin_data = skb_network_header(skb) + ip->ihl * 4 + tcp->doff * 4;
	int origin_len = skb->tail - skb->network_header - ip->ihl * 4 - tcp->doff * 4;

	ct = get_conntrack(skb, &conntrack_info);
	if (ct == NULL) {
		return NF_ACCEPT;
	}

	origin_tuple = &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple;

	// force accepted pkt
	if (is_accepted_pkt(origin_tuple, ip->daddr, tcp->dest) != 0) {
		return NF_ACCEPT;
	}

	// empty payload
	if (origin_len == 0) {
		return NF_ACCEPT;
	}

	// modify tcp payload
	ptr = encode32u(ptr, origin_tuple->dst.u3.ip);
	ptr = encode16u(ptr, ntohs(origin_tuple->dst.u.all));
	ptr = encode16u(ptr, origin_len);


	// expand skb tailroom
	if (skb_tailroom(skb) < 8) {
		if (expand_skb(skb, skb_headroom(skb), skb_tailroom(skb) + 40) != 0) {
			return NF_DROP;	
		}
	}

	// copy conntrack data into skb payload 
	memmove(origin_data + 8, origin_data, origin_len);
	memcpy(origin_data, &insert_data[0], 8);

	nfct_seqadj_ext_add(ct);
	__nf_nat_mangle_tcp_packet(skb, ct, conntrack_info, 
			ip->ihl * 4, 0, origin_len, 
			origin_data, origin_len + 8, true);

	return NF_ACCEPT;
}

在用户态先读八个字节的协议,将origin_ip和origin_port以及payload长度length读取出来,然后再读取length字节的数据。不需要任何库,难点在于编写一个稳定的内核模块。

ICKelin avatar Feb 02 '18 14:02 ICKelin