libipset源码分析

由于语言需求,需要使用go实现libipset的功能,与内核通信,就简单的看了一下源码(•̀⌄•́)
                              —— By Jihan


ipset官网
libipset手册
libmnl

本文主要根据ipset 7.x版本来进行介绍的。

简单介绍

简介

ipset是 Linux 防火墙 iptables 的一个协助工具。 通过这个工具可以轻松愉快地屏蔽一组IP地址。–来自wiki
ipset主要解决的是iptables在屏蔽大量ip产生的效率低下问题。搞一张图看看效率差距:
iptables和ipset性能对比
测评来源
并且,iptables在进行规则插入和删除的时候,也只能一条条的进行,速度也是非常慢的。我自己在自己的设备上测试效果如下(time + 脚本测试的):

添加Iptables数量 1 500 1000 2000 4000 10000
花费时间 0.002s 2.831s 4.115s 10.725s 33.365s 2m55.954s

那么,如果你有需求使用iptables屏蔽大量的ip,就可以考虑使用ipset。

使用

看源码之前,首先需要了解ipset有什么用,简单给个示例来屏蔽一个ip:
你要有两台机器,可以是自己的虚拟机,在一台机器上配置ipset + iptables,另外一台机器去ping测试。
安装(centos):yum install ipset

1
2
3
ipset create hash_test hash:ip #创建一个集合
ipset add hash_test 192.168.1.120 #添加一个成员到集合
iptables -I INPUT -m set --match-set hash_test src -j DROP #iptables配置匹配项

你也可以进行更多的尝试,参见官网
ipset的使用不是本文主要目的,简单给个示例就行了。

基本流程

libipset属于用户态部分的代码,负责与内核通信,真正的ipset工作的地方是在内核的netfilter中。
我们就根据上面ipset create hash_test hash:ip命令来分析大概的流程。
libipset流程图
特殊说明在流程图里都有备注,顺便说明一下几个固定列表的位置:

  • 各种类型的类型列表,包含所支持的命令,以及需要的参数:ipset_<type_name>.c
    haship
  • 错误码:errcode.c
    errcode
  • 输入命令参数列表(主要是-s这种类型的参数):ipset.c
    ipset
  • 输入命令参数列表(主要是add这种类型的参数):args.c
    args
  • 某个命令的消息协议:PROTOCOL
    protocol

消息格式

ipset使用的消息是Netlink通信,在ipset用户态构造的sock参数:AF_NETLINK,SOCK_RAW,NETLINK_NETFILTER,sock具体用法自己去查。ipset用户态构造的sock需要进行bind才能使用,因为它不能像udp这种自动分配发送端的端口。
首先来看看netlink的消息格式(主要参考):
struct sockaddr_nl结构:

1
2
3
4
5
6
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* AF_NETLINK (跟AF_INET对应)*/
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* port ID (通信端口号)*/
__u32 nl_groups; /* multicast groups mask */
};

struct nlmsghd 结构:

1
2
3
4
5
6
7
8
/* struct nlmsghd 是netlink消息头*/
struct nlmsghdr {
__u32 nlmsg_len; /* Length of message including header */
__u16 nlmsg_type; /* Message content */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process port ID */
};

通常这个头消息的构造,在ipset中是下面的代码(其中seq是一个自增检验数,pid通常设置0):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void
ipset_mnl_fill_hdr(struct ipset_handle *handle, enum ipset_cmd cmd,
void *buffer, size_t len UNUSED, uint8_t envflags)
{
struct nlmsghdr *nlh;
struct nfgenmsg *nfg;

assert(handle);
assert(buffer);
assert(cmd > IPSET_CMD_NONE && cmd < IPSET_MSG_MAX);

nlh = mnl_nlmsg_put_header(buffer);
nlh->nlmsg_type = cmd | (NFNL_SUBSYS_IPSET << 8);
nlh->nlmsg_flags = cmdflags[cmd - 1];
if (envflags & IPSET_ENV_EXIST)
nlh->nlmsg_flags &= ~NLM_F_EXCL;

//这儿是扩展头,通常是固定的\x02\x00\x00\x00
nfg = mnl_nlmsg_put_extra_header(nlh, sizeof(struct nfgenmsg));
nfg->nfgen_family = AF_INET;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(0);
}

struct msghdr 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct iovec {                    /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
/* iov_base: iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,
以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度 (即有多少个buff)
*/
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
/* msg_name: 数据的目的地址,网络包指向sockaddr_in, netlink则指向sockaddr_nl;
msg_namelen: msg_name 所代表的地址长度
msg_iov: 指向的是缓冲区数组
msg_iovlen: 缓冲区数组长度
msg_control: 辅助数据,控制信息(发送任何的控制信息)
msg_controllen: 辅助信息长度
msg_flags: 消息标识
*/

这个结构体主要在ipset接收消息的时候使用。具体代码包含在libmnl库中,需要下载源码。然后就可以看到以下的接收消息的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ssize_t mnl_socket_recvfrom(const struct mnl_socket *nl, void *buf,
size_t bufsiz)
{
ssize_t ret;
struct sockaddr_nl addr;
struct iovec iov = {
.iov_base = buf,
.iov_len = bufsiz,
};
struct msghdr msg = {
.msg_name = &addr,
.msg_namelen = sizeof(struct sockaddr_nl),
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = NULL,
.msg_controllen = 0,
.msg_flags = 0,
};
ret = recvmsg(nl->fd, &msg, 0);
if (ret == -1)
return ret;

if (msg.msg_flags & MSG_TRUNC) {
errno = ENOSPC;
return -1;
}
if (msg.msg_namelen != sizeof(struct sockaddr_nl)) {
errno = EINVAL;
return -1;
}
return ret;
}

上述的netlink消息数据结构,也就是ipset里使用的主要结构,而这些消息结构的关系如下:
netlink数据结构之间的关系
图片来源
上述示例的就是ipset中使用到的主要消息结构了,更多细节,还是在源码中查看。

示例Demo

这里我们列出两个语言的demo,C和Go的。

C

c的相对简单,因为只需要调用libipset提供的接口就行。这里推荐写法和官方的ipset的main函数写法一致。
官方main函数(这个函数只有在ipset 7版本中才有)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <assert.h>			/* assert */
#include <stdio.h> /* fprintf */
#include <stdlib.h> /* exit */

#include <config.h>
#include <libipset/ipset.h> /* ipset library */

int
main(int argc, char *argv[])
{
struct ipset *ipset;
int ret;

/* Load set types */
ipset_load_types();

/* Initialize ipset library */
ipset = ipset_init();
if (ipset == NULL) {
fprintf(stderr, "Cannot initialize ipset, aborting.");
exit(1);
}

ret = ipset_parse_argv(ipset, argc, argv);

ipset_fini(ipset);

return ret;
}

是不是简单到爆。
第二种,是使用libipset中的session结构体(参考来源):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <libipset/types.h>
#include <libipset/session.h>
#include <libipset/data.h>
#include <netinet/in.h>
#include <arpa/inet.h>

/* Setname X which can be used in "ipset list X". */
#define SETNAME_IPV4 "ipset-ipv4"
#define SETNAME_IPV6 "ipset-ipv6"

struct ipset_state {
struct ipset_session *session;
};

struct address {
int family;
union {
struct in_addr ip4_addr;
struct in6_addr ip6_addr;
};
};

static bool try_ipset_cmd(struct ipset_session *session, enum ipset_cmd cmd,
const char *setname, int family, const void *addr)
{
ipset_session_data_set(session, IPSET_SETNAME, setname);
if (!ipset_type_get(session, cmd)) {
fprintf(stderr, "Cannot find ipset %s: %s\n", setname,
ipset_session_error(session));
return false;
}
ipset_session_data_set(session, IPSET_OPT_FAMILY, &family);
ipset_session_data_set(session, IPSET_OPT_IP, addr);

if (ipset_cmd(session, cmd, /*lineno*/ 0)) {
fprintf(stderr, "Failed to add to set %s: %s\n", setname,
ipset_session_error(session));
return false;
}
return true;
}

static bool try_ipset_create(struct ipset_session *session, const char *setname,
const char *typename, int family)
{
const struct ipset_type *type;
uint32_t timeout;

ipset_session_data_set(session, IPSET_SETNAME, setname);
ipset_session_data_set(session, IPSET_OPT_TYPENAME, typename);
type = ipset_type_get(session, IPSET_CMD_CREATE);
if (type == NULL) {
fprintf(stderr, "Cannot find ipset type %s: %s\n", typename,
ipset_session_error(session));
return false;
}

timeout = 0; /* timeout support, but default to infinity */
ipset_session_data_set(session, IPSET_OPT_TIMEOUT, &timeout);
ipset_session_data_set(session, IPSET_OPT_TYPE, type);
ipset_session_data_set(session, IPSET_OPT_FAMILY, &family);

if (ipset_cmd(session, IPSET_CMD_CREATE, /*lineno*/ 0)) {
fprintf(stderr, "Failed to create ipset %s: %s\n", setname,
ipset_session_error(session));
return false;
}
return true;
}

struct ipset_state *ipset_init(void)
{
struct ipset_state *state;

state = malloc(sizeof(*state));
if (!state)
return NULL;

ipset_load_types();

state->session = ipset_session_init(printf);
if (!state->session) {
fprintf(stderr, "Cannot initialize ipset session.\n");
goto err_session;
}

/* Return success on attempts to create a compatible ipset or attempts to
* * add an existing rule. */
ipset_envopt_parse(state->session, IPSET_ENV_EXIST, NULL);

if (!try_ipset_create(state->session, SETNAME_IPV4, "hash:ip", NFPROTO_IPV4))
goto err_set;
if (!try_ipset_create(state->session, SETNAME_IPV6, "hash:ip", NFPROTO_IPV6))
goto err_set;

return state;

err_set:
err_session:
ipset_session_fini(state->session);
free(state);
return NULL;
}

void ipset_add_ip(struct ipset_state *state, struct address *addr)
{
struct ipset_session *session = state->session;

switch (addr->family) {
case AF_INET:
try_ipset_cmd(session, IPSET_CMD_ADD, SETNAME_IPV4, NFPROTO_IPV4, &addr->ip4_addr);
break;
case AF_INET6:
try_ipset_cmd(session, IPSET_CMD_ADD, SETNAME_IPV6, NFPROTO_IPV6, &addr->ip6_addr);
break;
default:
fprintf(stderr, "Unrecognized address family 0x%04x\n", addr->family);
return;
}
ipset_session_report_reset(session);
}

void ipset_fini(struct ipset_state *state)
{
ipset_session_fini(state->session);
free(state);
}


//for test
int main(){
struct address addr;
addr.family = AF_INET;
inet_aton("192.168.1.10", &addr.ip4_addr);
struct ipset_state *state = ipset_init();
ipset_add_ip(state, &addr);
ipset_fini(state);
return 0;
}

编译(根据各自的环境调整):

1
2
gcc -g -O2 -Wall -Werror -c ipset_test.c -o ipset_test.o
gcc -g -O2 -Wall -Werror ./ipset_test.o -o a.out -L/usr/lib64/ -lipset

GO

go有三种方式来调用ipset,第一种是用执行命令的方式,第二种是使用cgo的方式,第三种是使用netlink通信的方式。这里简单给下第二种和第三种的demo:
cgo:
ipset.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

// #cgo LDFLAGS: -L${SRCDIR}/ -Wl,-rpath,${SRCDIR}/ -lcipset -L/usr/lib64/ -lipset
// #include <stdlib.h>
// #include "cipset.h"
import (
"C"
)
import "unsafe"

func main() {
cip := C.CString("10.92.2.100")
defer C.free(unsafe.Pointer(cip))
var caddr C.struct_address
C.ip2addr(cip, &caddr)
//caddr.family = C.AF_INET
//C.inet_aton(cip, caddr.ip4_addr)
var cstate *C.struct_ipset_state = C.ipset_init()
C.ipset_add_ip(cstate, &caddr)
C.ipset_fini(cstate)
}

cipset.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef __cipset
#define __cipset

/* Setname X which can be used in "ipset list X". */
#define SETNAME_IPV4 "ipset-ipv4"
#define SETNAME_IPV6 "ipset-ipv6"

struct ipset_state;
struct address;

struct ipset_state *ipset_init(void);
void ipset_add_ip(struct ipset_state *state, struct address *addr);
void ipset_fini(struct ipset_state *state);
void ip2addr(char *ip, struct address *addr);

#endif

cipset.c 和上面的C第二种示例代码一致,这里就不占空间了。

编译执行:

1
2
gcc -g -O2 -Wall -Werror -rdynamic -fPIC -shared -o ./libcipset.so ./cipset.c  #动态库生成
go run ipset.go #执行go程序

goipset实现:
我已经初步用golang实现了ipset,详情可我的开源:https://github.com/JiHanHuang/goipset

以上,如果有什么问题,欢迎随时交流。-(¬∀¬)σ

-------------本文结束感谢您的阅读-------------