这篇笔记记录了AF_INET协议族的filter表的初始化,通过filter表,可以看出协议族是如何与Netfilter框架配合,完成防火墙规则的过滤和管理的。filter表涉及内容较多,分两篇笔记完成,涉及的核心代码文件有:
代码路径 | 说明 |
---|---|
net/ipv4/netfilter/iptable_filter.c | IPv4 filter表实现 |
filter表以模块的形式存在,其控制开关为:CONFIG_IP_NF_FILTER。
初始化函数即模块初始化函数,如下:
static int __net_init iptable_filter_net_init(struct net *net)
{
//注册filter表
net->ipv4.iptable_filter =
ipt_register_table(net, &packet_filter, &initial_table.repl);
if (IS_ERR(net->ipv4.iptable_filter))
return PTR_ERR(net->ipv4.iptable_filter);
return 0;
}
static struct pernet_operations iptable_filter_net_ops = {
.init = iptable_filter_net_init,
.exit = iptable_filter_net_exit,
};
static int __init iptable_filter_init(void)
{
int ret;
if (forward < 0 || forward > NF_MAX_VERDICT) {
printk("iptables forward must be 0 or 1\n");
return -EINVAL;
}
/* Entry 1 is the FORWARD hook */
initial_table.entries[1].target.verdict = -forward - 1;
ret = register_pernet_subsys(&iptable_filter_net_ops);
if (ret < 0)
return ret;
//向Netfilter框架注册三个钩子:INPUT、OUTPUT、FORWARD
ret = nf_register_hooks(ipt_ops, ARRAY_SIZE(ipt_ops));
if (ret < 0)
goto cleanup_table;
return ret;
...
}
初始化干了两件重要的事情:
内容较多,这篇笔记只看filter表的注册过程,钩子的注册过程见下一篇笔记。
在看具体的实现之前,需要先来看看注册表时传入的两个参数:packet_filter和initial_table.repl。
//filter表中的规则只能在LOCAL_IN、FORWARD、LOCAL_OUT三个点工作
#define FILTER_VALID_HOOKS ((1 << NF_INET_LOCAL_IN) | \
(1 << NF_INET_FORWARD) | \
(1 << NF_INET_LOCAL_OUT))
//packet_filter就是Netfilter框架定义的表结构struct xt_table对象,这里表名字为filter
static struct xt_table packet_filter = {
.name = "filter",
.valid_hooks = FILTER_VALID_HOOKS,
.lock = RW_LOCK_UNLOCKED,
.me = THIS_MODULE,
.af = AF_INET,
};
该结构正是Netfilter对table的定义,那么额外的initial_table.repl又是干什么的呢?
static struct
{
struct ipt_replace repl;
//这个组织是很有讲究的,紧挨着在repl的后面放了4条规则,因为repl的最后一个成员
//entries是零长度数组,所以其直接指向了这里的规则。filter表注册时内置了4调规则
struct ipt_standard entries[3];
struct ipt_error term;
} initial_table;
和注册相关的核心数据结构是struct ipt_replace。iptables工具在更新规则时实际上是整体替换的,即先从内核dump所有规则,然后修改,最后整体替换,并非单独修改某一条规则。注册也可以理解为是一种替换,只是要替换表原本不存在而已,所以只要设计上面在某些地方做些许兼容,注册和替换的代码逻辑是可以复用的。而struct ipt_replace结构就是AF_INET协议族专门为了实现table替换而定义的辅助结构。
/* The argument to IPT_SO_SET_REPLACE. */
struct ipt_replace
{
char name[IPT_TABLE_MAXNAMELEN];
/* Which hook entry points are valid: bitmask. You can't change this. */
unsigned int valid_hooks;
/* Number of entries */
unsigned int num_entries;
/* Total size of new entries */
unsigned int size;
/* Hook entry points. */
unsigned int hook_entry[NF_INET_NUMHOOKS];
/* Underflow points. */
unsigned int underflow[NF_INET_NUMHOOKS];
/* Information about old entries: */
/* Number of counters (must be equal to current number of entries). */
unsigned int num_counters;
/* The old entries' counters. */
struct xt_counters __user *counters;
//末尾指针很重要,指向规则,对于initial_table来说就是repl后面的4条规则
struct ipt_entry entries[0];
};
如Netfilter之table、rule、match、target数据结构中关于struct xt_table和struct xt_table_info的介绍,可以发现struct ipt_replace差不多是这两个结构的综合,而且它们共同的字段的含义确实是一致的。
回头再来看filter表对struct ipt_replace的初始化:
.repl = {
.name = "filter",
.valid_hooks = FILTER_VALID_HOOKS,
//初始化时有4条规则,如上,initial_table中在repl后面定义了4条规则
.num_entries = 4,
//size大小就是所有规则大小的累加
.size = sizeof(struct ipt_standard) * 3 + sizeof(struct ipt_error),
//因为hook_entry[]和underflow[]分别记录的是该表在每个hook点上的第一条和
//最后一条规则距离表起点的偏移量,并且当前表中每个hook点只有一条规则,所以
//它们的初始化值都是相同的
.hook_entry = {
[NF_INET_LOCAL_IN] = 0,
[NF_INET_FORWARD] = sizeof(struct ipt_standard),
[NF_INET_LOCAL_OUT] = sizeof(struct ipt_standard) * 2,
},
.underflow = {
[NF_INET_LOCAL_IN] = 0,
[NF_INET_FORWARD] = sizeof(struct ipt_standard),
[NF_INET_LOCAL_OUT] = sizeof(struct ipt_standard) * 2,
},
//下面是对默认的4条规则的初始化。虽然entries和term并不属于repl,但是因为它们
//在内存上面是连续的,所以可以使用这种方式进行初始化,内核中有很多这样的小技巧
//三个hook点各放一条无条件接受的规则。这三条规则实际上就是hook点的策略规则,
//它们会永远位于给hook点上所有规则的末尾,当其它规则都不匹配时,交由该规则
//觉得数据包最后的命运
.entries = {
IPT_STANDARD_INIT(NF_ACCEPT), /* LOCAL_IN */
IPT_STANDARD_INIT(NF_ACCEPT), /* FORWARD */
IPT_STANDARD_INIT(NF_ACCEPT), /* LOCAL_OUT */
},
//这里放置了一条错误规则。个人理解:这条规则应该永远都不会被遍历到,因为在遍历
//规则时是根据hook点遍历的,而hook点规则由hook_entry[]和underflow[]定界,永远
//都不应该走到这里,放着这么一条应该是为了对代码异常场景提供一种保护
.term = IPT_ERROR_INIT, /* ERROR */
},
表的注册并不是直接调用Netfilter框架提供的xt_register_table(),AF_INET协议族对注册过程有特殊处理,所以对注册过程用ipt_register_table()进行了封装。
struct xt_table *ipt_register_table(struct net *net, struct xt_table *table,
const struct ipt_replace *repl)
{
int ret;
struct xt_table_info *newinfo;
//该结构就是为了复用表替换代码而虚构出来的表
struct xt_table_info bootstrap
= { 0, 0, 0, { 0 }, { 0 }, { } };
void *loc_cpu_entry;
struct xt_table *new_table;
//按照规则大小分配struct xt_table_info结构
newinfo = xt_alloc_table_info(repl->size);
if (!newinfo) {
ret = -ENOMEM;
goto out;
}
//将repl末尾保存的规则拷贝到struct xt_table_info中本地CPU的指针位置
loc_cpu_entry = newinfo->entries[raw_smp_processor_id()];
memcpy(loc_cpu_entry, repl->entries, repl->size);
//用指定的信息检查新table中的规则信息(newinfo指向)
ret = translate_table(table->name, table->valid_hooks,
newinfo, loc_cpu_entry, repl->size,
repl->num_entries, repl->hook_entry, repl->underflow);
if (ret != 0)
goto out_free;
//调用Netfilter框架的tablle注册接口将filter表注册到系统中
new_table = xt_register_table(net, table, &bootstrap, newinfo);
if (IS_ERR(new_table)) {
ret = PTR_ERR(new_table);
goto out_free;
}
return new_table;
...
}
如注释所述,该函数的作用就是用调用者指定的信息校验并初始化newinfo,其实就是对要注册的表内容进行合法性检查。
/* Checks and translates the user-supplied table segment (held in newinfo) */
static int translate_table(const char *name, unsigned int valid_hooks,
struct xt_table_info *newinfo, void *entry0, unsigned int size,
unsigned int number, const unsigned int *hook_entries,
const unsigned int *underflows)
{
unsigned int i;
int ret;
//规则总的占用字节数和规则数目
newinfo->size = size;
newinfo->number = number;
/* Init all hooks to impossible value. */
for (i = 0; i < NF_INET_NUMHOOKS; i++) {
newinfo->hook_entry[i] = 0xFFFFFFFF;
newinfo->underflow[i] = 0xFFFFFFFF;
}
duprintf("translate_table: size %u\n", newinfo->size);
i = 0;
//检查规则内的偏移量成员指定的是否准确
ret = IPT_ENTRY_ITERATE(entry0, newinfo->size, check_entry_size_and_hooks,
newinfo, entry0, entry0 + size, hook_entries, underflows, &i);
if (ret != 0)
return ret;
//i记录的是检测通过的规则数目,如果和指定的数字不一致,说明有规则没有通过检查
if (i != number) {
duprintf("translate_table: %u not %u entries\n", i, number);
return -EINVAL;
}
//在上一步的检测过程中如果规则检测pass,会对hook_entry和underflow赋值,
//这里检查是否所有有效的HOOK点都已经指定了合理的值
for (i = 0; i < NF_INET_NUMHOOKS; i++) {
/* Only hooks which are valid */
if (!(valid_hooks & (1 << i)))
continue;
if (newinfo->hook_entry[i] == 0xFFFFFFFF) {
duprintf("Invalid hook entry %u %u\n", i, hook_entries[i]);
return -EINVAL;
}
if (newinfo->underflow[i] == 0xFFFFFFFF) {
duprintf("Invalid underflow %u %u\n", i, underflows[i]);
return -EINVAL;
}
}
//检查表中的规则是否构成环路
if (!mark_source_chains(newinfo, valid_hooks, entry0))
return -ELOOP;
//无论是match还是target,都有对应的check回调,这里检查每条规则的match和target
i = 0;
ret = IPT_ENTRY_ITERATE(entry0, newinfo->size, find_check_entry, name, size, &i);
if (ret != 0) {
//没有通过检查,清除所有的规则
IPT_ENTRY_ITERATE(entry0, newinfo->size, cleanup_entry, &i);
return ret;
}
//通过了所有的检查,将规则信息拷贝到其它CPU的内存中
for_each_possible_cpu(i) {
if (newinfo->entries[i] && newinfo->entries[i] != entry0)
memcpy(newinfo->entries[i], entry0, newinfo->size);
}
return ret;
}
如上,注册过程中,会逐个检查规则信息,而遍历过程都是用宏IPT_ENTRY_ITERATE实现的,该宏的定义比较复杂,难以理解,这里直接看展开后的结果:
{
unsigned int __i, __n;
int __ret = 0;
struct ipt_entry *__entry;
//size是表中所有规则的大小,__entry->next_offset保存的是距下一条规则的偏移
for (__i = 0, __n = 0; __i < (size); __i += __entry->next_offset, __n++) {
//指向一条规则的起点
__entry = (void *)(entries) + __i;
//__n在本场景不可能会小于0
if (__n < 0)
continue;
//如果回调函数fn()返回非0,结束遍历,并且返回非0值
__ret = fn(__entry , ## args);
if (__ret != 0)
break;
}
__ret;
}
static int check_entry_size_and_hooks(struct ipt_entry *e,
struct xt_table_info *newinfo, unsigned char *base, unsigned char *limit,
const unsigned int *hook_entries, const unsigned int *underflows,
unsigned int *i)
{
unsigned int h;
//边界对齐和规则长度是否超过了总的大小
if ((unsigned long)e % __alignof__(struct ipt_entry) != 0
|| (unsigned char *)e + sizeof(struct ipt_entry) >= limit) {
duprintf("Bad offset %p\n", e);
return -EINVAL;
}
//距下一条规则的偏移不合理,因为一条规则至少包含struct ipt_entry和struct
//ipt_entry_target,所以next_offset一定大于这两个结构体的长度
if (e->next_offset < sizeof(struct ipt_entry) + sizeof(struct ipt_entry_target)) {
duprintf("checking: element %p size %u\n", e, e->next_offset);
return -EINVAL;
}
//如果指定规则属于某个HOOK点的边界规则,赋值hook_entry和underflow
for (h = 0; h < NF_INET_NUMHOOKS; h++) {
if ((unsigned char *)e - base == hook_entries[h])
newinfo->hook_entry[h] = hook_entries[h];
if ((unsigned char *)e - base == underflows[h])
newinfo->underflow[h] = underflows[h];
}
//规则命中的计数器清零
e->counters = ((struct xt_counters) { 0, 0 });
e->comefrom = 0;
//规则检查通过,计数器累加1
(*i)++;
return 0;
}
每条规则都是有0个或多个match以及1个target组成,每个match和target都有自己特有的参数,在校验规则时需要回调match和target的check_xxx()回调对规则进行校验。
static int find_check_entry(struct ipt_entry *e, const char *name,
unsigned int size, unsigned int *i)
{
struct ipt_entry_target *t;
struct xt_target *target;
int ret;
unsigned int j;
//对规则中的标准匹配以及target之间的偏移量进行校验
ret = check_entry(e, name);
if (ret)
return ret;
//对规则中match进行校验,包括该match是否注册以及调用match自身check()回调
j = 0;
ret = IPT_MATCH_ITERATE(e, find_check_match, name, &e->ip, e->comefrom, &j);
if (ret != 0)
goto cleanup_matches;
//获取规则中的target,检查该target是否存在
t = ipt_get_target(e);
target = try_then_request_module(xt_find_target(AF_INET, t->u.user.name,
t->u.user.revision), "ipt_%s", t->u.user.name);
if (IS_ERR(target) || !target) {
duprintf("find_check_entry: `%s' not found\n", t->u.user.name);
ret = target ? PTR_ERR(target) : -ENOENT;
goto cleanup_matches;
}
//找到对应的target模块,用u.kernel.target指向该target
t->u.kernel.target = target;
//调用target的check()回调校验target
ret = check_target(e, name);
if (ret)
goto err;
(*i)++;
return 0;
...
}