1.框架概念
Netfiler是一套融入到linux内核网络协议栈的框架。
在报文流经的关键位置处,基于不同协议(ipv4/ipv6)的hook方式,使用hook列表中对应的钩子函数匹配处理,以实现过滤、修改报文、跟踪等功能;最后处理过的报文会被决定以“放行”或者“丢弃”行为。
1.1 hook注册的钩子函数具有优先级顺序的执行, 值越小,优先级越高
enum nf_ip_hook_priorities {
...
NF_IP_PRI_RAW = -300, /*对应 raw table*/
...
NF_IP_PRI_MANGLE = -150, /*对应 mangle table*/
NF_IP_PRI_NAT_DST = -100, /*对应 nat table*/
NF_IP_PRI_FILTER = 0, /*对应 filter table*/
...
NF_IP_PRI_NAT_SRC = 100, /*对应 nat table*/
...
};
1.2 hook被划分成5个关键位置处:PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING
enum nf_inet_hooks {
NF_INET_PRE_ROUTING, /*对应 PREROUTING*/
NF_INET_LOCAL_IN, /*对应 INPUT*/
NF_INET_FORWARD, /*对应 FORWARD*/
NF_INET_LOCAL_OUT, /*对应 OUTPUT*/
NF_INET_POST_ROUTING,/*对应 POSTROUTING*/
NF_INET_NUMHOOKS
};
1.3 hook与chain(链)是一个东西,linux系统环境的iptables命令帮助信息(iptables -h),使用术语为chain(链)
iptables v1.4.21
Usage: iptables -[ACD] chain rule-specification [options]
iptables -I chain [rulenum] rule-specification [options]
iptables -R chain rulenum rule-specification [options]
iptables -D chain rulenum [options]
...
1.4 一个报文会经过的路径:
PRE ROUTING->INPUT:报文会在路由选择之前,简单的健全性检查,然后先经过PRE ROUTING点,再进行路由选择,若是本地的报文,途经INPUT点,然后交由上层处理。当然路由选择时可以丢弃不可达的报文
PRE ROUTING->FORWARD->POST ROUTING:报文会在路由选择之前,简单的健全性检查,然后先经过PRE ROUTING点,再进行路由选择,若不是本地的报文,则进入FORWARD点,然后途经最后一个POST ROUTING点,选择对应子网的出口发送出去。
OUTPUT->POST ROUTING:上层处理完后,创建报文,先经过OUTPUT点,然后途经最后一个POST ROUTING点,根据子网选择对应网口发送出去。
注意:网络上有文章描述OUTPUT点之前或者之后会调用”路由选择“行为,刚接触的同学们会被整懵;小生通过代码阅读以及找到netfiler官方文档也作了描述(https://netfilter.org/documentation/HOWTO/netfilter-hacking-HOWTO-3.html))。实际上,报文先处理路由选择,计算源地址和IP选项 ,然后才进入该hook点。如下的流程图关注的是Hook点的经过,因此没有把 “本地发送->route ->OUTPUT”流程描述出来。
2. hook点和table的关系
每个hook点对应的功能繁多,需要配置N条规则,并且有序的执行;如果过多规则被配置的话,不好维护,所以table就诞生了。table一方面细分功能,另一方面把不同的规则划分到不同table里,加以限定执行的顺序。
比如:用户基于PRE ROUTING点,在RAW表、MANGLE表配置多条规则;然后当报文经过PRE ROUTING点时,因RAW表优先级比MANGLE表高(hook优先级列表),所以RAW表的规则先执行,再执行MANGLE表的规则。
因此所谓的”四表五链“,就是基于4张table和5个hook的关系,来管理诸多规则。
4张表神奇的作用:raw表用于连接跟踪、 mangle表用于修改报文信息、filter表用于过滤报文信息、nat表用于映射地址信息。
3. hook的数据结构、注册钩子列表、调用关键函数
如下hook定义的结构体信息,可以看出它存在于不同net namespace的 struct net结构中,由于net namespace方面可以在同一个系统中建立不同的网络环境,因此hook上的table规则是独立于net namespace中的。
它们之间的包含关系,以及使用struct nf_hook_entries记录不同协议族的不同hook钩子函数信息(ipv4/ipv6协议族的hook点信息)),
struct net ->struct netns_nf -> struct nf_hook_entries-> struct nf_hook_entry[] -> struct nf_hook_ops
/*安装在hook点的钩子函数结构体*/
struct nf_hook_ops {
/* User fills in from here down. */
nf_hookfn *hook; /*钩子函数,报文经过hook点时调用*/
struct net_device *dev; /*对应的网络设备*/
void *priv; /*每个hook点独有的私有数据*/
u_int8_t pf; /*特定的协议族:NFPROTO_IPV4/NFPROTO_IPV6/...*/
unsigned int hooknum;/*钩子安装到哪个hook点*/
/* Hooks are ordered in ascending priority. */
int priority; /*优先级设置,决定该钩子函数的执行位置,优先级高,则先执行*/
};
/*以下结构体基于net namespace方面而定义*/
struct nf_hook_entry {
nf_hookfn *hook;/*隐式的指向nf_hook_ops结构体,仅显式的暴露钩子函数*/
void *priv;
};
struct nf_hook_entries {
u16 num_hook_entries;/*hook点的钩子函数注册的数量*/
/* padding */
struct nf_hook_entry hooks[];/*指向一堆hook点对应的钩子函数*/
/* trailer: pointers to original orig_ops of each hook,
* followed by rcu_head and scratch space used for freeing
* the structure via call_rcu.
*
* This is not part of struct nf_hook_entry since its only
* needed in slow path (hook register/unregister):
* const struct nf_hook_ops *orig_ops[]
*
* For the same reason, we store this at end -- its
* only needed when a hook is deleted, not during
* packet path processing:
* struct nf_hook_entries_rcu_head head
*/
};
/*定义包含不同协议族的hook点钩子函数的结构体*/
struct netns_nf {
...
struct nf_hook_entries __rcu *hooks_ipv4[NF_INET_NUMHOOKS];
struct nf_hook_entries __rcu *hooks_ipv6[NF_INET_NUMHOOKS];
#ifdef CONFIG_NETFILTER_FAMILY_ARP
struct nf_hook_entries __rcu *hooks_arp[NF_ARP_NUMHOOKS];
#endif
#ifdef CONFIG_NETFILTER_FAMILY_BRIDGE
struct nf_hook_entries __rcu *hooks_bridge[NF_INET_NUMHOOKS];
#endif
#if IS_ENABLED(CONFIG_DECNET)
struct nf_hook_entries __rcu *hooks_decnet[NF_DN_NUMHOOKS];
#endif
...
};
/*net namespace的结构体,系统可以有多个不同网络空间,隔离网络资源。*/
struct net {
...
struct netns_nf nf;/*每个net namespace独有的hook点列表*/
...
}
ipv4协议族的hook函数注册列表:仅把4张表的安装的钩子函数罗列出来,其它的表可自行查看。
4张表的内核源文件:iptable_raw.c、iptable_mangle.c、iptable_nat.c、iptable_filter.c
nf_hook函数和NF_HOOK宏定义,它们会在所有hook点的位置被调用, 即是入口点。
提示:可通过它们找到网络协议栈的源码位置,加以阅读哦。
static inline int
NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk, struct sk_buff *skb,
struct net_device *in, struct net_device *out,
int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
/*pf = 协议族, hook = 使用哪个hook点, skb = 报文*/
int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);
if (ret == 1)
ret = okfn(net, sk, skb);
return ret;
}
/**
* nf_hook - call a netfilter hook
*
* Returns 1 if the hook has allowed the packet to pass. The function
* okfn must be invoked by the caller in this case. Any other return
* value indicates the packet has been consumed by the hook.
*/
static inline int nf_hook(u_int8_t pf, unsigned int hook, struct net *net,
struct sock *sk, struct sk_buff *skb,
struct net_device *indev, struct net_device *outdev,
int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
struct nf_hook_entries *hook_head = NULL;
int ret = 1;
...
/*选择协议族的hook列表*/
rcu_read_lock();
switch (pf) {
case NFPROTO_IPV4:
hook_head = rcu_dereference(net->nf.hooks_ipv4[hook]);
break;
case NFPROTO_IPV6:
hook_head = rcu_dereference(net->nf.hooks_ipv6[hook]);
break;
case NFPROTO_ARP:
#ifdef CONFIG_NETFILTER_FAMILY_ARP
if (WARN_ON_ONCE(hook >= ARRAY_SIZE(net->nf.hooks_arp)))
break;
hook_head = rcu_dereference(net->nf.hooks_arp[hook]);
#endif
break;
case NFPROTO_BRIDGE:
#ifdef CONFIG_NETFILTER_FAMILY_BRIDGE
hook_head = rcu_dereference(net->nf.hooks_bridge[hook]);
#endif
break;
#if IS_ENABLED(CONFIG_DECNET)
case NFPROTO_DECNET:
hook_head = rcu_dereference(net->nf.hooks_decnet[hook]);
break;
#endif
default:
WARN_ON_ONCE(1);
break;
}
if (hook_head) {
struct nf_hook_state state;
/*把相关信息,设置到state结构里*/
nf_hook_state_init(&state, hook, pf, indev, outdev,
sk, net, okfn);
/*找到对应的hook函数列表 hook_head,遍历有序的函数进行匹配*/
ret = nf_hook_slow(skb, &state, hook_head, 0);
}
rcu_read_unlock();
return ret;
}
以上源代码以及内容基于内核4.19.1产出,不同的内核版本,表定义的hook点会有不同,如:nat表 原来只定义了pre routing/output/post routing,4.19.1中的源码新增input。
—越简单,易接受。在折腾路上…