作者简介:张铭轩,西安邮电大学计算机专业研二学生,导师陈莉君教授,热衷于探索linux内核。
iptables和netfilter
iptables是Linux 上最常用的防火墙工具,iptables 与协议栈内有包过滤功能的 hook 交互来完成工作。这些内核 hook 构成了 netfilter 框架
每个进入网络系统的包(接收或发送)在经过协议栈时都会触发这些 hook,程序可以通过注册 hook 函数的方式在一些关键路径上处理网络流量。iptables 相关的内核模块在这些 hook 点注册了处理函数,因此可以通过配置 iptables 规则来使得网络流量符合防火墙规则。
netfilter框架
netfilter 提供了 5 个 hook 点。包经过协议栈时会触发内核模块注册在这里的处理函数 。
其中主要包括:
NF_IP_PRE_ROUTING
: 接收到的包进入协议栈后立即触发此 hook,在进行任何路由判断 (将包发往哪里)之前NF_IP_LOCAL_IN
: 接收到的包经过路由判断,如果目的是本机,将触发此 hookNF_IP_FORWARD
: 接收到的包经过路由判断,如果目的是其他机器,将触发此 hookNF_IP_LOCAL_OUT
: 本机产生的准备发送的包,在进入协议栈后立即触发此 hookNF_IP_POST_ROUTING
: 本机产生的准备发送的包或者转发的包,在经过路由判断之后, 将触发此 hook
以 ip_rcv
为例,它主要负责接收和处理 IPv4 数据包,在处理完核心逻辑后,会进入内核预义的钩子进行后续处理。
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,struct net_device *orig_dev)
{
struct net *net = dev_net(dev);//返回与给定网络设备关联的net结构
//net结构用以表示内核中的网络命名空间
skb = ip_rcv_core(skb, net);
if (skb == NULL)
return NET_RX_DROP;
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}
具体进入函数层面的分析:
#define NF_HOOK(proto, hooknum, net, skb, dev, in, out) \
nf_hook(proto, hooknum, net, skb, dev, in, out)
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; // 默认返回值
...
// 根据协议族选择钩子链表
switch (pf) {
case NFPROTO_IPV4:
hook_head = rcu_dereference(net->nf.hooks_ipv4[hook]);
// 获取与当前网络命名空间相关的 IPv4 钩子链表
break;
...
}
// 执行钩子处理
if (hook_head) {
struct nf_hook_state state;
// 初始化钩子状态
nf_hook_state_init(&state, hook, pf, indev, outdev, sk, net, okfn);
// 调用钩子处理函数
ret = nf_hook_slow(skb, &state, hook_head, 0);
}
...
return ret; // 返回处理结果
}
//进入具体的处理函数
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *e, unsigned int s)
{
unsigned int verdict;
int ret;
// 遍历所有钩子函数
for (; s < e->num_hook_entries; s++) {
// 调用当前钩子函数
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
// 如果钩子返回接受,则继续下一个钩子
break;
case NF_DROP:
// 如果钩子返回丢弃
kfree_skb_reason(skb, SKB_DROP_REASON_NETFILTER_DROP);
ret = NF_DROP_GETERR(verdict);
if (ret == 0)
ret = -EPERM; // 返回权限错误
return ret; // 终止处理并返回丢弃结果
case NF_QUEUE:
// 如果钩子返回队列
ret = nf_queue(skb, state, s, verdict);
if (ret == 1)
continue; // 继续下一个钩子
return ret; // 返回处理结果
default:
// 对于 NF_STOLEN 或其他非标准结果的隐式处理
return 0;
}
}
return 1; // 所有钩子处理完毕,返回接受结果
}
上述代码总结为,当 NF_HOOK
被调用时,Netfilter 会执行以下步骤:
查找钩子列表:根据传入的协议和钩子点,Netfilter 查找已注册的钩子函数列表。 调用钩子函数:遍历所有注册的钩子函数,依次调用每个钩子函数,并将数据包( skb
)传递给它们。处理结果:每个钩子函数可以选择:
继续处理数据包,返回 NF_ACCEPT
。丢弃数据包,返回 NF_DROP
。修改数据包并继续处理,返回 NF_QUEUE
或其他值。
iptables 表和链
iptables 使用 table 来组织规则,根据用来做什么类型的判断,将规则分为不同table。在每个 table 内部,规则被进一步组织成 chain,内置的 chain 是由内置的 hook 触发 的。chain 基本上能决定规则何时被匹配。内置的 chain 名字和 netfilter hook 名字是一一对应的:如PREROUTING是 由 NF_IP_PRE_ROUTING hook 触发的chain
因此不同 table 的 chain 最终都是注册到netfilter hook 。例如,有三个 table 有 PRETOUTING
chain。当这些 chain 注册到对应的 NF_IP_PRE_ROUTING
hook 点时,它们需要指定优先级,应该依次调用哪个 table 的 PRETOUTING
chain,优先级从高到低。
table种类
filter table:
filter table 是最常用的 table 之一,用于判断是否允许一个包通过。
nat table:
nat table 用于实现网络地址转换规则。当包进入协议栈的时候,这些规则决定是否以及如何修改包的源/目的地址,以改变包被路由时的行为。nat
table 通常用于将包路由到无法直接访问的网络。
mangle table:
mangle table 用于修改包的 IP 头。例如,可以修改包的 TTL,增加或减少包可以经过的跳数。这个 table 还可以对包打只在内核内有效的“标记”,后续的 table 或工具处理的时候可以用到这些标记。标记不会修改包本身,只是在包的内核表示上做标记。
raw table:
raw table 定义目的是使一个让包绕过连接跟踪。建立在 netfilter 之上的连接跟踪特性使得 iptables 将包 看作已有的连接或会话的一部分,而不是一个由独立、不相关的包组成的流。 数据包到达网络接口之后很快就会有连接跟踪逻辑判断。
security table:
security table 的作用是给包打上 SELinux 标记,以此影响 SELinux 或其他可以解读 SELinux 安全上下文的系统处理包的行为。这些标记可以基于单个包,也可以基于连接。
每种 table 实现的 chain
当一个包触发 netfilter hook 时,处理过程将沿着列从上向下执行。其有内置的优先级,数值越小越优先。
enum nf_ip_hook_priorities {
NF_IP_PRI_FIRST = INT_MIN,
NF_IP_PRI_RAW_BEFORE_DEFRAG = -450,
NF_IP_PRI_CONNTRACK_DEFRAG = -400,
NF_IP_PRI_RAW = -300, //raw
NF_IP_PRI_SELINUX_FIRST = -225,
NF_IP_PRI_CONNTRACK = -200, //conntrack
NF_IP_PRI_MANGLE = -150, //manage
NF_IP_PRI_NAT_DST = -100, //dnat
NF_IP_PRI_FILTER = 0, //filter
NF_IP_PRI_SECURITY = 50, //security
NF_IP_PRI_NAT_SRC = 100, //snat
NF_IP_PRI_SELINUX_LAST = 225,
NF_IP_PRI_CONNTRACK_HELPER = 300,
NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX,
NF_IP_PRI_LAST = INT_MAX,
};
特定事件会导致 table 的 chain 被跳过。例如,只有每个连接的第一个包会去匹配 NAT 规则,对这个包的动作会应用于此连接后面的所有包。到这个连接的应答包会被自动应用反方向的 NAT 规则。
对于不同的包,由于netfilter挂载函数的不同,导致其对应的chain也不同
收到的、目的是本机的包: PRETOUTING
->INPUT
收到的、目的是其他主机的包: PRETOUTING
->FORWARD
->POSTROUTING
本地产生的包: OUTPUT
->POSTROUTING
综合前面讨论的 table 顺序问题,我们可以看到对于一个收到的、目的是本机的包: 首先依次经过 PRETOUTING chain 上面的 raw、mangle、nat table;然后依次经 过 INPUT chain 的 mangle、filter、security、nat table,然后才会到达本机的某个 socket。
iptables 规则
规则放置在特定 table 的特定 chain 里面。当 chain 被调用的时候,包会依次匹配 chain 里面的规则。每条规则都有一个匹配部分和一个目标部分
规则的匹配部分指定了一些条件,包必须满足这些条件才会和相应的将要执行的动作进行关联。规则可以匹配协议类型、目的或源地址、目的或源端口、目的或源网段、接收或发送的接口(网卡)、协议头、连接状态等等条件。这些综合起来,能够组合成非常复杂的规则来区分不同的网络流量。
包符合某种规则的条件而触发的动作叫做目标。目标分为两种类型:
终止目标:这种 target 会终止 chain 的匹配,将控制权转移回 netfilter hook。根据返回值的不同,hook 或者将包丢弃,或者允许包进行下一 阶段的处理 非终止目标:非终止目标执行动作,然后继续 chain 的执行。虽然每个 chain 最终都会回到一个终止目标,但是在这之前,可以执行任意多个非终止目标
每个规则可以跳转到哪个 target 依上下文而定,例如,table 和 chain 可能会设置 target 可用或不可用。规则里激活的 extensions 和匹配条件也影响 target 的可用性。
还有一种特殊的非终止目标:跳转目标。jump target 是跳转到其他 chain 继续处理的动作。向用户自定义 chain 添加规则和向内置的 chain 添加规则的方式是相同的。不同的地方在于, 用户定义的 chain 只能通过从另一个规则跳转(jump)到它,因为它们没有注册到 netfilter hook。用户定义的 chain 可以看作是对调用它的 chain 的扩展。
iptables 和 conntrack
在讨论 raw table 和 匹配连接状态的时候,我们介绍了构建在 netfilter 之上的连接跟踪系统。连接跟踪系统使得 iptables 基于连接上下文而不是单个包来做出规则判断, 给 iptables 提供了有状态操作的功能。
跟踪系统将包和已有的连接进行比较,如果包所属的连接已经存在就更新连接状态, 否则就创建一个新连接。如果 raw table 的某个 chain 对包标记为目标是 NOTRACK, 那这个包会跳过连接跟踪系统。
连接跟踪系统中的连接状态有:
NEW
:如果到达的包关连不到任何已有的连接,但包是合法的,就为这个包创建一个新连接。对 面向连接的(connection-aware)的协议例如 TCP 以及非面向连接的(connectionless )的协议例如 UDP 都适用ESTABLISHED
:当一个连接收到应答方向的合法包时,状态从NEW
变成ESTABLISHED
。对 TCP 这个合法包其实就是SYN/ACK
包;对 UDP 和 ICMP 是源和目 的 IP 与原包相反的包RELATED
:包不属于已有的连接,但是和已有的连接有一定关系。这可能是辅助连接( helper connection),例如 FTP 数据传输连接,或者是其他协议试图建立连接时的 ICMP 应答包INVALID
:包不属于已有连接,并且因为某些原因不能用来创建一个新连接,例如无法 识别、无法路由等等UNTRACKED
:如果在raw
table 中标记为目标是UNTRACKED
,这个包将不会进入连 接跟踪系统SNAT
:包的源地址被 NAT 修改之后会进入的虚拟状态。连接跟踪系统据此在收到反向包时对地址做反向转换DNAT
:包的目的地址被 NAT 修改之后会进入的虚拟状态。连接跟踪系统据此在收到反向包时对地址做反向转换
iptable使用
iptables可以配置和管理 Netfilter 框架,其是一个用户态工具,用以定义网络数据包过滤规则,此处主要以NAT为例作为讲解
NAT
NAT是一种网络技术,用于在网络设备(如路由器、网关)上对数据包的源地址或目的地址进行修改,通常在局域网与互联网之间进行转换。NAT 主要用于节约 IPv4 地址,隐藏内部网络的结构,并在网络层提供一定程度的安全性。
NAT 在路由器或防火墙处修改通过的数据包的 IP 地址信息。常见的 NAT 类型包括:
SNAT(Source NAT):修改数据包的源 IP 地址,通常用于内网设备访问外网时,将内网设备的私有 IP 地址替换为公共 IP 地址。 DNAT(Destination NAT):修改数据包的目的 IP 地址,通常用于将外部请求映射到内网的某台服务器(如端口转发)。 NAT 通过维护一个映射表,跟踪修改前后的 IP 地址和端口号,从而保证数据包能正确地返回给发起请求的内部设备。
NAT 的类型
静态 NAT:一个内部 IP 地址映射到一个固定的外部 IP 地址。通常用于让内部服务器暴露给外部网络。 动态 NAT:内部 IP 地址池映射到外部 IP 地址池。适用于内网设备临时需要与外网通信,使用的外部 IP 地址动态分配。 PAT(端口地址转换),也叫端口多路复用:多个内部 IP 地址可以共享一个或多个外部 IP 地址,但通过不同的端口区分不同的连接。它是 NAT 中最常见的类型,也称为NAT 重载。
和 NAT 相关的最重要的规则,都在 nat table 里。在相应chain中配置所需的规则,即可实现NAT的功能
iptables可以配置和管理 Netfilter 框架,定义网络数据包过滤规则
iptables [-t table] command [match pattern] [action]
在指定好table和chain之后,需要对匹配模式进行指定
如
iptables -t nat -A POSTROUTING -p tcp -s 192.168.1.2 [...]
iptables -t nat -A POSTROUTING -p udp -d 192.168.1.2 [...]
iptables -t nat -A PREROUTING -s 192.168.0.0/16 -i eth0 [...]
至此,我们已经可以指定匹配模式来过滤包了,接下来就是选择合适的动作,对于 nat table,有如下几种动作SNAT, MASQUERADE, DNAT, REDIRECT,都需要通过 -j 指定
iptables [...] -j SNAT --to-source 123.123.123.123
iptables [...] -j MASQUERADE
iptables [...] -j DNAT --to-destination 123.123.123.123:22
iptables [...] -j REDIRECT --to-ports 8080
SNAT:修改源 IP 为固定新 IP,SNAT 只对离开路由器的包有意义,因此它只用在 POSTROUTING chain 中
MASQUERADE:修改源 IP 为动态新 IP,和 SNAT
类似,但是对每个包都会动态获取指定输出接口(网卡)的 IP
DNAT : 修改目的 IP
REDIRECT : 将包重定向到本机另一个端口
我们希望实现的是:从本地网络发出的、目的是公网的包,将发送方地址修改为路由器 的地址。
接下来假设路由器的本地网络走 eth0
端口,到公网的网络走 eth1
端口。那么如下iptables命令就能完成我们期望的功能:
iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
注册钩子函数
之后进入源码查看,一个钩子函数是怎么被注册的:
nf_hook_ops是 netfilter 框架中用于定义钩子操作的结构体
struct nf_hook_ops {
nf_hookfn *hook;//指向钩子函数的指针,钩子函数将在数据包通过特定钩子时被调用
struct net_device *dev;//指向特定网络设备的指针,表示该钩子操作适用的网络接口。
void *priv;//指向用户自定义私有数据的指针,可以在钩子函数中使用,以便传递额外信息
u8 pf;//协议族,用于指定该钩子操作支持的协议
enum nf_hook_ops_type hook_ops_type:8;//钩子操作的类型,通常用于区分不同用途的钩子操作
unsigned int hooknum;//钩子的位置,指示该钩子操作在 Netfilter 钩子链中的位置
int priority;//钩子的优先级,值越小优先级越高
};
钩子函数的注册通常随着模块的加载,在模块初始化过程中通过直接或间接调用nf_register_net_hooks或nf_register_net_hook
依旧以nat为例,其hook点和相应信息被定义如下
static const struct xt_table nf_nat_ipv4_table = {
.name = "nat",
.valid_hooks = (1 << NF_INET_PRE_ROUTING) |
(1 << NF_INET_POST_ROUTING) |
(1 << NF_INET_LOCAL_OUT) |
(1 << NF_INET_LOCAL_IN),
.me = THIS_MODULE,
.af = NFPROTO_IPV4,
};
初始化 NAT 表时,调用iptable_nat_init进行注册
module_init(iptable_nat_init);
static int __init iptable_nat_init(void)
{
int ret = xt_register_template(&nf_nat_ipv4_table,
iptable_nat_table_init);
...
}
其关联到iptable_nat_table_init函数进行初始化
static int iptable_nat_table_init(struct net *net)
{
...
repl = ipt_alloc_initial_table(&nf_nat_ipv4_table);//分配一个初始的NAT表结构
...
ret = ipt_register_table(net, &nf_nat_ipv4_table, repl, NULL);//将表注册到netfilter框架中
...
ret = ipt_nat_register_lookups(net);//注册NAT查找函数
...
}
使用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 *template_ops)
{
...
ret = translate_table(net, newinfo, loc_cpu_entry, repl);//参数检查
...
new_table = xt_register_table(net, table, &bootstrap, newinfo);//注册到netfilter框架中。
...
ret = nf_register_net_hooks(net, ops, num_ops);//注册钩子操作
...
}
nf_register_net_hooks调用nf_register_net_hook函数来创建钩子函数
int nf_register_net_hook(struct net *net, const struct nf_hook_ops *reg)
{
int err;
if (reg->pf == NFPROTO_INET) {//NFPROTO_INET协议族
if (reg->hooknum == NF_INET_INGRESS) {//chain为INGRESS
err = __nf_register_net_hook(net, NFPROTO_INET, reg);
if (err < 0)
return err;
} else {
err = __nf_register_net_hook(net, NFPROTO_IPV4, reg);//注册ipv4钩子
if (err < 0)
return err;
err = __nf_register_net_hook(net, NFPROTO_IPV6, reg);//注册ipv6钩子
...
}
} else {
err = __nf_register_net_hook(net, reg->pf, reg);//注册钩子
if (err < 0)
return err;
}
return 0;
}
以ipv4钩子为例,进入__nf_register_net_hook函数
static int __nf_register_net_hook(struct net *net, int pf,
const struct nf_hook_ops *reg)
{
struct nf_hook_entries *p, *new_hooks;
struct nf_hook_entries __rcu **pp;
int err;
switch (pf) {
case NFPROTO_NETDEV:
...
case NFPROTO_INET:
if (reg->hooknum != NF_INET_INGRESS)
break;
err = nf_ingress_check(net, reg, NF_INET_INGRESS);//检查
if (err < 0)
return err;
break;
}
pp = nf_hook_entry_head(net, pf, reg->hooknum, reg->dev);//获取指定钩子的头部指针
...
p = nf_entry_dereference(*pp);
new_hooks = nf_hook_entries_grow(p, reg);//扩展钩子条目,以容纳新注册的钩子。
...
nf_static_key_inc(reg, pf);//增加钩子的计数
...
return 0;
}
其核心在nf_hook_entries_grow函数
static struct nf_hook_entries *
nf_hook_entries_grow(const struct nf_hook_entries *old,
const struct nf_hook_ops *reg)
{
...
new = allocate_hook_entries_size(alloc_entries);//分配新的钩子条目空间
new_ops = nf_hook_entries_get_hook_ops(new);// 获取新分配的钩子的操作指针
//插入新钩子
while (i < old_entries) {//遍历旧钩子
//比较优先级
//如果新的钩子优先级低,将原始钩子保留在新列表
if (inserted || reg->priority > orig_ops[i]->priority) {
new_ops[nhooks] = (void *)orig_ops[i];
new->hooks[nhooks] = old->hooks[i];
i++;
} else {//如果新的钩子优先级高,插入新钩子
new_ops[nhooks] = (void *)reg;
new->hooks[nhooks].hook = reg->hook;
new->hooks[nhooks].priv = reg->priv;
inserted = true;
}
nhooks++;
}
//未找到 即新插入的钩子优先级最低
if (!inserted) {//在末尾插入
new_ops[nhooks] = (void *)reg;
new->hooks[nhooks].hook = reg->hook;
new->hooks[nhooks].priv = reg->priv;
}
return new;
}
至此,将相应的的钩子插入到netfilter框架之中。
iptables调用逻辑
iptables的具体实现逻辑如下图所示
在 iptables
中,从用户态添加一个规则到内核态的流程如下:
用户态调用:
用户通过 iptables 命令行工具调用添加规则的命令: 该命令被解析,并调用相应的库函数(通常是 libiptc
中的函数)。
准备规则:
在用户空间,命令行工具会构造规则结构体,并将规则信息填充到这个结构体中。
调用 libiptc:
用户态工具调用 libiptc
中的函数,例如iptc_append_entry
,来将规则添加到指定的链中。libiptc
会使用套接字(通常是原始套接字)与内核通信。
套接字通信:
libiptc
使用socket
系统调用创建一个原始套接字,并使用getsockopt
或setsockopt
来发送规则。具体而言,会调用 TC_INIT
来初始化句柄,并通过getsockopt
获取当前规则的信息。
进入内核态:
当调用 setsockopt
发送规则时,控制权转移到内核态。内核中的netfilter
处理逻辑会处理这个请求。
处理规则:
内核中的 netfilter
接收这个请求,并将规则添加到相应的链中。具体实现位于net/netfilter
目录下。内核会更新相应的数据结构(如 ip_tables
)。
返回结果:
添加规则后,内核会将结果返回给用户态程序,通常通过返回值或设置 errno
的方式。
用户态接收结果:
用户态工具接收内核的返回结果,并打印成功或错误信息给用户。
具体流程再次不进行叙述,主要核心逻辑在于xt_replace_table
struct xt_table_info *xt_replace_table(struct xt_table *table,
unsigned int num_counters,
struct xt_table_info *newinfo,
int *error)
{
struct xt_table_info *private;
...
//将旧表的初始条目复制到新信息中
newinfo->initial_entries = private->initial_entries;
table->private = newinfo;
...
}
参考博文:A Deep Dive into Iptables and Netfilter Architecture | DigitalOcean
对应译文:[译] 深入理解 iptables 和 netfilter 架构 (arthurchiao.art)
参考博文:NAT with Linux and iptables - Tutorial (Introduction) (karlrupp.net)
对应译文:[译] NAT - 网络地址转换(2016) (arthurchiao.art)
参考博文:来,今天飞哥带你理解 iptables 原理!-腾讯云开发者社区-腾讯云 (tencent.com)
源码环境为6.2.0