一 概述
iptables 其实不是真正的防火墙,我们可以把它理解为一个客户端代理,用户通过iptables 这个代理,将用户的安全设置执行到对应的“安全框架”中,这个安全框架才是真正的防火墙。这个框架的名称叫做netfilter 。
二 五链表(hook)
iptables 工作在用户空间中,定义规则的工具,本身并不算是防火墙。它们定义的规则,可以让在内核空间当中的netfilter来读取,并且实现让防火墙工作。而放入内核的地方必须要是特定的位置,必须是tcp/ip的协议栈经过的地方。而这个tcp/ip协议栈必须经过的地方,可以实现读取规则的地方就叫做 netfilter.(网络过滤器)
而内核在tcp/ip协议经过的地方,选取了5个位置,做了5条规则链,也叫做五个钩子(hook),分别对经过规则链的数据包做了对应的过滤,这5位置 如下图所示
· PRE_ROUTING 进入本机网口接口的数据包
· INPUT 数据包从内核流入用户空间的
· FORWARD 直接内核空间中, 从一个网络接口进来,到另一个网络接口去的
· OUTPUT 数据包从用户空间流出的
· POST_ROUTING 离开本机的网口的数据包
在这五条钩子(规则链)上,会注册很多回调函数,也叫钩子函数,对进到该钩子的数据包,会通过钩子函数处理,返回一个状态给netfilter 包括该数据包的死活。 死活的状态如下几种:
· NF_ACCEPT 继续正常传输数据报。这个返回值告诉 Netfilter:到目前为止,该数据包还是被接受的并且该数据包应当被递交到网络协议栈的下一个阶段。
· NF_DROP 丢弃该数据报,不再传输。
· NF_STOLEN 模块接管该数据报,告诉Netfilter“忘掉”该数据报。该回调函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是回调函数从Netfilter 获取了该数据包的所有权.
· NF_QUEUE 对该数据报进行排队(通常用于将数据报给用户空间的进程进行处理)
· NF_REPEAT 再次调用该回调函数,应当谨慎使用这个值,以免造成死循环。
三 四表
除了上面提到的5条链,还会有四张表。
netfilter 是liunx 操作系统核心层内部的一个数据包处理模块,它具有以下功能。
· 网络地址转换
· 数据包内容修改
· 以及数据包过滤
· 连接跟踪
而netfilter具备的以下功能,其实就是对应的每张表的功能
· filter表——过滤数据包
· Nat表——用于网络地址转换(IP、端口)
· Mangle表——修改数据包的服务类型、TTL、并且可以配置路由实现QOS
· Raw表——决定数据包是否被状态跟踪机制处理
四张表也是有优先级的 数据包进来的时候,优先级如下 raw 表 -> Mangle 表->Nat 表 ->filter 表
四 四表五链的关系
每张表可以关联多条链,而且每张表管理的链表都是唯一的,例如nat 和 raw 表各自有各自的PRE_ROUTING链表。
对于filter来讲一般只能做在3个链上:INPUT ,FORWARD ,OUTPUT
对于nat来讲一般也只能做在3个链上:PREROUTING ,OUTPUT ,POSTROUTING
而mangle则是5个链都可以做:PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING
最后下图可直观反应数据进来时,4表,5链间的关系
首先一条数据进来,会先进到PREROUTING 这条链(hook) ,而raw , mangle , nat 表都有自己的管理的PREROUTING 链的规则,(是自己的链,不是共用),然后按照表的优先级,先匹配raw 表中PREROUTING 链的规则,再是mangle,nat 。在PREROUTING 链 中可能会有个多个钩子函数,这些钩子函数也会按照优先级对数据做相应的处理。每个钩子函数会返回一个状态,通知netfliter 这条数据钩子函数处理后是继续往后面的链传,还是DROP等到操作。既之前提到的返回的几种死活状态
四表中我们常用的就只有filter和nat,所以再来一张鸟哥版精简图:
到这里,你其实对netfilter/iptables 防火墙如何工作的有了个大概了解。其实内核数据包处理就是对这四表五链进行操作,比如说你需要过滤进入本机的数据包,其实就是在filter 表 INPUT 添加一条过滤规则。而怎么添加规则,到内核如何将这条规则生效。是我们下面需要深入的。这里说的怎么添加规则,并不是怎么用iptables 命令添加一条规则,而是iptables 命令添加一条规则,内部是生产了一条怎么样的规则,和对这条规则的如何管理的。
五 规则(iptables 命令)
在说明iptables 源码之前,你必须知道iptables 创建的规则,一条规则包含哪些东西
规则:根据指定的匹配条件来尝试匹配每个流经此处的报文,一旦匹配成功,则由规则后面指定的动作处理。
而一条规则 由 通用匹配+扩展匹配+动作。对应iptables 命令如下
1. 通用匹配
源地址目标地址的匹配
-s:指定作为源地址匹配,这里不能指定主机名称,必须是IP
IP | IP/MASK | 0.0.0.0/0.0.0.0
而且地址可以取反,加一个“!”表示除了哪个IP之外
-d:表示匹配目标地址
-p:用于匹配协议的(这里的协议通常有3种,TCP/UDP/ICMP)
-i eth0:从这块网卡流入的数据
流入一般用在INPUT和PREROUTING上
-o eth0:从这块网卡流出的数据
流出一般在OUTPUT和POSTROUTING上
2. 扩展匹配
2.1 隐含扩展
对协议的扩展
-p tcp :TCP协议的扩展。一般有三种扩展
--dport XX-XX:指定目标端口,不能指定多个非连续端口,只能指定单个端口,比如
--dport 21 或者 --dport 21-23 (此时表示21,22,23)
--sport:指定源端口
--tcpflags:TCP的标志位(SYN,ACK,FIN,PSH,RST,URG)
对于它,一般要跟两个参数:
1.检查的标志位
2.必须为1的标志位
--tcpflags syn,ack,fin,rst syn = --syn
表示检查这4个位,这4个位中syn必须为1,其他的必须为0。所以这个意思就是用于检测三次握手的第一次包的。对于这种专门匹配第一包的SYN为1的包,还有一种简写方式,叫做--syn
-p udp:UDP协议的扩展
--dport
--sport
-p icmp:icmp数据报文的扩展
--icmp-type:
echo-request(请求回显),一般用8 来表示
所以 --icmp-type 8 匹配请求回显数据包
echo-reply (响应的数据包)一般用0来表示
2.2 显示扩展
扩展各种模块(-m)
-m multiport:表示启用多端口扩展
之后我们就可以启用比如 --dports 21,23,80
3.处理动作
-j ACTION
常用的ACTION:
DROP:悄悄丢弃
一般我们多用DROP来隐藏我们的身份,以及隐藏我们的链表
REJECT:明示拒绝
ACCEPT:接受
custom_chain:转向一个自定义的链
DNAT
SNAT
MASQUERADE:源地址伪装
REDIRECT:重定向:主要用于实现端口重定向
MARK:打防火墙标记的
RETURN:返回
六 规则(iptables 源码内部)
而在iptables/netfilter 内部实现中也是将一条规则,分成三个部分组装成的。
· entry:规则的入口,同时做一些匹配数据包的工作。通用的匹配在entry里匹配
· match:匹配数据包的条件大多放在这里。
· target:对于符合条件的数据包要执行的动作放在这里。
对应下图中三个结构体,其中match可以有多条匹配条件
从上图分析,可以发现一条rule 可以由一个ipt_entry ,多个match 模块,最后再加一个target模块组成。这张图特别重要,后面分析代码时怎么组装规则就是按照上图结构组成出的
七 iptables源码分析
知道了规则概念,接下来来分析iptables源码就能更好的理解了。
版本 iptables 1.3.5 iptables 源码没有特别深入,只是对里面几个重要的地方进行了理解。
首先iptables 在 extensions 文件中对rules 支持的 match 匹配,和target 动作规则 进行了模块化。他们在iptables程序启动就会对这些模块初始化,从而方便后面iptables 根据命令参数更快的在规则中加入对应的match,traget 模块。
关于加载模块,这个啰嗦几句。
iptables 支持了两种加载模块方式
· 1.程序启动加载所有的模块init_extensions(),
· 2.动态加载需要的动态库
在定义了NO_SHARED_LIBS 宏变量时初始化走的第一种方式,而默认情况加载模块走的第二种,这里主要分析动态加载模块,而加载的动态库你会发现不管是match模块,target模块。每个模块中都有一个名为_init()的函数。 iptables在加载动态库时用的是dlopen()函数,就会调用_init函数,从而完成初始化。为啥dlopen()函数 打开动态库,就会调用_init 函数呢?
_init()定义在xtables.h中,其定义如下:
#if defined(ALL_INCLUSIVE) || defined(NO_SHARED_LIBS)
# ifdef _INIT
# undef _init
# define _init _INIT
# endif
extern void init_extensions(void);
#else
# define _init __attribute__((constructor)) _INIT
#endif
函数属性
__attribute__ ((constructor))会使函数在main()函数之前被执行
__attribute__ ((destructor))会使函数在main()退出后执行
加载动态库的时候,动态库以__attribute__ ((constructor))属性声明的函数也会先运行,因此在iptables中当我们调用dlopen函数来加载动态库时,率先执行每个动态库里的_init()函数,而该函数要么是将该match注册到全局链表xtables_matches里,或者是将target注册到全局链表xtables_targets中。
那么iptables 主要做了哪些功能呢?
· do_command() 进入到解析 iptables命令,对相应命令做相关处理的主函数。
· 根据参数组装对应的规则。就是上面所说的三个结构体。ipt_entry ,ipt_entry_match,ipt_entry_target。
· 对组装后的规则,让libiptc.so库操作处理,例如将规则添加iptc_append_entry ,删除iptc_delete_entry 。最终提交这条规则 iptc_commit
这么一看iptables 做的工作是不是太简单了。其实主要iptables主要工作就是在 组装规则上了。最后将组装的规则下发到内核中,内核netfilter 再根据规则,来决定是过滤包,还是转发包,等等操作。
看过iptables源码的,你会发现跟规则相关的结构体,iptables源码上用户空间,用的三个结构体分别是 ipt_entry ,iptables_match,iptables_target 这三个结构体,但是事实上
iptables_match 结构体里包含了 ipt_entry_match 结构体 ,
iptables_target 结构体里包含了 ipt_entry_target 结构体 ,
因此组装一条规则,本质就是填充ipt_entry ,ipt_entry_match, ipt_entry_target 这三个结构体。
八 自己实现规则
说好的分析源码呢? 怎么都没看到代码。其实并不打算拿iptables 源码来深入分析。对iptables 的源码,就着重看了 iptables 源码核心做了哪些事。你明白了规则的组装,自己就可以实现iptables 源码中的添加删除规则。
下面代码调用了 libiptc.so libip4tc.so 两个库,iptables 源码中就有这两个库的实现。
这两个库主要实现对规则的操作,和与内核netfilter交互。
#define TableName "nat"
#define P_TCP 1
#define P_UDP 2
#define PRE "PREROUTING"
#define POST "POSTROUTING"
#define S_NAT "SNAT"
#define D_NAT "DNAT"
#define MAX_PORT 65535
typedef struct _my_sock_addr {
unsigned int n_ip;
unsigned int n_port;
} my_addr;
int my_sock_addr(const char *ip, int port, my_addr *addr) {
if (ip == NULL || addr == NULL) {
return -1;
}
addr->n_ip = inet_addr(ip);
addr->n_port = htons(port);
return 0;
}
static int fill_entry(struct ipt_entry *e, __u32 size, __u32 match_size, __u32 src_ip, uint32_t src_msk, __u32 dst_ip, uint32_t dst_msk, __u32 protocol) {
if (e == NULL) {
printf("fill_entry_error! %x \n", *(char *) e);
return -1;
}
/*初始化entry的源地址,目的地址和掩码*/
e->ip.src.s_addr = src_ip;
e->ip.dst.s_addr = dst_ip;
if (src_msk == -1) {
e->ip.dmsk.s_addr = htonl(0xFFFFFFFF << (32 - dst_msk));
} else {
e->ip.smsk.s_addr = htonl(0xFFFFFFFF << (32 - src_msk));
}
if (protocol == P_TCP) {
e->ip.proto = IPPROTO_TCP;
} else if (protocol == P_UDP) {
e->ip.proto = IPPROTO_UDP;
}
e->target_offset = IPT_ALIGN(sizeof(struct ipt_entry)) +match_size ;
e->next_offset = size;
return 0;
}
static int fill_match(struct ipt_entry *e, struct ipt_entry_match *pm, __u32 match_size, __u16 src_port, __u16 dst_port, __u32 protocol) {
if (e == NULL || pm == NULL ) {
printf("fill_match_error! %x --- %x\n", *(char *) e, *(char *) pm);
return -1;
}
pm = (struct ipt_entry_match*) e->elems;
pm->u.user.match_size = match_size;
if (protocol == P_TCP) {
strcpy(pm->u.user.name, "tcp");
struct ipt_tcp *ptcp = (struct ipt_tcp *) pm->data;
if (src_port == 0) {
ptcp->spts[0] = src_port;
ptcp->spts[1] = MAX_PORT;
ptcp->dpts[0] = dst_port;
ptcp->dpts[1] = dst_port;
} else {
ptcp->spts[0] = src_port;
ptcp->spts[1] = src_port;
ptcp->dpts[0] = dst_port;
ptcp->dpts[1] = MAX_PORT;
}
} else if (protocol == P_UDP) {
strcpy(pm->u.user.name, "udp");
struct ipt_udp *pudp = (struct ipt_udp *) pm->data;
if (src_port == 0) {
pudp->spts[0] = src_port;
pudp->spts[1] = MAX_PORT;
pudp->dpts[0] = dst_port;
pudp->dpts[1] = dst_port;
} else {
pudp->spts[0] = src_port;
pudp->spts[1] = src_port;
pudp->dpts[0] = dst_port;
pudp->dpts[1] = MAX_PORT;
}
}
return 0;
}
static int fill_target(struct ipt_entry *e, struct ipt_entry_target *pt, struct nf_nat_multi_range_compat * p_target, const char *target, __u32 match_size,
__u32 target_size, __u32 out_ip, __u16 out_port, __u32 protocol) {
if (e == NULL || pt == NULL || p_target == NULL) {
printf("fill_target_error! %x---%x---%x\n", *(char *) e, *(char *) pt, *(char *) p_target);
return -1;
}
pt = (struct ipt_entry_target *) (e->elems + match_size);
pt->u.target_size = target_size;
// pt->target.u.kernel.target = NULL;
strncpy(pt->u.user.name, target, sizeof(pt->u.user.name) - 1);
p_target = (struct nf_nat_multi_range_compat *) pt->data;
//
p_target->rangesize = 1;
if (out_port == 0) {
p_target->range[0].flags = 1;
} else {
p_target->range[0].flags = 3;
}
p_target->range[0].min_ip = out_ip;
p_target->range[0].max_ip = out_ip;
if (protocol == P_TCP) {
p_target->range[0].min.tcp.port = out_port;
p_target->range[0].max.tcp.port = out_port;
} else if (protocol == P_UDP) {
p_target->range[0].min.udp.port = out_port;
p_target->range[0].max.udp.port = out_port;
}
return 0;
}
static int iptc_entry_add(struct iptc_handle *handle, const char *chain, const char *target, __u32 protocol, __u32 src_ip, __u16 src_port, __u32 src_msk,
__u32 dst_ip, __u16 dst_port, __u32 dst_msk, __u32 out_ip, __u16 out_port) {
if (handle == NULL || chain == NULL || target == NULL) {
printf("iptc_entry_add error! %x---%x---%x\n", *(char *) handle, *(char *) chain, *(char *) target);
return -1;
}
struct ipt_entry *e = NULL; //对应规则的通用匹配
struct ipt_entry_match pm; // match模块的匹配
struct ipt_entry_target pt; // target 模块的匹配
struct nf_nat_multi_range_compat p_target;
__u32 target_size, match_size, size;
__u32 ret = 0;
if(protocol == P_TCP){
match_size = IPT_ALIGN(sizeof(struct ipt_entry_match)) + IPT_ALIGN(sizeof(struct ipt_tcp));
}else{
match_size = IPT_ALIGN(sizeof(struct ipt_entry_match)) + IPT_ALIGN(sizeof(struct ipt_udp));
}
target_size = IPT_ALIGN(sizeof(struct ipt_entry_target))+ IPT_ALIGN(sizeof(struct nf_nat_multi_range_compat));
size = IPT_ALIGN(sizeof(struct ipt_entry)) + target_size + match_size;
e = malloc(size);
memset((void *) e, 0, size);
fill_entry(e, size, match_size, src_ip, src_msk, dst_ip, dst_msk, protocol);// 填充通用匹配规则
fill_match(e, &pm, match_size, src_port, dst_port, protocol);// 填充match 模块规则
fill_target(e, &pt, &p_target, target, match_size, target_size, out_ip, out_port, protocol);// 填充target 模块规则
/* 在规则链中插入一项 */
ret = iptc_append_entry(chain, e, handle);
if (e) {
free(e);
}
return ret;
}
int my_iptc_add(struct iptc_handle *handle, char* pro, my_addr *p_addr, my_addr *s_addr, __u32 out_ip) {
__u32 ret = 0;
__u32 protocol;
__u32 src_msk = 0;
__u32 dst_msk = 0;
printf("iptc_operate >>>>> ======================srv_addr[%x:%x],prx_addr[%x:%x],out_ip[%x]\n",
s_addr->n_ip,s_addr->n_port,p_addr->n_ip,p_addr->n_port,out_ip);
if (strcmp(pro, "tcp") == 0) {
protocol = P_TCP;
ret = iptc_entry_add(handle, PRE, D_NAT, protocol, 0, 0, -1, p_addr->n_ip, ntohs(p_addr->n_port), dst_msk, s_addr->n_ip, s_addr->n_port); // 在PREROUTING链 添加DNAT转换规则
if (ret < 0) {
return ret;
}
ret = iptc_entry_add(handle, POST, S_NAT, protocol, 0, 0, -1, s_addr->n_ip, ntohs(s_addr->n_port), dst_msk, out_ip, 0);// 在POSTROUTING链 添加SNAT转换规则
if (ret < 0) {
return ret;
}
} else if (strcmp(pro, "udp") == 0) {
protocol = P_UDP;
ret = iptc_entry_add(handle, PRE, D_NAT, protocol, 0, 0, -1, p_addr->n_ip, ntohs(p_addr->n_port), dst_msk, s_addr->n_ip, s_addr->n_port);
if (ret < 0) {
return ret;
}
ret = iptc_entry_add(handle, POST, S_NAT, protocol, 0, 0, -1, s_addr->n_ip, ntohs(s_addr->n_port), dst_msk, out_ip, 0);
if (ret < 0) {
return ret;
}
} else {
printf("input protocol error!\n");
return -1;
}
return ret;
}
static int iptc_operate(char method, my_addr *srv_addr, my_addr *prx_addr, __u32 out_ip, char *proto) {
int ret = 0;
struct iptc_handle *h;
h = iptc_init(TableName);
if (!h) {
return -1;
}
if (method == 'A') {
ret = my_iptc_add(h, proto, prx_addr, srv_addr, out_ip);
if (ret < 0) {
iptc_free(h);
return ret;
}
}
ret = iptc_commit(h);
if (ret <= 0) {
printf( "IPTC_COMMIT_ERROR [%d:%s]", ret, iptc_strerror(errno));
}
iptc_free(h);
return ret;
}
int main(int argc, char **argv){
if(argc < 6){
printf("./my_iptable porxyIp proxyPort servIp servPort outIp protocol\n");
return 0;
}
my_addr prx_addr;
my_addr srv_addr;
my_sock_addr(argv[1],atoi(argv[2]),&prx_addr);
my_sock_addr(argv[3],atoi(argv[4]),&srv_addr);
unsigned int out_ip = inet_addr(argv[5]);
iptc_operate('A',&srv_addr,&prx_addr,out_ip,argv[6]);
return 1;
}
运行代码如下图,这里的10.68.22.189 是需要创建规则主机的IP
上面代码主要功能 在nat表转发链上添加规则,实现发送到22.189的地址(proxy),给他转发到服务端22.140上(server),同时出口地址转换为本地地址(outIp)发出的,最后指定tcp,还是udp协议。
运行结果,发现nat的iptables规则生成了,如下图
验证结果:
通过网络调试助手,发现发往22.189:9000的数据包给它转到了22.140:9000端口上了,说明规则起到对应效果了。
最后代码分析
核心函数my_iptc_add() 主要功能组装规则。规则用到了三个结构体,ipt_entry ,
ipt_entry_match ,ipt_entry_target 。 这个在第六章提到过。这里详细说下这三个结构体。
struct ipt_entry {
struct ipt_ip ip; // 通用的匹配规则
/* Mark with fields that we care about. */
unsigned int nfcache;
/* Size of ipt_entry + matches */
u_int16_t target_offset; // 偏移量 指向规则target模块内容
/* Size of ipt_entry + matches + target */
u_int16_t next_offset; // 偏移量 指向下一个规则
/* Back pointer */
unsigned int comefrom;
/* Packet and byte counters. */
struct xt_counters counters; // 统计计算一条链过滤了packet 数目 ,和字节
/* The matches (if any), then the target. */
unsigned char elems[0]; // 可变数组,有match 模块,target模块可变数据内容就会包含这两模块
};
#define ipt_entry_match xt_entry_match
#define ipt_entry_target xt_entry_target
struct xt_entry_match {
union {
struct {
__u16 match_size;
/* Used by userspace */
char name[XT_FUNCTION_MAXNAMELEN-1];
__u8 revision;
} user;
struct {
__u16 match_size;
/* Used inside the kernel */
struct xt_match *match;
} kernel;
/* Total length */
__u16 match_size;
} u;
unsigned char data[0];
};
struct xt_entry_target {
union {
struct {
__u16 target_size;
/* Used by userspace */
char name[XT_FUNCTION_MAXNAMELEN-1];
__u8 revision;
} user;
struct {
__u16 target_size;
/* Used inside the kernel */
struct xt_target *target;
} kernel;
/* Total length */
__u16 target_size;
} u;
unsigned char data[0];
};
xt_entry_match ,xt_entry_target 这两结构体惊人相似,有一个联合体,一个是用户空间用的,一个是内核空间用的。都会有一个模块的size 标明这模块有多少字节。最后一个还是可变数组,里面保存对应match模块的或者target模块的规则信息。
上面代码中match模块可变数组根据udp/tcp 分别对应ipt_udp/ipt_tcp 这两个结构体
target模块因为是nat转发,对应了nf_nat_multi_range_compat 结构体。
至此规则组装完,后调用了libiptc库函数。下发到内核netfilter,iptc_commit 生效添加的规则。
最后备注个之前碰到的坑
之前32位liunx可以执行,发现放到64位后规则一直不成功,说是无效的参数,后发现在计算大小,偏移量,IPT_ALIGN这个宏定义特别重要,保证字节对齐。
本章其实对iptables1.3.5 源码只是针对的分析了下,明白了规则,我觉得也就够了,核心还是放到netfilter 内核下对规则的处理,明白内核如何实现让规则产生对应的防火墙作用,未完待续。
目前有 1 条留言 访客:0 条, 博主:0 条 ,引用: 1 条
外部的引用: 1 条
- linux下的iptables/netfilter 防火墙 深度理解 中篇 | 求索阁