一 概述
前篇主要提到了用户空间iptables 1.3.5源码对规则的处理。但是并没有涉及内核空间netfliter 模块的处理。用户空间上的规则要生效最终肯定是通过传给内核空间的netfilter,让netfliter这个老大哥处理。因此抛出两个问题。
· 用户空间的是怎么获取内核空间已经存在的规则,或者用户空间是如何将需要netfilter处理的规则下发给内核?简而言之,规则在用户空间是如何跟内核空间交互的。
· netfilter 内核收到用户空间的请求,规则在内核是怎么处理的?最终又是怎么让规则起作用,达到防火墙预想的功能呢?
本篇主要解决这两个问题
二 iptables 用户空间 与 netfilter 内核空间规则的交互
本章深入的内核版本是比较新的4.18.15 版本。
在说明内核,用户间如何交互时,前篇有张图其实很清楚了展示了交互的过程,如下图所示
这幅图,很直观的反应了用户空间的iptables和内核空间的基于Netfilter的ip_tables模块之间的关系和其通讯方式,以及Netfilter在这其中所扮演的角色。
从图中可以看出iptables 是通过setsockopt,getsockopt 系统调用获取传递规则链的。
那么iptables 1.3.5 源码是真的这么实现的吗?前篇似乎并没有看到这两个系统调用。
其实不然,iptables 操作规则跟内核交互都交给了libiptc.so库实现了。
1.用户层的交互处理
libiptc.so 库里调用的iptc_init() ,iptc_commit() 里面分配调用了setsockopt,getsockopt
iptc_init() 函数
iptc_handle_t iptc_init(const char *tablename);
#define TC_INIT iptc_init //宏定义
#define TC_HANDLE_T iptc_handle_t //宏定义
TC_HANDLE_T
TC_INIT(const char *tablename)
{
TC_HANDLE_T h;
STRUCT_GETINFO info;
unsigned int tmp;
socklen_t s;
iptc_fn = TC_INIT;
if (strlen(tablename) >= TABLE_MAXNAMELEN) {
errno = EINVAL;
return NULL;
}
if (sockfd_use == 0) {
sockfd = socket(TC_AF, SOCK_RAW, IPPROTO_RAW);
if (sockfd < 0)
return NULL;
}
sockfd_use++;
s = sizeof(info);
/*----------------------关键部分一 start-----------------------*/
strcpy(info.name, tablename);
if (getsockopt(sockfd, TC_IPPROTO, SO_GET_INFO, &info, &s) < 0) {
if (--sockfd_use == 0) {
close(sockfd);
sockfd = -1;
}
return NULL;
}
/*---------------------------end------------------------------*/
DEBUGP("valid_hooks=0x%08x, num_entries=%u, size=%u\n",
info.valid_hooks, info.num_entries, info.size);
if ((h = alloc_handle(info.name, info.size, info.num_entries))
== NULL) {
if (--sockfd_use == 0) {
close(sockfd);
sockfd = -1;
}
return NULL;
}
/* Initialize current state */
/*----------------------关键部分二start-----------------------*/
h->info = info;
h->entries->size = h->info.size;
tmp = sizeof(STRUCT_GET_ENTRIES) + h->info.size;
if (getsockopt(sockfd, TC_IPPROTO, SO_GET_ENTRIES, h->entries,
&tmp) < 0)
goto error;
/*-----------------------end--------------------------------*/
#ifdef IPTC_DEBUG2
{
int fd = open("/tmp/libiptc-so_get_entries.blob",
O_CREAT|O_WRONLY);
if (fd >= 0) {
write(fd, h->entries, tmp);
close(fd);
}
}
#endif
if (parse_table(h) < 0)
goto error;
CHECK(h);
return h;
error:
if (--sockfd_use == 0) {
close(sockfd);
sockfd = -1;
}
TC_FREE(&h);
return NULL;
}
接下来分析代码中标志的关键部分
关键部分一:
strcut STRUCT_GETINFO info;
getsockopt(sockfd, TC_IPPROTO, SO_GET_INFO, &info, &s);
根据info.name 告知内核需要那张表的规则信息。然后内核会返回这张表内规则的想关信息。例如这张表管理了几条链,这张表有多少条规则,规则的占用的空间大小等等
关键部分二:
getsockopt(sockfd, TC_IPPROTO, SO_GET_ENTRIES, h->entries, &tmp) < 0
再根据SO_GET_INFO获取的表内规则的信息,内核会再把这表上的具体规则数据返回给用户。
iptc_commit() 函数 (本函数比较长,只列出了关键代码)
int iptc_commit(iptc_handle_t *handle);
#define TC_COMMIT iptc_commit//宏定义
int
TC_COMMIT(TC_HANDLE_T *handle)
{
ret = setsockopt(sockfd, TC_IPPROTO, SO_SET_REPLACE, repl,
sizeof(*repl) + repl->size);
if (ret < 0) {
errno = ret;
goto out_free_newcounters;
}
ret = setsockopt(sockfd, TC_IPPROTO, SO_SET_ADD_COUNTERS,
newcounters, counterlen);
if (ret < 0) {
errno = ret;
goto out_free_newcounters;
}
本函数大部分都在填充要传递到内核的STRUCT_REPLACE *repl;STRUCT_COUNTERS_INFO *newcounters;两个结构体,然后下发到内核。
STRUCT_REPLACE *repl 后面内核的时候会做详细讲解,保存着链上的规则。
而newcounters 结构体是干嘛用的呢?每条规则entry都一个计数器,用来记录该规则处理了多少数据包。
至此用户层如何跟内核交互已经了解了大概。
· setsockopt新增命令字:
#define SO_SET_REPLACE//设置规则
#define SO_SET_ADD_COUNTERS //加入计数器
· getsockopt新增命令字;
#define SO_GET_INFO //获取ipt_info
#define SO_GET_ENTRIES //获取规则
但是内核层又是怎么处理用户层的请求的呢?
2 内核层的交互处理
上图是之前大神总结的用户层调用setsockopt 系统调用时内核层是如何一步一步处理的。本人也跟着上图步骤验证过。
// socket.c 文件中
static int __sys_setsockopt(int fd, int level, int optname,
char __user *optval, int optlen)
{
int err, fput_needed;
struct socket *sock;
if (optlen < 0)
return -EINVAL;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock != NULL) {
err = security_socket_setsockopt(sock, level, optname);
if (err)
goto out_put;
if (level == SOL_SOCKET)
err =
sock_setsockopt(sock, level, optname, optval,
optlen);
else
err =
sock->ops->setsockopt(sock, level, optname, optval,
optlen);
out_put:
fput_light(sock->file, fput_needed);
}
return err;
}
还记得iptables 用户空间怎么创建了套接字 用来规则的交互。
sockfd = socket(TC_AF, SOCK_RAW, IPPROTO_RAW);
setsockopt(sockfd, TC_IPPROTO, SO_SET_REPLACE, repl, sizeof(*repl) + repl->size);
从图中也可以发现不是标准的命令字SOL_SOCKET 而是根据SOCK_RAW,创建的fd调用了raw_setsockopt 。
而代码上是调用了sock->ops->setsockopt 回调函数。它是怎么最终调用了raw_setsockopt函数呢?
// 在 ipv4/raw.c 代码中
struct proto raw_prot = {
.name = "RAW",
.owner = THIS_MODULE,
.close = raw_close,
.destroy = raw_destroy,
.connect = ip4_datagram_connect,
.disconnect = __udp_disconnect,
.ioctl = raw_ioctl,
.init = raw_init,
.setsockopt = raw_setsockopt,
.getsockopt = raw_getsockopt,
.sendmsg = raw_sendmsg,
.recvmsg = raw_recvmsg,
.bind = raw_bind,
.backlog_rcv = raw_rcv_skb,
.release_cb = ip4_datagram_release_cb,
.hash = raw_hash_sk,
.unhash = raw_unhash_sk,
.obj_size = sizeof(struct raw_sock),
.useroffset = offsetof(struct raw_sock, filter),
.usersize = sizeof_field(struct raw_sock, filter),
.h.raw_hash = &raw_v4_hashinfo,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_raw_setsockopt,
.compat_getsockopt = compat_raw_getsockopt,
.compat_ioctl = compat_raw_ioctl,
#endif
.diag_destroy = raw_abort,
};
static int raw_setsockopt(struct sock *sk, int level, int optname,
char __user *optval, unsigned int optlen)
{
if (level != SOL_RAW)
return ip_setsockopt(sk, level, optname, optval, optlen);
return do_raw_setsockopt(sk, level, optname, optval, optlen);
}
static int raw_getsockopt(struct sock *sk, int level, int optname,
char __user *optval, int __user *optlen)
{
if (level != SOL_RAW)
return ip_getsockopt(sk, level, optname, optval, optlen);
return do_raw_getsockopt(sk, level, optname, optval, optlen);
}
简而言之,当你创建socket的时候,底层就会对struct socket opt 操作集赋值成 ipv4/af_inet.c 中的inet_sockraw_ops 操作集。struct sock port 操作集赋值成 ipv4/raw raw_prot 操作集中。这个过程在调用socket函数就确定了。
而再调用setsockopt 函数时,其实就是根据socket套接字一层层调用每层赋值的操作集函数即可。
对应上面过程: struct socket sock->ops->setsockopt 其实就是调用 ipv4/af_inet.c 中的inet_sockraw_ops 操作集sock_common_setsockopt函数,
然后sock_common_setsockopt函数里面会调用struct sock port 本质就是调用 ipv4/raw raw_prot 操作集中 raw_setsockopt 函数。这是协议栈的很重要的一个封装思想,一层层的调用本层处理函数
ip_getsockopt 最终会调用nf_sockopt 函数。
static int nf_sockopt(struct sock *sk, u_int8_t pf, int val,
char __user *opt, int *len, int get)
{
struct nf_sockopt_ops *ops;
int ret;
ops = nf_sockopt_find(sk, pf, val, get);
if (IS_ERR(ops))
return PTR_ERR(ops);
if (get)
ret = ops->get(sk, val, opt, len);
else
ret = ops->set(sk, val, opt, *len);
module_put(ops->owner);
return ret;
}
netfitler对于会实例化一些struct nf_sockopt_ops{}对象,然后通过nf_register_sockopt()将其注册到全局链表nf_sockopts里。因此最终nf_sockopt 函数通过全局链表找到对应注册的对象,最终调用do_ipt_get_ctl函数
// ip_tables.c 文件中
static struct nf_sockopt_ops ipt_sockopts = {
.pf = PF_INET,
.set_optmin = IPT_BASE_CTL,
.set_optmax = IPT_SO_SET_MAX+1,
.set = do_ipt_set_ctl,
#ifdef CONFIG_COMPAT
.compat_set = compat_do_ipt_set_ctl,
#endif
.get_optmin = IPT_BASE_CTL,
.get_optmax = IPT_SO_GET_MAX+1,
.get = do_ipt_get_ctl,
#ifdef CONFIG_COMPAT
.compat_get = compat_do_ipt_get_ctl,
#endif
.owner = THIS_MODULE,
};
static int
do_ipt_get_ctl(struct sock *sk, int cmd, void __user *user, int *len)
{
int ret;
if (!ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN))
return -EPERM;
/*--------------------------关键部分--------------------------*/
switch (cmd) {
case IPT_SO_GET_INFO:
ret = get_info(sock_net(sk), user, len, 0);
break;
case IPT_SO_GET_ENTRIES:
ret = get_entries(sock_net(sk), user, len);
break;
/*----------------------------------------------------*/
case IPT_SO_GET_REVISION_MATCH:
case IPT_SO_GET_REVISION_TARGET: {
struct xt_get_revision rev;
int target;
if (*len != sizeof(rev)) {
ret = -EINVAL;
break;
}
if (copy_from_user(&rev, user, sizeof(rev)) != 0) {
ret = -EFAULT;
break;
}
rev.name[sizeof(rev.name)-1] = 0;
if (cmd == IPT_SO_GET_REVISION_TARGET)
target = 1;
else
target = 0;
try_then_request_module(xt_find_revision(AF_INET, rev.name,
rev.revision,
target, &ret),
"ipt_%s", rev.name);
break;
}
default:
ret = -EINVAL;
}
return ret;
}
最后看代码标注的关键部分,可以发现最终用户层的请求传到真正的netfilter处理函数。额外提下,你会发现内核中有两个函数,copy_from_user ,copy_to_user 专门负责用户空间内存与内核空间内存间的copy ,而上面的系统跟踪貌似看起来跟我们要说的netfiter 原理没啥大的关系。但是通过跟踪,你会发现liunx 内核有个很重要的概念 就是模块化。例如上面提到的怎么调用raw_setsockopt ,其实就是将 ipv4/raw 模块初始化好了,而后根据fd找到这个模块名称,最后调用模块的自己的处理函数。还有后面的nf_sockopt_ops 注册到全局链,再通过查找全局链找到相应对象,再做对应处理。这也算是模块化,模块化可以很方便的扩展,同时也不会影响到其他模块的代码。
言归正传。用户空间与内核空间如何交互已经清楚了,但是比如说用户想获取filter表下的所有规则时,内核中是怎么拿到这规则返回给用户的呢?这也是本篇前面提到的第二个问题的一部分。
三 netfilter 对规则的管理
在说内核对规则怎么操作前,有必要先说内核中几个跟规则相关的结构体,和一些定义的宏变量
//在 netfilter.h 有几个重要的宏变量
#define NF_DROP 0
#define NF_ACCEPT 1
#define NF_STOLEN 2
#define NF_QUEUE 3
#define NF_REPEAT 4
#define NF_STOP 5 /* Deprecated, for userspace nf_queue compatibility. */
#define NF_MAX_VERDICT NF_STOP // 调用钩子函数时的返回值,可以表示该数据的钩子函数处理后的几个状态,前篇有介绍过
enum nf_inet_hooks {
NF_INET_PRE_ROUTING,
NF_INET_LOCAL_IN,
NF_INET_FORWARD,
NF_INET_LOCAL_OUT,
NF_INET_POST_ROUTING,
NF_INET_NUMHOOKS
}; // 这个枚举体定义了nf_inet 协议下的所支持的几张链,也就是hook
enum {
NFPROTO_UNSPEC = 0,
NFPROTO_INET = 1,
NFPROTO_IPV4 = 2,
NFPROTO_ARP = 3,
NFPROTO_NETDEV = 5,
NFPROTO_BRIDGE = 7,
NFPROTO_IPV6 = 10,
NFPROTO_DECNET = 12,
NFPROTO_NUMPROTO,
};// 枚举几种常用协议
// 在 xt_tables.h 文件
struct xt_table {
struct list_head list; // 双向链表,可以将不同的表链接起来。
/* What hooks you will enter on */
unsigned int valid_hooks; // 用了位操作,可以表示一张表中挂载了几条链(hook)
/* Man behind the curtain... */
struct xt_table_info *private;//描述表的具体属性,如表的size,表中的规则数等
/* Set this to THIS_MODULE if you are a module, otherwise NULL */
struct module *me; //如果要设计成模块,则为THIS_MODULE;否则为NULL
u_int8_t af; /* address/protocol family */
int priority; /* hook order */ // 优先级
/* called when table is needed in the given netns */
int (*table_init)(struct net *net); // 该回调可以将表挂载到net 上,后面可以根据struct net 就可以找到对应的xt_table.
/* A unique name... */
const char name[XT_TABLE_MAXNAMELEN]; //表的名称 如filter表 ,nat 表
};
/* The table itself */
struct xt_table_info {
/* Size per table */
unsigned int size; //表的大小,即占用的内存空间
/* Number of entries: FIXME. --RR */
unsigned int number; //表中的规则数
/* Initial number of entries. Needed for module usage count */
unsigned int initial_entries; //初始的规则数,用于模块计数
/* Entry points and underflows */
unsigned int hook_entry[NF_INET_NUMHOOKS]; //记录所影响的HOOK的规则入口,相对于下面的entries变量的偏移量
unsigned int underflow[NF_INET_NUMHOOKS]; //与hook_entry相对应的规则表上限偏移量
/*
* Number of user chains. Since tables cannot have loops, at most
* @stacksize jumps (number of user chains) can possibly be made.
*/
unsigned int stacksize;
void ***jumpstack;
unsigned char entries[0] __aligned(8);// 可变数组,最终表的规则都会放到这里
};
xt_table , xt_table_info 这两个结构体,就可以分析。内核在接收到SO_GET_INFO ,SO_GET_ENTRIES 这两个请求的时候。是如何将内核的规则返回给用户空间的了。
从第二章节中,我们可以知道内核在接收到SO_GET_INFO ,SO_GET_ENTRIES 两个请求时,最终分别调用了 get_info ,get_entries 两个函数,而get_info 获取内核规则的一些信息,比较简单,这里不做简述。主要讲get_entries函数,获取表内的所有规则信息。
static int
get_entries(struct net *net, struct ipt_get_entries __user *uptr,
const int *len)
{
int ret;
struct ipt_get_entries get;
struct xt_table *t;
if (*len < sizeof(get))
return -EINVAL;
if (copy_from_user(&get, uptr, sizeof(get)) != 0)
return -EFAULT;
if (*len != sizeof(struct ipt_get_entries) + get.size)
return -EINVAL;
get.name[sizeof(get.name) - 1] = '\0';
t = xt_find_table_lock(net, AF_INET, get.name);
if (!IS_ERR(t)) {
const struct xt_table_info *private = t->private;
if (get.size == private->size)
ret = copy_entries_to_user(private->size,
t, uptr->entrytable);
else
ret = -EAGAIN;
module_put(t->me);
xt_table_unlock(t);
} else
ret = PTR_ERR(t);
return ret;
}
你会发现get_entries 函数里先调用 xt_find_table_lock(net, AF_INET, get.name);根据表名获取到xt_table 结构体,而拿到了xt_table ,要是之前对该结构体的分析你深入理解了,其实你就能拿到这个表内所有的规则信息。而这段代码的核心就是copy_entries_to_user 这函数。
static int
copy_entries_to_user(unsigned int total_size,
const struct xt_table *table,
void __user *userptr)
{
unsigned int off, num;
const struct ipt_entry *e;
struct xt_counters *counters;
const struct xt_table_info *private = table->private;
int ret = 0;
const void *loc_cpu_entry;
counters = alloc_counters(table);
if (IS_ERR(counters))
return PTR_ERR(counters);
loc_cpu_entry = private->entries;
/* FIXME: use iterator macros --RR */
/* ... then go back and fix counters and names */
for (off = 0, num = 0; off < total_size; off += e->next_offset, num++){
unsigned int i;
const struct xt_entry_match *m;
const struct xt_entry_target *t;
e = loc_cpu_entry + off;
if (copy_to_user(userptr + off, e, sizeof(*e))) {
ret = -EFAULT;
goto free_counters;
}
if (copy_to_user(userptr + off
+ offsetof(struct ipt_entry, counters),
&counters[num],
sizeof(counters[num])) != 0) {
ret = -EFAULT;
goto free_counters;
}
for (i = sizeof(struct ipt_entry);
i < e->target_offset;
i += m->u.match_size) {
m = (void *)e + i;
if (xt_match_to_user(m, userptr + off + i)) {
ret = -EFAULT;
goto free_counters;
}
}
t = ipt_get_target_c(e);
if (xt_target_to_user(t, userptr + off + e->target_offset)) {
ret = -EFAULT;
goto free_counters;
}
}
free_counters:
vfree(counters);
return ret;
}
上面这段代码,前篇提到的一条规则由那几个部分组成。如果你还记得,你会发现 for (off = 0, num = 0; off < total_size; off += e->next_offset, num++){
} 就是循环将一条规则的各个组成部分,分别copy到用户空间。循环结束,实则用户空间已经拿到跟这张表的所有规则。可以回顾前篇提到的这张图
已经了解了用户层规则是如何从内核获取到,但是内核层xt_table内部是在哪里初始化,注册的呢?
在说明这问题时,还是先看几个重要的结构体
struct ipt_replace 结构体
// 在 ip_tables.h
struct ipt_replace {
/* Which table. */
char name[XT_TABLE_MAXNAMELEN]; //表的名字
/* Which hook entry points are valid: bitmask. You can't
change this. */
unsigned int valid_hooks; //所影响的HOOK点
/* 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]; //进入HOOK的入口点
/* Underflow points. */
unsigned int underflow[NF_INET_NUMHOOKS];
/* 这个结构不同于ipt_table_info之处在于它还要保存旧的规则信息*/
/* 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;
/* The entries (hang off end: not really an array). */
struct ipt_entry entries[0];
};
你会发现用户层iptables 1.3.5 设置规则时,传入的参数就是这个结构体。最终会将要设置的规则,加入到xt_table 中的xt_table_info 的规则中。而初始化xt_table 时也会用这个结构体。这个后面一会细说。
struct nf_hook_ops 结构体
// 在 netfilter.h 文件
typedef unsigned int nf_hookfn(void *priv,
struct sk_buff *skb,
const struct nf_hook_state *state); // 钩子回调函数
struct nf_hook_ops {
/* User fills in from here down. */
nf_hookfn *hook; // 钩子回调函数
struct net_device *dev;
void *priv;
u_int8_t pf; //该hook函数所处理的协议。
unsigned int hooknum; //钩子函数的挂载点,即HOOK点; 表示挂载在那条链上的
/* Hooks are ordered in ascending priority. */
int priority; // 优先级。一个HOOK点可能挂载了多个钩子函数
};
这个结构体是干嘛用的呢?你知道规则产生后,最终一条数据进来,你首先会去匹配规则,要是规则对应上,然后交给对应的钩子函数处理,对于filter表,它关联了三条链(INPUT , FORWARD ,OUTPUT),每条都会有钩子函数,会决定数据的去留。本章只简单说下钩子函数如何注册,后篇会侧重说下钩子函数做了那些事。
struct net 结构体有两个变量
· struct netns_nf nf; // 里面挂载着根据不同的链注册的钩子函数
· struct netns_xt xt; //
struct netns_nf {
struct nf_hook_entries __rcu *hooks_ipv4[NF_INET_NUMHOOKS]; // 不同的链对应不同的钩子函数
}
struct nf_hook_entries {
u16 num_hook_entries; //表示这条链注册了多少个钩子函数
/* padding */
struct nf_hook_entry hooks[]; // 对应注册的所有钩子函数数组
}
struct nf_hook_entry {
nf_hookfn *hook;
void *priv;
};
struct netns_xt {
struct list_head tables[NFPROTO_NUMPROTO];
bool notrack_deprecated_warning;
bool clusterip_deprecated_warning;
}
这个结构体struct net 有啥用呢? 其实最后注册的钩子函数都是挂载在struct netns_nf nf下的,注册的所有的表也是挂载在struct netns_xt xt; 下的
我们知道iptables 用到了 raw,mangle,nat ,fliter 四张表,每张表都需要初始化注册。而内核在这里也采用了模块化,所以这里分析,主要分析常用的filter 表,其他表相信大伙能举一反三。
// 在 iptable_filter.c 文件
/*----------初始化的filter表------------------*/
#define FILTER_VALID_HOOKS ((1 << NF_INET_LOCAL_IN) | \
(1 << NF_INET_FORWARD) | \
(1 << NF_INET_LOCAL_OUT))
static const struct xt_table packet_filter = {
.name = "filter",
.valid_hooks = FILTER_VALID_HOOKS,
.me = THIS_MODULE,
.af = NFPROTO_IPV4,
.priority = NF_IP_PRI_FILTER,
.table_init = iptable_filter_table_init,
};
/*---------------钩子回调函数------------------*/
static unsigned int
iptable_filter_hook(void *priv, struct sk_buff *skb,
const struct nf_hook_state *state)
{
return ipt_do_table(skb, state, state->net->ipv4.iptable_filter);
}
static struct nf_hook_ops *filter_ops __read_mostly;
/*----------filter 模块初始化最开始执行的函数------------------*/
static int __init iptable_filter_init(void)
{
int ret;
filter_ops = xt_hook_ops_alloc(&packet_filter, iptable_filter_hook);
if (IS_ERR(filter_ops))
return PTR_ERR(filter_ops);
ret = register_pernet_subsys(&iptable_filter_net_ops);
if (ret < 0)
kfree(filter_ops);
return ret;
}
你会发现程序初始化了 xt_table 表结构,从初始化的valid_hooks变量,fliter表关联了INPUT ,FORWARD,OUTPUT三条链,而xt_hook_ops_alloc 函数其实就是根据filter关联的链,为每条链初始化了 struct nf_hook_ops 结构体,(即每条链都初始化了对应的钩子处理函数)。而register_pernet_subsys 最终会调用一个关键函数iptable_filter_table_init
// 也是在 iptable_filter.c 文件
static int __net_init iptable_filter_table_init(struct net *net)
{
struct ipt_replace *repl;
int err;
if (net->ipv4.iptable_filter)
return 0;
repl = ipt_alloc_initial_table(&packet_filter);
if (repl == NULL)
return -ENOMEM;
/* Entry 1 is the FORWARD hook */
((struct ipt_standard *)repl->entries)[1].target.verdict =
forward ? -NF_ACCEPT - 1 : -NF_DROP - 1;
err = ipt_register_table(net, &packet_filter, repl, filter_ops,
&net->ipv4.iptable_filter);
kfree(repl);
return err;
}
ipt_alloc_initial_table 函数其实就初始化了 struct ipt_replace 结构体,代码如下图
void *ipt_alloc_initial_table(const struct xt_table *info)
{
return xt_alloc_initial_table(ipt, IPT);
}
#define xt_alloc_initial_table(type, typ2) ({ \
unsigned int hook_mask = info->valid_hooks; \
unsigned int nhooks = hweight32(hook_mask); \
unsigned int bytes = 0, hooknum = 0, i = 0; \
struct { \
struct type##_replace repl; \
struct type##_standard entries[]; \
} *tbl; \
struct type##_error *term; \
size_t term_offset = (offsetof(typeof(*tbl), entries[nhooks]) + \
__alignof__(*term) - 1) & ~(__alignof__(*term) - 1); \
tbl = kzalloc(term_offset + sizeof(*term), GFP_KERNEL); \
if (tbl == NULL) \
return NULL; \
term = (struct type##_error *)&(((char *)tbl)[term_offset]); \
strncpy(tbl->repl.name, info->name, sizeof(tbl->repl.name)); \
*term = (struct type##_error)typ2##_ERROR_INIT; \
tbl->repl.valid_hooks = hook_mask; \
tbl->repl.num_entries = nhooks + 1; \
tbl->repl.size = nhooks * sizeof(struct type##_standard) + \
sizeof(struct type##_error); \
for (; hook_mask != 0; hook_mask >>= 1, ++hooknum) { \
if (!(hook_mask & 1)) \
continue; \
tbl->repl.hook_entry[hooknum] = bytes; \
tbl->repl.underflow[hooknum] = bytes; \
tbl->entries[i++] = (struct type##_standard) \
typ2##_STANDARD_INIT(NF_ACCEPT); \
bytes += sizeof(struct type##_standard); \
} \
tbl; \
})
你会发现对struct ipt_replace 初始化 其实放到了一个宏定义中,你把\ 去掉宏定义传入的参数替换掉,最终其实就是返回一个结构体struct { struct ipt_replace repl; struct ipt_standard entries[]; } *tbl; 这里是不是有点奇怪,看初始化的是struct ipt_replace repl 是这个结构体,怎么多了 struct ipt_standard entries[]; 其实这块强转struct ipt_replace 结构体 就对应struct ipt_replace repl结构体内的最后的那个可变数组变量
对于filter表 初始化时有"4"条规则链,每个HOOK点(对应用户空间的“规则链”)初始化成一条链,最后以一条“错误的规则”表示结束,filter表占(sizeof(struct ipt_standard) * 3+sizeof(struct ipt_error))字节的存储空间
到这里,你发现初始化了那些东西了呢?
· filter 表每条链的 struct nf_hook_ops filter_ops 钩子函数
· 初始化每条链对应的规则 struct ipt_replace repl
· 还有就是初始化了表xt_table的一些变量
还缺少关键的一步就是把初始化的 filter_ops 钩子函数,和初始化的规则tbl 都注册到xt_table 表中。最关键的函数 ipt_register_table();到底做了那些东西呢?
int ipt_register_table(struct net *net, const struct xt_table *table,
const struct ipt_replace *repl,
const struct nf_hook_ops *ops, struct xt_table **res)
{
int ret;
struct xt_table_info *newinfo;
struct xt_table_info bootstrap = {0};
void *loc_cpu_entry;
struct xt_table *new_table;
newinfo = xt_alloc_table_info(repl->size);
if (!newinfo)
return -ENOMEM;
loc_cpu_entry = newinfo->entries;
memcpy(loc_cpu_entry, repl->entries, repl->size);
ret = translate_table(net, newinfo, loc_cpu_entry, repl);
if (ret != 0)
goto out_free;
new_table = xt_register_table(net, table, &bootstrap, newinfo);
if (IS_ERR(new_table)) {
ret = PTR_ERR(new_table);
goto out_free;
}
/* set res now, will see skbs right after nf_register_net_hooks */
WRITE_ONCE(*res, new_table);
if (!ops)
return 0;
ret = nf_register_net_hooks(net, ops, hweight32(table->valid_hooks));
if (ret != 0) {
__ipt_unregister_table(net, new_table);
*res = NULL;
}
return ret;
out_free:
xt_free_table_info(newinfo);
return ret;
}
· translate_table 函数 主要就是将struct ipt_replace 数据复制给struct xt_table_info *newinfo;结构体,同时也验证了每条链的偏移量等一些信息。
· xt_register_table 函数 最终会把struct xt_table_info 结构体复制到xt_tables 结构体中的变量xt_table_info ,然后把xt_tables 挂载到 struct net 结构体下的struct netns_xt xt 结构体中。
· nf_register_net_hooks 函数 主要就是把每条链注册的钩子函数,挂载到 struct net 结构体下的struct netns_nf nf 结构体中。
四 回顾总结
先说个知识点
如果你细心,你会发现struct xt_table 结构体
和 struct net 下的 struct netns_xt xt 结构体 第一个变量都是 struct list_head 结构体;
struct list_head {
struct list_head *next, *prev;
};
xt_table 结构体保存规则的所有信息,其实它的数据结构简化后就是如下图所示。
xt_table 结构体内存分布
4字节指针 next
4字节指针 prev
规则的所有信息
其实最终struct xt_table 结构体 是挂载到 struct netns_xt 结构上的,它的实现就是通过list_add(&table->list, &net->xt.tables[table->af]);
将xt_table 下的struct list_head 加入到跟协议簇的关联的struct list_head [ ]数组链表上 。获取xt_table 时,就只需要获取到net->xt.tables[table->af]协议链的首地址,然后遍历双向链表获取到另一个的struct list_head 结构体的地址,也就是xt_table 前八个字节的结构体首地址。你已经拿到了xt_table 结构体保存规则的首指针了。然后通过强制转换成xt-table结构体 。当然最重要的一点,你得确保struct list_head 结构体的地址后的保存规则的所有信息没有被释放。
这是Linux内核中处理双向链表的标准方式。当某种类型的数据结构需要被组织成双向链表时,会在该数据结构的第一个字段放置一个list_head{}类型的成员。在后面的使用过程中可以通过强制类型转换来实现双向链表的遍历操作。
最后回顾下整个过程
· xt_table 表中的所有规则信息最终通过struct list 双向链表 挂载到 struct net 结构体中,表是有优先级的,先处理raw表,mangle表,nat表,filter表
· 每条链上的规则对应的钩子函数,也会挂载到 struct net 结构体中,一条链子也可能注册多个钩子函数,钩子函数也是有优先级的。
· xt_table 中所有的规则信息,存放在xt_table_info 结构体中,一条规则由三个部分组成,一个ipt_entry规则相关信息, 多个match模块,一个target模块
· 不同的协议下各自都会有张链表记录该协议下所有注册的表,不同的表中会关联不同的链表(hook),一条链表下有多条规则,而对于一条链表匹配的规则,会有多个钩子函数处理。
通过本篇相信对内核中netfilter 规则怎么处理有一定了解了。而本章已经提到了钩子函数,也知道钩子函数是对匹配规则的处理,起到真正防火墙的作用。但是却没有深入讨论这钩子函数。后篇会说明netfilter 的过滤filter功能 ,当然nat地址转换,还有连接跟踪数据包功能。也是很重要的一个功能,后面看要不要具体再写一章。
目前有 1 条留言 访客:0 条, 博主:0 条 ,引用: 1 条
外部的引用: 1 条
- linux下的iptables/netfilter防火墙 深度理解 后篇 | 求索阁