article
article copied to clipboard
连接跟踪的一种非常暴力的获取方式
在项目当中有时需要连接跟踪相关的信息,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字节的数据。不需要任何库,难点在于编写一个稳定的内核模块。