iptables源代码分析之set集合模块

适合的读者
1.能够熟练使用iptables和ipset命令,增删改查等

2.心里好奇iptables的命令是如何生效的?

3.对研究netfilter源代码有浓厚兴趣的技术人员

4.用户态的iptables和内核态的netfilter是如何交互的

一、研究背景

使用iptables来针对某ip指定规则,从而达到抵御网络攻击的目的。
但有时可能对成千上万的ip进行封禁,如果添加成千上万条iptables规则,针对每个数据包都匹配一次iptables规则,那么势必会影响机器的性能,ipset就是为了解决这个问题而产生的。

使用方法如下

ipset create vader hash:ip

iptables -I INPUT -m set --match-set vader src -j DROP

-m:指定要加载的模块,上面加载的是set模块

/usr/lib/modules/3.10.0-327.el7.x86_64/kernel/net/netfilter/ipset目录下存着全部ipset相关的ko内核模块
在这里插入图片描述

二、提出疑问

1.iptables将set集合内容存在什么地方?

2.netfilter如何获取set集合中内容

3.netfilter是如何匹配数据包,并和set集合中的项进行对比的

三 set模块源代码

3.1 用户态iptables

iptables的set模块代码文件是libxt_set.c,位于iptables/extensions目录

其流程分为以下两步

第一步、声明xtables_match类型的结构体并给对应值赋值

第二步、注册填充好的match规则到内核中

xtables_match结构体的填充如下

static struct xtables_match set_mt_reg[] = {
{
.name = "set",
.revision = 0,
.version = XTABLES_VERSION,
.family = NFPROTO_IPV4,
.size = XT_ALIGN(sizeof(struct xt_set_info_match_v0)),
.userspacesize = XT_ALIGN(sizeof(struct xt_set_info_match_v0)),
.help = set_help_v0,
.parse = set_parse_v0,
.final_check = set_check_v0,
.print = set_print_v0,
.save = set_save_v0,
.extra_opts = set_opts_v0,
}

family:协议族
size:Size of match data 匹配数据的大小,即结构体的大小
userspacesize:用与用户层匹配的数据大小

其中程序员需要实现以下自定义函数(revision 分别为0,1,2)
set_help_v0
set_parse_v0(解析命令行选项,成功返回true)(重点)
set_check_v0 //在添加和删除项时进行校验
set_print_v0、set_save_v0

其中extra_opts是额外选项,此时被初始化为set_opts_v0

static const struct option set_opts_v0[] = {
{.name = "match-set", .has_arg = true, .val = '1'},
{.name = "set", .has_arg = true, .val = '2'},
XT_GETOPT_TABLEEND,
};

name:可选项的名称
has_arg:true表示有参数,false表示没参数
val:元素的默认值,set模块中为1和2,1和2的作用会在文章后续解释

重点看下set_parse_v0函数

static int set_parse_v0(int c, char **argv, int invert, unsigned int *flags,const void *entry, struct xt_entry_match **match)
{
struct xt_set_info_match_v0 *myinfo = 
    (struct xt_set_info_match_v0 *) (*match)->data;
struct xt_set_info_v0 *info = &myinfo->match_set;

switch (c) {
case '2':
    //--set 选项已经废弃
    fprintf(stderr,
        "--set option deprecated, please use --match-set\n");
case '1':        /* --match-set  [, */
    if (info->u.flags[0])
        xtables_error(PARAMETER_PROBLEM,
                  "--match-set can be specified only once");
    if (invert)
        info->u.flags[0] |= IPSET_MATCH_INV;

    if (!argv[optind]
        || argv[optind][0] == '-'
        || argv[optind][0] == '!')
        xtables_error(PARAMETER_PROBLEM,
                  "--match-set requires two args.");

    if (strlen(optarg) > IPSET_MAXNAMELEN - 1)
        xtables_error(PARAMETER_PROBLEM,
                  "setname `%s' too long, max %d characters.",
                  optarg, IPSET_MAXNAMELEN - 1);

    //通过名字获取set集合
    get_set_byname(optarg, (struct xt_set_info *)info);

    //匹配dst,src关键词,如没有则给出提示
    parse_dirs_v0(argv[optind], info);
    
    DEBUGP("parse: set index %u\n", info->index);
    optind++;

    *flags = 1;
    break;
}

return 1;
}

从上面代码可看出,用户态iptables和内核态netfilter之间通信的结构体是struct xt_set_info_match_v0类型,如下:

struct xt_set_info_v0 {
	ip_set_id_t index;
	union {
		__u32 flags[IPSET_DIM_MAX + 1];
		struct {
			__u32 __flags[IPSET_DIM_MAX];
			__u8 dim;
			__u8 flags;
		} compat;
	} u;
};

/* match and target infos */
struct xt_set_info_match_v0 {
	struct xt_set_info_v0 match_set;
};

其中index为标识ip_set集合的索引

get_set_byname函数通过set名称获取set集合

static void
get_set_byname(const char *setname, struct xt_set_info *info)
{
	struct ip_set_req_get_set_family req;
	socklen_t size = sizeof(struct ip_set_req_get_set_family);
	int res, sockfd, version;

	sockfd = get_version(&req.version);
	version = req.version;
	req.op = IP_SET_OP_GET_FNAME;
	strncpy(req.set.name, setname, IPSET_MAXNAMELEN);
	req.set.name[IPSET_MAXNAMELEN - 1] = '\0';
	res = getsockopt(sockfd, SOL_IP, SO_IP_SET, &req, &size);

	if (res != 0 && errno == EBADMSG)
		/* Backward compatibility */
		return get_set_byname_only(setname, info, sockfd, version);

	close(sockfd);    
	info->index = req.set.index;
}

使用getsockopt函数,传递SOL_IP, SO_IP_SET参数给函数,获取到set集合的索引,也是巧妙哈。

get_set_byname_only是为了向后兼容性。

parse_dirs_v0函数匹配参数中是否存在‘src’或‘dst’

3.2 内核态代码

下面我们一起看下内核代码的实现(linux内核代码版本3.10)
在netfilter内核层有代码,定位到xt_set.c

3.2.1 内核态match编写流程
1)填充xt_match结构体各个成员变量
2)注册xt_match结构体到内核netfilter框架中

static struct xt_match set_matches[] __read_mostly = {
	{
		.name		= "set",
		.family		= NFPROTO_IPV4,
		.revision	= 0,
		.match		= set_match_v0,
		.matchsize	= sizeof(struct xt_set_info_match_v0),
		.checkentry	= set_match_v0_checkentry,
		.destroy	= set_match_v0_destroy,
		.me		= THIS_MODULE
	}

name:match的名称
family:协议族
math:匹配函数
matchsize:match规则结构体的大小
me:一般填写THIS_MODULE

重点看set_match_v0匹配函数

static bool
set_match_v0(const struct sk_buff *skb, struct xt_action_param *par)
{
const struct xt_set_info_match_v0 *info = par->matchinfo;
ADT_OPT(opt, par->family, info->match_set.u.compat.dim,
info->match_set.u.compat.flags, 0, UINT_MAX);

return match_set(info->match_set.index, skb, par, &opt,
         info->match_set.u.compat.flags & IPSET_INV_MATCH);
}

之前在分析用户态模块时,对于set模块来说,用户态iptables传递给内核态netfilter的规则是struct xt_set_info_match_v0类型,所以这里将par->matchinfo转换为struct xt_set_info_match_v0类型的指针

match_set内部调用ip_set_test函数

static inline int
match_set(ip_set_id_t index, const struct sk_buff *skb,
const struct xt_action_param *par,
struct ip_set_adt_opt *opt, int inv)
{
if (ip_set_test(index, skb, par, opt))
inv = !inv;
return inv;
}

ip_set_test先调用ip_set_rcu_get(index),根据index索引值,获取ip_set_list链表中的ip_set元素

然后加锁调用set->variant->kadt(set, skb, par, IPSET_TEST, opt)函数

read_lock_bh(&set->lock);
ret = set->variant->kadt(set, skb, par, IPSET_TEST, opt);
read_unlock_bh(&set->lock);

想了解kadt的回调函数的原理,请参考连接
https://blog.csdn.net/haolipengzhanshen/article/details/85109324

对应hash:ip类型set来说,kadt为hash_ip4_kadt函数

hash_ip4_kadt(struct ip_set *set, const struct sk_buff *skb,
	      const struct xt_action_param *par,
	      enum ipset_adt adt, struct ip_set_adt_opt *opt)
{
	const struct hash_ip *h = set->data;
	ipset_adtfn adtfn = set->variant->adt[adt];
	struct hash_ip4_elem e = {};
	struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, h);
	__be32 ip;

	ip4addrptr(skb, opt->flags & IPSET_DIM_ONE_SRC, &ip);
	ip &= ip_set_netmask(h->netmask);
	if (ip == 0)
		return -EINVAL;

	e.ip = ip;
	return adtfn(set, &e, &ext, &opt->ext, opt->cmdflags);
}

流程为:
第一步 获取ipset对应的adt函数
ad函数的注册如下
.adt = {
[IPSET_ADD] = mtype_add,
[IPSET_DEL] = mtype_del,
[IPSET_TEST] = mtype_test,
},

adt参数的解释如下:底层的add/del/test函数
/* Low level add/del/test functions */
ipset_adtfn adt[IPSET_ADT_MAX];

2.ip4addrptr函数获取skb结构体中的ip值

3.调用adtfn函数

现在我们回到kadt回调函数调用的位置
ret = set->variant->kadt(set, skb, par, IPSET_TEST, opt);
传递的是IPSET_TEST,所以对应的adt[IPSET_TEST]的回调函数是mtype_test

mtype_test函数在ip_set_hash_gen.h文件中

mtype_test(struct ip_set *set, void *value, const struct ip_set_ext *ext,
	   struct ip_set_ext *mext, u32 flags)
{
	struct htype *h = set->data;
	struct htable *t;
	struct mtype_elem *d = value;
	struct hbucket *n;
	struct mtype_elem *data;
	int i, ret = 0;
	u32 key, multi = 0;

	t = rcu_dereference_bh(h->table);
#ifdef IP_SET_HASH_WITH_NETS
	/* If we test an IP address and not a network address,
	 * try all possible network sizes
	 */
	for (i = 0; i < IPSET_NET_COUNT; i++)
		if (DCIDR_GET(d->cidr, i) != HOST_MASK)
			break;
	if (i == IPSET_NET_COUNT) {
		ret = mtype_test_cidrs(set, d, ext, mext, flags);
		goto out;
	}
#endif

	key = HKEY(d, h->initval, t->htable_bits);
	n = rcu_dereference_bh(hbucket(t, key));
	if (!n) {
		ret = 0;
		goto out;
	}
	for (i = 0; i < n->pos; i++) {
		if (!test_bit(i, n->used))
			continue;
		data = ahash_data(n, i, set->dsize);
		if (!mtype_data_equal(data, d, &multi))
			continue;
		ret = mtype_data_match(data, ext, mext, set, flags);
		if (ret != 0)
			goto out;
	}
out:
	return ret;
}

HKEY宏获取hash key值
ahash_data获取数据
mtype_data_equal数据是否相等
mtype_data_match数据是否匹配match规则

mtype_data_equal是每种类型自己实现的

static inline int
mtype_data_match(struct mtype_elem *data, const struct ip_set_ext *ext,
struct ip_set_ext *mext, struct ip_set set, u32 flags)
{
if (!ip_set_match_extensions(set, ext, mext, flags, data))
return 0;
/
nomatch entries return -ENOTEMPTY */
return mtype_do_data_match(data);
}
则继续调用mtype_do_data_match,mtype_do_data_match为每种类型自己实现的
分析完毕。

在代码中不仅有MTYPE的宏定义,还有HTYPE宏定义,那么HTYPE的主要作用是什么呢?

static int IPSET_TOKEN(HTYPE, _create)(struct net *net, struct ip_set *set, struct nlattr *tb[], u32 flags)
大家看仔细哦,此函数的函数名称是使用宏构造出来的,IPSET_TOKEN(HTYPE, _create)宏根据HTYPE的值生成一个函数名称,如下所示
如hash:mac类型的集合定义HTYPE如下
#define HTYPE hash_mac
则拼装的函数名和曾为hash_mac_create
iptables源代码分析之set集合模块_第1张图片
在内核模块ip_set_hash_mac.c中注册给create函数

你可能感兴趣的:(iptables源代码分析之set集合模块)