于Linux-2.6.32内核上编译ipset-6.23的坎坷经历

时间:2022-05-19 02:24:11

新版本的ipset

上周在儿童医院给小小看病等待叫号的间隙,收到了Netfilter邮件列表的推送消息,一览了ipset最新的6.23版本的新特性,很多正是我目前所需要的,特别是timeout和skbinfo参数的支持,具体的详情请自行查看manual,如果不想看那么多,我这里简单的贴一下:

   timeout
       All  set  types  supports the optional timeout parameter when creating a set and adding entries. The value of the timeout
       parameter for the create command means the default timeout value (in seconds) for new entries. If a set is  created  with
       timeout support, then the same timeout option can be used to specify non-default timeout values when adding entries. Zero
       timeout value means the entry is added permanent to the set.  The timeout value of already added elements can be  changed
       by readding the element using the -exist option. Example:

              ipset create test hash:ip timeout 300
              ipset add test 192.168.0.1 timeout 60
              ipset -exist add test 192.168.0.1 timeout 600
...
   skbinfo, skbmark, skbprio, skbqueue
       All set types support the optional skbinfo extension. This extension allow to store the metainfo (firewall mark, tc class
       and  hardware queue) with every entry and map it to packets by usage of SET netfilter target with --map-set option.  skb┅\
       mark option format: MARK or MARK/MASK, where MARK and MASK are 32bit hex numbers with 0x prefix. If only mark  is  speci‐
       fied  mask  0xffffffff  are used.  skbprio option has tc class format: MAJOR:MINOR, where major and minor numbers are hex
       without 0x prefix.  skbqueue option is just decimal number.

              ipset create foo hash:ip skbinfo
              ipset add foo skbmark 0x1111/0xff00ffff skbprio 1:10 skbqueue 10
...
   nomatch
       The  hash  set  types  which can store net type of data (i.e. hash:*net*) support the optional nomatch option when adding
       entries. When matching elements in the set, entries marked as nomatch are skipped as if those were not added to the  set,
       which makes possible to build up sets with exceptions. See the example at hash type hash:net below.

       When  elements  are  tested  by ipset, the nomatch flags are taken into account. If one wants to test the existence of an
       element marked with nomatch in a set, then the flag must be specified too.
...

编译

总之,相比较老版本的4.5,确实增加了不少新的东西,于是就迫不及待地下载,编译,试用,一般而言,这些步骤都是例行的,都不会遇到什么特别大的困难,特别是看了其README之后:

0. You need the source tree of your kernel (version >= 2.6.32)
   and it have to be configured with ip6tables support enabled,
   modules compiled. For kernel versions < 2.6.39 please apply
   the netlink.patch against your kernel tree, which adds the
   new subsystem identifier for ipset.
而我的内核就是2.6.32版本的,虽然比较老了,但没有办法。不过既然明说了支持2.6.32,那就放心了,除了README之外,其网站上也明确说明支持2.6.32内核:
For the new branch
    linux kernel source code (version >= 2.6.32)
    source of ipset: ipset-6.23.tar.bz2 (md5sum)
于是开始例行的工作:
tar xjvf ipset-6.23.tar.bz2
cd ipset-6.23
./configure
报告说没有为内核打netlink.patch,不过这不是什么事儿,但是在这个点上,我提出了质疑:
质疑:明明只是更新了头文件,为何非要给内核源码树打patch啊?
我感到这个动作不太合乎常规,为了不依赖源码树,我将这个补丁打到了编译所需的2.6.32的内核头文件中,于是configure顺利通过,但是遇到了另一个问题,提示我没有安装libmnl,由于我这是一个干净的环境,所以在Netfilter网站下载了次新版的libmnl源码,例行安装libmnl。这一步对于我而言并非必须的,像我这样天天折腾Netfilter的,怎么可能不安装libmnl啊...
       接来下就是make了,顺利通过,然后make modules,报错一大堆,然后我感到,这个ipset-6.23根本TMD就不支持2.6.32,所有的文档说明都是TMD在胡扯!报错的是下面这个文件:
ipset-6.23/kernel/net/netfilter/xt_set.c
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c: In function 'set_match_v0_checkentry':
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c:99: error: 'const struct xt_mtchk_param' has no member named 'net'
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c:108: error: 'const struct xt_mtchk_param' has no member named 'net'
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c: In function 'set_match_v0_destroy':
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c:123: error: 'const struct xt_mtdtor_param' has no member named 'net'
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c: In function 'set_match_v1_checkentry':
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c:148: error: 'const struct xt_mtchk_param' has no member named 'net'
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c:157: error: 'const struct xt_mtchk_param' has no member named 'net'
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c: In function 'set_match_v1_destroy':
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c:169: error: 'const struct xt_mtdtor_param' has no member named 'net'
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c: In function 'set_target_v0':
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c:231: error: 'const struct xt_match_param' has no member named 'targinfo'
/usr/src/ipset-6.23/kernel/net/netfilter/xt_set.c: In function 'set_target_v0_checkentry':
我在这个源文件中发现了下面的宏定义:
#ifdef HAVE_CHECKENTRY_BOOL
#define CHECK_OK    1
#define CHECK_FAIL(err) 0
#define CONST       const
#define FTYPE       bool
#define XT_PAR_NET(par) NULL
#else /* LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,35) */
#define CHECK_OK    0
#define CHECK_FAIL(err) (err)
#define CONST
#define FTYPE       int
#define XT_PAR_NET(par) (par)->net
#endif
很显然,根据注释,我应该定义HAVE_CHECKENTRY_BOOL,但是这个宏的定义应该自动化才合理,完全不应该去手工干预,在configure文件中,发现了下面的定义语句:
if test -f $ksourcedir/net/netfilter/xt_state.c && \
    $GREP -q 'bool state_mt_check' $ksourcedir/net/netfilter/xt_state.c; then
    { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
$as_echo "yes" >&6; }
    HAVE_CHECKENTRY_BOOL=define
else
    { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
$as_echo "no" >&6; }
    HAVE_CHECKENTRY_BOOL=undef
fi
注意 $ksourcedir/net/netfilter/xt_state.c这个万恶的语句,我顿时火冒三丈,指定源码文件就是为了在这个文件中寻找match check回调函数的返回值类型,作者难道不能通过内核的版本号还区分吗?难道对于一个固定的内核版本,match check的返回值规范不是固定的吗?对于2.6.32内核,下面的调用难道可以改变吗?:
if (par->target->checkentry != NULL && !par->target->checkentry(par))
    return -EINVAL;
我很生气,但我不能强求开源软件一定怎么怎么样,就像比尔.盖茨曾经说的那样,得不到报酬的程序员是写不出一流的软件的,这一点我稍微有点信了。好吧,我指定一定源码文件,让configure的过程去定义那个万恶的HAVE_CHECKENTRY_BOOL宏,值得注意的是,除了那个宏之外,起到相同的旨在不同内核版本间适配作用的还有一个宏:HAVE_XT_TARGET_PARAM,它的作用是在没有定义xt_action_param结构体的低版本内核中将其定义为xt_target_param,在必要的时候强转成xt_match_param。定义了这两个宏之后,xt_set编译通过,但是ipset内核模块本身却报错了,而这个ipset内核模块本身是要比xt_set更重要的,要知道xt_set只是一个和iptables联动时所用的模块,即便真的无法适配,自己写一个应该也不难,然而对于ipset本身的内核模块,如果要自己写,那就相当于自己实现ipset-6.23本身了...还好,这次的新错误不多:
  CC [M]  /usr/src/ipset-6.23/kernel/net/netfilter/ipset/ip_set_core.o
/usr/src/ipset-6.23/kernel/net/netfilter/ipset/ip_set_core.c: In function 'call_ad':
/usr/src/ipset-6.23/kernel/net/netfilter/ipset/ip_set_core.c:1444: error: 'SIZE_MAX' undeclared (first use in this function)
/usr/src/ipset-6.23/kernel/net/netfilter/ipset/ip_set_core.c:1444: error: (Each undeclared identifier is reported only once
/usr/src/ipset-6.23/kernel/net/netfilter/ipset/ip_set_core.c:1444: error: for each function it appears in.)
/usr/src/ipset-6.23/kernel/net/netfilter/ipset/ip_set_core.c:1444: warning: type defaults to 'int' in declaration of '_min1'
/usr/src/ipset-6.23/kernel/net/netfilter/ipset/ip_set_core.c:1444: warning: comparison of distinct pointer types lacks a cast
make[4]: *** [/usr/src/ipset-6.23/kernel/net/netfilter/ipset/ip_set_core.o] 错误 1
说的是SIZE_MAX没有定义,这个常量在3.5以上的高版本内核上才有,2.6.32如果不手工定义的话根据不能编译通过,虽然手工定义一下并不难,但是越发觉得ipset-6.23的编译文档和支持版本的说明文档就是在不负责任地胡说八道,简直就是在扯淡!!我找到了另外 一篇文档,上面明确了一个多少还能说得过去的事实:
2 Supported Configurations
* iptables >= 1.4.3
* kernel-source >= 2.6.29
For ipset-6 you need:
* libmnl
* Linux kernel >= 2.6.35
起码和ipset-6.23源码中的注释能对得上,除此之外也是在胡扯。
       这个事实太恶心人了啊!我定义了SIZE_MAX之后,编译就可以顺利通过了,虽然有一些类型转换的警告,但是忽略它们并不会有什么大不了的后果。于是我觉得为了让ipset-6.23自带的文档更好地服务大众,有必要做一个真正的patch,事实上我确实这么做了,我想做的事情有以下的目标:
1.编译ipset-6.23仅仅依赖内核头文件而不再依赖源码;
2.依赖内核版本而适配数据结构和宏定义而不是在内核源码中找特征值。

由于我的目标仅仅是在2.6.32内核上编译成功,因为我并没有测试2.6.33/34/35/36以及3.0/1/2/3/4/5/6/7/8...但我相信,3.5以上的内核版本上是一定可以成功编译的。ipset-6.23的打包者可能根本就没有在低版本比如2.6.32内核上进行测试,这个工作以及紧随其后的修正工作也许本来就是留给我这样的人的,再者说,2.6.32这个版本也许使用的人本来就不多,不提供全访问的支持也是理所当然。我并没有怪作者和打包者的意思,也许在修正过程中确实有些冲动,所以在此澄清。
       这让人想起了时尚这个名词,这是在19世纪末粗放的工业化达到顶峰时诞生的一个词,人们普遍认为,进步是必然的,最新的就是最好的,变化的向前的,速度是加快的,保持时尚的方式就是站在潮流最前端。这个理念被普遍信奉和传承,一直到Linux 3.X内核时代...如果你还在用2.6.9内核,并且发现了它的一个大bug,没人会理你的,站在潮流最前端的冲浪者会说:世界在进步,为何不试试3.17版本的内核呢?!
       直接进入ipset-6.23目录,执行patch -p1 < ../ipset-6.23.patch即可,而ipset-6.23.patch的内容如下:
diff -Nur ipset-6.23/kernel/include/linux/netfilter/ipset/ip_set.h ipset-6.23.new/kernel/include/linux/netfilter/ipset/ip_set.h
--- ipset-6.23/kernel/include/linux/netfilter/ipset/ip_set.h    2014-09-23 19:18:34.000000000 +0800
+++ ipset-6.23.new/kernel/include/linux/netfilter/ipset/ip_set.h        2014-11-13 16:27:15.000000000 +0800
@@ -26,6 +26,9 @@
 #define IP_SET_MODULE_DESC(a, b, c)            \
        _IP_SET_MODULE_DESC(a, __stringify(b), __stringify(c))
 
+#if LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,35)
+#define SIZE_MAX       (~(size_t)0)
+#endif
 /* Set features */
 enum ip_set_feature {
        IPSET_TYPE_IP_FLAG = 0,
diff -Nur ipset-6.23/kernel/net/netfilter/xt_set.c ipset-6.23.new/kernel/net/netfilter/xt_set.c
--- ipset-6.23/kernel/net/netfilter/xt_set.c    2014-09-23 19:18:34.000000000 +0800
+++ ipset-6.23.new/kernel/net/netfilter/xt_set.c        2014-11-13 16:26:50.000000000 +0800
@@ -28,12 +28,18 @@
 MODULE_ALIAS("ipt_SET");
 MODULE_ALIAS("ip6t_SET");
 
-#ifdef HAVE_CHECKENTRY_BOOL
+#if LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,35)
 #define CHECK_OK       1
 #define CHECK_FAIL(err)        0
 #define        CONST           const
 #define FTYPE          bool
+/* Only confirm version 2.6.32 :) */
+#if LINUX_VERSION_CODE == KERNEL_VERSION(2,6,32)
+/* netns is not supported completly */
+#define        XT_PAR_NET(par) (&init_net)
+#else
 #define        XT_PAR_NET(par) NULL
+#endif
 #else /* LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,35) */
 #define CHECK_OK       0
 #define CHECK_FAIL(err)        (err)
@@ -217,7 +223,7 @@
 
 /* Revision 0 interface: backward compatible with netfilter/iptables */
 
-#ifdef HAVE_XT_TARGET_PARAM
+#if LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,35)
 #undef xt_action_param
 #define xt_action_param        xt_target_param
 #define CAST_TO_MATCH  (const struct xt_match_param *)

打上这个patch,在2.6.32内核上便可以直接编译ipset-6.23软件包了。

编译后记:

1.关于命名空间

ipset-6.23的HAVE_CHECKENTRY_BOOL宏将XT_PAR_NET定义成了NULL,而XT_PAR_NET这个宏取出的是net结构体:
struct net {
    ...
    struct net_generic    *gen;
}
值得注意的是gen字段:
struct net_generic {
    unsigned int len;
    struct rcu_head rcu;

    void *ptr[0];
};
我们看一下它的注释:
/*
 * Generic net pointers are to be used by modules to put some private
 * stuff on the struct net without explicit struct net modification
 *
 * The rules are simple:
 * 1. register the ops with register_pernet_gen_device to get the id
 *    of your private pointer;
 * 2. call net_assign_generic() to put the private data on the struct
 *    net (most preferably this should be done in the ->init callback
 *    of the ops registered);
 * 3. do not change this pointer while the net is alive;
 * 4. do not try to have any private reference on the net_generic object.
 *
 * After accomplishing all of the above, the private pointer can be
 * accessed with the net_generic() call.
 */

ipset就是将自己的数据放在了gen字段里面,如果net由于HAVE_CHECKENTRY_BOOL宏置为NULL了,那么皮之不存,毛将焉附?即便编译通过,能用吗?事实上,由于在取值的时候,并没有判断net是否为空:
static inline struct ip_set_net *ip_set_pernet(struct net *net)
{
    return net_generic(net, ip_set_net_id);
}
这样就会导致panic崩溃。因此这个原始的代码根本就不可能在HAVE_CHECKENTRY_BOOL宏被定义了的时候使用。因为net为空,如果判断了,内核不会崩溃,但是却取不到任何数据,如果没有判断,内核就会崩溃。因此这个代码本身可以说是错误的!
       我们知道,2.6.32内核可能对net命名空间支持的还不够完善,但是不管怎样,init_net是存在的,因此我将XT_PAR_NET定义成了&init_net。

2.我是不是该提交一个patch

我怕被骂,而且我也不想骂人,所以这件事交给远方的朋友去做了。在我看来,很多犯错误的人都是执迷不悟的,轻轻说一句就可以展开骂战。只会编程的人是惹不起的。别的不说,反正我是看着README操作的,上面写了>=2.6.32的都可以,然而我就是没法编译,confiure里面的办法真的很恶心。反正就是写的不对!!!照着做就是TMD不行!!!