连接跟踪子系统之helper---ftp

这篇笔记记录了FTP协议是如何利用helper实现连接跟踪的。核心代码文件如下:

代码路径说明
net/netfilter/nf_conntrack_ftp.c实现文件
include/net/netfilter/nf_conntrack_ftp.h头文件

FTP协议的连接跟踪由开关CONFIG_NF_CONNTRACK_FTP控制。

1. 基本原理

向连接跟踪子系统注册一个helper来跟踪FTP控制连接上传输的报文,通过搜索这些报文中的PORT(主动模式)和PASV(被动模式)命令,进而获取到即将要建立的期望连接信息(端口和IP地址)。

2. 初始化

可以想的见,初始化时最关键的操作应该就是向连接跟踪子系统注册helper。初始化函数如下:

//该buffer用来拷贝skb中FTP控制连接报文内容,搜索关键字之前会先把报文拷贝到该
//buffer中,然后对buffer进行搜索,这样虽然会影响效率,但是实现简单
static char *ftp_buffer;
static DEFINE_SPINLOCK(nf_ftp_lock);

//虽然标准协议规定FTP的控制端口就是21,但是为了可扩展,内核在实现时,将可
//监听的控制端口做成了参数可配的,并且可以同时最多跟踪8个控制端口,下面分析
//代码时,为了理解方便,可以都视作只跟踪21号端口
#define MAX_PORTS 8
//要跟踪的控制端口号
static u_int16_t ports[MAX_PORTS];
//ports数组中当前指定了几个控制端口
static unsigned int ports_c;
module_param_array(ports, ushort, &ports_c, 0400);
//AF_INET和AF_INET6协议族各注册一个helper
static struct nf_conntrack_helper ftp[MAX_PORTS][2] __read_mostly;
//每个helper都有一个名字,这里保存名字,命名方法见下方init()
static char ftp_names[MAX_PORTS][2][sizeof("ftp-65535")] __read_mostly;

static int __init nf_conntrack_ftp_init(void)
{
	int i, j = -1, ret = 0;
	char *tmpname;
	//ftp_buffer用于保存FTP控制连接的应用层数据,最长为65535,其具体用法见下方help()
	ftp_buffer = kmalloc(65536, GFP_KERNEL);
	if (!ftp_buffer)
		return -ENOMEM;
	//如果模块加载时没有指定控制端口信息,那么默认只监听21号端口
	if (ports_c == 0)
		ports[ports_c++] = FTP_PORT;
	//初始化helper,然后将其向系统注册
	for (i = 0; i < ports_c; i++) {
		//初始化tuple(源和目的)
		ftp[i][0].tuple.src.l3num = PF_INET;
		ftp[i][1].tuple.src.l3num = PF_INET6;
		for (j = 0; j < 2; j++) {
			ftp[i][j].tuple.src.u.tcp.port = htons(ports[i]);
			ftp[i][j].tuple.dst.protonum = IPPROTO_TCP;
			//允许同时只存在1个未确认的期望连接
			ftp[i][j].max_expected = 1;
			//未确认的期望连接的超时时间为5分钟
			ftp[i][j].timeout = 5 * 60;	/* 5 Minutes */
			ftp[i][j].me = THIS_MODULE;
			//help()回调,非常重要,见下方
			ftp[i][j].help = help;
			//21号控制端口的helper名字为"ftp",其它端口的helper名字为"ftp-端口号"
			tmpname = &ftp_names[i][j][0];
			if (ports[i] == FTP_PORT)
				sprintf(tmpname, "ftp");
			else
				sprintf(tmpname, "ftp-%d", ports[i]);
			ftp[i][j].name = tmpname;
			//向连接跟踪子系统注册helper
			ret = nf_conntrack_helper_register(&ftp[i][j]);
			if (ret) {
				nf_conntrack_ftp_fini();
				return ret;
			}
		}
	}
	return 0;
}

下面在看help()回调的实现之前,先来看看两个help()实现关键点:

  1. 搜索关键字定义;
  2. 连接序号缓存;

3. 搜索关键字

通过struct ftp_search定义了要搜索的一组关键字,以及一些搜索方法。

static struct ftp_search {
	const char *pattern;	//关键字
	size_t plen;			//关键字长度
	char skip;				//搜索过程中跳过什么字符
	char term;				//搜索遇到什么字符就停止搜索
	enum nf_ct_ftp_type ftptype;//搜索的关键字类型
	//搜索到关键字后,如何从关键字信息中提取IP地址和端口号
	int (*getnum)(const char *, size_t, struct nf_conntrack_man *, char);
} search[IP_CT_DIR_MAX][2] = {
	[IP_CT_DIR_ORIGINAL] = {
		{
			.pattern	= "PORT",
			.plen		= sizeof("PORT") - 1,
			.skip		= ' ',
			.term		= '\r',
			.ftptype	= NF_CT_FTP_PORT,
			.getnum		= try_rfc959,
		},
		{
			.pattern	= "EPRT",
			.plen		= sizeof("EPRT") - 1,
			.skip		= ' ',
			.term		= '\r',
			.ftptype	= NF_CT_FTP_EPRT,
			.getnum		= try_eprt,
		},
	},
	[IP_CT_DIR_REPLY] = {
		{
			.pattern	= "227 ",
			.plen		= sizeof("227 ") - 1,
			.skip		= '(',
			.term		= ')',
			.ftptype	= NF_CT_FTP_PASV,
			.getnum		= try_rfc959,
		},
		{
			.pattern	= "229 ",
			.plen		= sizeof("229 ") - 1,
			.skip		= '(',
			.term		= ')',
			.ftptype	= NF_CT_FTP_EPSV,
			.getnum		= try_epsv_response,
		},
	},
};

这里需要多思考一下匹配方向,为什么PORT就一定是ORGINAL呢?这是因为PORT和控制连接的方向是一致的,如果还是不理解,列举下两种可能性就都清楚了。PASV在REPLY方向是一个道理。

help()中,检查到一个命令行后,会调用find_pattern()进行关键字匹配,传入该函数的参数就是上面的struct ftp_search中的字段。

@data:要搜索的报文内容
@dlen:data的长度
@numoff:输出参数,如果搜索命中,记录地址信息在data中的偏移
@numlen: 输出参数,从numoff开始的numlen个字节为地址信息
@cmd:搜索命中后,调用getnum()填充该参数(将其作为第二个参数传入getnum())
@ret:搜索命中返回1,不命中返回0,如果只命中了一部分,那么返回-1表示失败
static int find_pattern(const char *data, size_t dlen,
	const char *pattern, size_t plen, char skip, char term, unsigned int *numoff,
	unsigned int *numlen, struct nf_conntrack_man *cmd,
	int (*getnum)(const char *, size_t, struct nf_conntrack_man *, char))
{
	size_t i;

	if (dlen == 0)
		return 0;
	if (dlen <= plen) {
		//报文长度比关键字还短,但是可以匹配一部分关键字,返回-1
		if (strnicmp(data, pattern, dlen) == 0)
			return -1;
		else return 0;
	}
	//不匹配返回0
	if (strnicmp(data, pattern, plen) != 0) 
		return 0;
	//命令已经匹配,下面尝试从命令中解析IP地址和端口号

	//先跳过skip字符
	for (i = plen; data[i] != skip; i++)
		if (i == dlen - 1) return -1;
	i++;
	//记录地址信息偏移,调用getnum()解析地址信息并记录numlen
	*numoff = i;
	*numlen = getnum(data + i, dlen - i, cmd, term);
	//命令匹配,但是没解析到任何地址参数,解析失败,返回-1
	if (!*numlen)
		return -1;
	//一切正常并且搜索命中,返回1
	return 1;
}

搜索过程非常直接,就是直接匹配FTP控制连接中的命令,不熟悉的可以查下FTP中的PORT和PASV命令格式。

4. 连接序号缓存

FTP控制连接上的命令都是以行为单位进行收发的,即它们是有边界的,但是TCP的传输是字节流,所有在匹配时,就非常有必要先识别报文中数据边界,然后才能对其进行搜索等后续处理。

在实现时,定义了一个结构专门缓存当前收到的一些行的tcp序号,通过这些序号进行定界。

#define NUM_SEQ_TO_REMEMBER 2
struct nf_ct_ftp_master {
	//如果一个报文以新行结尾,则其末尾序号+1就是一个新行的起始序号,
	//该序号就会被缓存到seq_aft_nl中(数组名是seq after newline的缩写)
	u_int32_t seq_aft_nl[IP_CT_DIR_MAX][NUM_SEQ_TO_REMEMBER];
	//记录了seq_aft_nl[]中当前缓存了几个序号
	int seq_aft_nl_num[IP_CT_DIR_MAX];
};

helper在注册时,会将该结构作为扩展信息注册到FTP控制连接的连接跟踪信息块中。help()函数中,在收到报文时,如果该报文以\n结尾则尝试更新缓存信息。

4.1 序列号缓存项更新

static void update_nl_seq(u32 nl_seq, struct nf_ct_ftp_master *info, int dir,
			  struct sk_buff *skb)
{
	unsigned int i, oldest = NUM_SEQ_TO_REMEMBER;
	//从当前已缓存项中找一个最老的,即序号最小的
	for (i = 0; i < info->seq_aft_nl_num[dir]; i++) {
		//如果要缓存的seq已经在缓存数组中了,当然无需更新了
		if (info->seq_aft_nl[dir][i] == nl_seq)
			return;
		//这个oldest的比较是个bug吧
		if (oldest == info->seq_aft_nl_num[dir] ||
		    before(info->seq_aft_nl[dir][i], info->seq_aft_nl[dir][oldest]))
			oldest = i;
	}
	if (info->seq_aft_nl_num[dir] < NUM_SEQ_TO_REMEMBER) {
		//数组还没有满,放入下一个空闲位置即可,并且累加有效缓存项个数
		info->seq_aft_nl[dir][info->seq_aft_nl_num[dir]++] = nl_seq;
		nf_conntrack_event_cache(IPCT_HELPINFO_VOLATILE, skb);
	} else if (oldest != NUM_SEQ_TO_REMEMBER &&
		   after(nl_seq, info->seq_aft_nl[dir][oldest])) {
		//数组已满,并且要缓存的序号确实大于该最老的缓存项时进行更新
		info->seq_aft_nl[dir][oldest] = nl_seq;
		nf_conntrack_event_cache(IPCT_HELPINFO_VOLATILE, skb);
	}
}

上面的更新函数实现这么怪,是因为seq_aft_nl[]数组并没有按照有序数组来维护。但是它的更新原则还是非常简单的:就是数组没满时,直接往后累加,如果满了,那么将tcp序列号最小的一个替换。

4.2 序列号缓存项查找

调用find_nl_seq()对缓存的序号进行查找。

static int find_nl_seq(u32 seq, const struct nf_ct_ftp_master *info, int dir)
{
	unsigned int i;

	for (i = 0; i < info->seq_aft_nl_num[dir]; i++)
		if (info->seq_aft_nl[dir][i] == seq)
			return 1;
	return 0;
}

从下面的help()实现中可以看到,更新缓存项时传入的seq时下一个命令的起始序号,查找缓存项时传入的seq是当前报文的起始序号。仔细想,这是合理的,因为这样可以保证如果查找时序号如果命中,就可以说明当前报文就是一个命令行的开始,这样就可以进行前面的关键字搜索等后续处理了。

5. help()回调

实现FTP协议连接跟踪功能的核心是help(),从连接跟踪子系统之helper
中有看到,当非期望连接的skb在建立新的连接时,会根据Reply方向的tuple查找系统中已注册的helper,如果找到就会将该helper信息记录到连接跟踪信息块中,然后就会在help钩子处,调用helper中的help()回调。

static int help(struct sk_buff *skb, unsigned int protoff, struct nf_conn *ct,
		enum ip_conntrack_info ctinfo)
{
	unsigned int dataoff, datalen;
	struct tcphdr _tcph, *th;
	char *fb_ptr;
	int ret;
	u32 seq;
	int dir = CTINFO2DIR(ctinfo);
	unsigned int matchlen, matchoff;
	//上面介绍的序号缓存信息
	struct nf_ct_ftp_master *ct_ftp_info = &nfct_help(ct)->help.ct_ftp_info;
	struct nf_conntrack_expect *exp;
	union nf_inet_addr *daddr;
	struct nf_conntrack_man cmd = {};
	unsigned int i;
	int found = 0, ends_in_nl;
	typeof(nf_nat_ftp_hook) nf_nat_ftp;

	//我们关注的控制命令只可能出现在ESTABLISHTED连接上
	if (ctinfo != IP_CT_ESTABLISHED && ctinfo != IP_CT_ESTABLISHED+IP_CT_IS_REPLY) 
		return NF_ACCEPT;
	//protoff是tcp协议距离skb->data的偏移,将TCP首部拷贝到_tcph中,
	//并且th指向_tcph,既数据包中tcp的首部
	th = skb_header_pointer(skb, protoff, sizeof(_tcph), &_tcph);
	if (th == NULL)
		return NF_ACCEPT;
	//dataoff为应用层数据距离skb->data的偏移量
	dataoff = protoff + th->doff * 4;
	if (dataoff >= skb->len)
		return NF_ACCEPT;
	//datalen就是skb中剥除所有协议首部后剩余应用层数据的长度
	datalen = skb->len - dataoff;

	spin_lock_bh(&nf_ftp_lock);
	//将FTP应用层数据拷贝到ftp_buffer中,ftp_ptr指向ftp_buffer
	fb_ptr = skb_header_pointer(skb, dataoff, datalen, ftp_buffer);
	BUG_ON(fb_ptr == NULL);
	//ends_in_nl标志当前报文的最后一个字节是否以'\n'结尾,一般情况下都应如此
	ends_in_nl = (fb_ptr[datalen - 1] == '\n');
	//seq是报文的最后一个字节的序号+1
	seq = ntohl(th->seq) + datalen;
	//查找序号缓存项,如果没有找到,则尝试更新缓存信息后结束,
	//因为这个数据包的第一个字节不是一个命令行的开头
	if (!find_nl_seq(ntohl(th->seq), ct_ftp_info, dir)) {
		ret = NF_ACCEPT;
		goto out_update_nl;
	}
	//从连接跟踪信息块中找到skb传输方向上的地址信息
	cmd.l3num = ct->tuplehash[dir].tuple.src.l3num;
	memcpy(cmd.u3.all, &ct->tuplehash[dir].tuple.src.u3.all, sizeof(cmd.u3.all));
	//搜索数据包的内容,从中寻找是否有特定的关键字(PORT和PASV信息)
	for (i = 0; i < ARRAY_SIZE(search[dir]); i++) {
		found = find_pattern(fb_ptr, datalen, search[dir][i].pattern,
					search[dir][i].plen, search[dir][i].skip, search[dir][i].term,
				    &matchoff, &matchlen, &cmd, search[dir][i].getnum);
		if (found)
			break;
	}
	//虽然连接跟踪本意上不应该丢包,但是遇到这种部分匹配的异常情况,
	//说明传输确实是有问题的,直接丢弃也未尝不可
	if (found == -1) {
		ret = NF_DROP;
		goto out;
	} else if (found == 0) {
		//没有包含关注的关键字,尝试更新序列号缓存信息后结束		
		ret = NF_ACCEPT;
		goto out_update_nl;
	}
	//找到了我们关心的信息,说明即将要建立一个数据连接(即期望连接),
	//所以分配一个期望并对其进行初始化,然后将其加入期望连接表中,这样
	//等期望连接的数据包到达时就可以匹配到了
	exp = nf_ct_expect_alloc(ct);
	if (exp == NULL) {
		ret = NF_DROP;
		goto out;
	}
	//计算期望连接的tuple信息
	daddr = &ct->tuplehash[!dir].tuple.dst.u3;
	/* Update the ftp info */
	if ((cmd.l3num == ct->tuplehash[dir].tuple.src.l3num) &&
	    memcmp(&cmd.u3.all, &ct->tuplehash[dir].tuple.src.u3.all, sizeof(cmd.u3.all))) {
		/* Thanks to Cristiano Lincoln Mattos
		   <lincoln@cesar.org.br> for reporting this potential
		   problem (DMZ machines opening holes to internal
		   networks, or the packet filter itself). */
		if (!loose) {
			ret = NF_ACCEPT;
			goto out_put_expect;
		}
		daddr = &cmd.u3;
	}
	//初始化期望连接结构
	nf_ct_expect_init(exp, cmd.l3num, &ct->tuplehash[!dir].tuple.src.u3, daddr,
			  IPPROTO_TCP, NULL, &cmd.u.tcp.port);

	/* Now, NAT might want to mangle the packet, and register the
	 * (possibly changed) expectation itself. */
	//如果NAT会处理该数据包,则交给NAT处理,否则将其加入到期望连接跟踪表中
	nf_nat_ftp = rcu_dereference(nf_nat_ftp_hook);
	if (nf_nat_ftp && ct->status & IPS_NAT_MASK)
		ret = nf_nat_ftp(skb, ctinfo, search[dir][i].ftptype, matchoff, matchlen, exp);
	else {
		/* Can't expect this?  Best to drop packet now. */
		if (nf_ct_expect_related(exp) != 0)
			ret = NF_DROP;
		else
			ret = NF_ACCEPT;
	}
out_put_expect:
	nf_ct_expect_put(exp);
out_update_nl:
	//只有当前数据包是以'\n'结尾时才更新序号缓存数组
	if (ends_in_nl)
		update_nl_seq(seq, ct_ftp_info, dir, skb);
 out:
	spin_unlock_bh(&nf_ftp_lock);
	return ret;
}
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值