适合的读者
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集合中的项进行对比的
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’
下面我们一起看下内核代码的实现(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
在内核模块ip_set_hash_mac.c中注册给create函数