背景
Web服务器Nginx采用了开源的waf项目方案强化安全性能,但是在使用过程遇到了这样一个问题,waf自带的白名单功能只支持精确的IP地址却不支持IP地址段:
1 2 3 4 |
<strong>$ vim waf/config.lua</strong> ..... ipWhitelist={"127.0.0.1","123.23.34.45"} ..... |
如下,是白名单的功能函数,我们看到只有精确的等于匹配,并没有IP地址段的匹配功能:
1 2 3 4 5 6 7 8 9 10 11 12 |
<strong>$ vim waf/init.lua</strong> function whiteip() if next(ipWhitelist) ~= nil then for _,ip in pairs(ipWhitelist) do if getClientIp()==ip then return true end end end return false end |
这样对于运维的维护来说是一个不小的成本,每次内网IP段内的web服务器IP地址有变更,都得更新这个白名单,于是决定加个简单的功能函数,给此函数添加IP地址段的识别功能。
过程
说干就干,但是前提还需要交待清楚:
- 这是本人第一次接触lua代码
- 此功能必须具有较高的效率值,因为web服务器的访问每次都会经过这个判断
- 此功能必须精确,如果判断失效,将会导致web服务器误拦截,后果严重
虽然是第一次写lua代码,但因为实现的功能很简单,所以思路也很清晰,一气呵成:
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 |
require "bit" // web服务器默认使用的是luajit,所以包含bit模块,如果是lua5.1则没有此模块,lua5.2后开始有bit32模块 function ipRangeCheck(ip ,range_mask) local ip_r1, ip_r2, ip_r3, ip_r4 = ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)") local ip_s1,ip_s2,ip_s3,ip_s4,ip_mask = range_mask:match("(%d+)%.(%d+)%.(%d+)%.(%d+)%/(%d+)") -- 1. 先将ip地址转换为整形 local ipValueNum = bit.lshift(ip_r1, 24) + bit.lshift(ip_r2, 16) + bit.lshift(ip_r3, 8) + ip_r4 -- 2. 根据ip地址段的子网掩码得到ip地址段的表示范围的最小ip地址对应的整数 local ipRangeMin = bit.lshift(ip_s1, 24) + bit.lshift(ip_s2, 16) + bit.lshift(ip_s3, 8) + ip_s4 -- 下面这一步是为了防止ip地址段的写法不规范,比如是以192.168.12.6/24来表示,而不是标准的192.168.12.0/24。 local ipRangeMin = bit.band(ipRangeMin, bit.lshift(0xFFFFFFFF, 32 - ip_mask)) -- 3. 根据ip地址段的最小值与0xFFFFFFFF右移掩码位数的结果做或操作,得到IP地址段能表示的最大地址对应的整数 local ipRangeMax = bit.bor(ipRangeMin, bit.rshift(0xFFFFFFFF, ip_mask)) print (string.format("ipValueNum = %d , ipRangeMin = %d, ipRangeMax = %d", ipValueNum , ipRangeMin , ipRangeMax)) -- 4. 判断此ip是否在最大值和最小值之间 if (ipRangeMin < ipValueNum and ipValueNum < ipRangeMax) then print(string.format("%s is in the range of %s", ip, range_mask)) else print(string.format("%s is not in the range of %s", ip, range_mask)) end end ipRangeCheck("192.168.12.254", "192.168.12.0/8") |
上面的代码思路很清晰,看起来也没什么问题,执行结果貌似也都OK,于是我沾沾自喜的开始检测一些边界值和特殊情况以避免bug,当我做出如下检测时,我发现了令我十分费解的问题:
1 2 3 4 5 6 7 8 |
$ luajit/bin/luajit test.lua 169339872 169339648 169339903 10.23.235.224 is in the range of 10.23.235.0/24 -1062663153 -1062728704 -1062728449 192.169.12.15 is not in the range of 192.168.12.0/24 |
如上,我分别检测了两组IP和IP段,发现结果不一致,从打印出来的结果来看,第二组IP转换为整形的结果竟然是负数。 机智的我转念一想,应该是字符串溢出问题,即:
我在将192.168.12.65 转换为整形的时候相当于进行了如下操作:
1 2 3 4 5 6 7 8 9 10 11 12 |
ip_r1 = 192, ip_r2 = 168, ip_r3 = 12, ip_r4 = 65 local ipValueNum = bit.lshift(ip_r1, 24) + bit.lshift(ip_r2, 16) + bit.lshift(ip_r3, 8) + ip_r4 等价于: local ipValueNum = 192 左移24位 + 168 左移16位 + 12 左移8位 + 65 如上,如果我们以2进制来看的话,192这个数字是 1100 0000,左移24位之后,变成了 1100 0000 0000 0000 0000 0000 0000 0000 <strong>问题就出现在这里,对于32位的整形来说,最高位代表的符号位,1表示负数,所以才会出现上面的情况。</strong> |
注: bit模块使用可参考 http://bitop.luajit.org/api.html
那找到问题了,简单啊,将有符号的整形换做无符号整形或者长整形不就得了 ? 结果问题来了,我查了好久,竟然没有找到luajit以及lua5.1可以支持这两种类型的(或许是我没查到,但我真的查了挺久)。
后来,查到luajit可以通过ffi 库来调用 C 程序,我一想,那么好办了,C语言里面可是什么类型都有的啊,于是就先简单学习接触了一下luajit的这个ffi库(具体内容可参考网上的这篇博文),于是我写出了如下的程序:
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 |
require "bit" local ffi = require("ffi") ffi.cdef[[ uint32_t inet_addr(const char *cp); uint32_t ntohl(uint32_t netlong); ]] function ipRangeCheck(ip ,range_mask) local ip_s1,ip_s2,ip_s3,ip_s4,ip_mask = range_mask:match("(%d+)%.(%d+)%.(%d+)%.(%d+)%/(%d+)") ip_s = string.format("%s.%s.%s.%s", ip_s1, ip_s2, ip_s3, ip_s4) local ipValueNum = ffi.C.inet_addr(ip) local ipValueNum = ffi.C.ntohl(ipValueNum) local ipRangeMin = ffi.C.inet_addr(ip_s) <span style="color: #ff00ff;"><strong> -- 与之前的主要区别是这里将ipRangeMin转换成long类型再与别人进行与操作</strong></span> local ipRangeMin = ffi.C.ntohl(ipRangeMin) print(ipRangeMin) local ipRangeMin = bit.band(ipRangeMin, bit.lshift(0xFFFFFFFF, 32 - ip_mask)) local ipRangeMax = bit.bor(ipRangeMin, bit.rshift(0xFFFFFFFF, ip_mask)) print (string.format("ipValueNum = %d , ipRangeMin = %d, ipRangeMax = %d", ipValueNum , ipRangeMin , ipRangeMax)) if (ipRangeMin < ipValueNum and ipValueNum < ipRangeMax) then print(string.format("%s is in the range of %s", ip, range_mask)) else print(string.format("%s is not in the range of %s", ip, range_mask)) end end ipRangeCheck("192.168.12.254", "192.168.12.0/8") ~ |
运行后发现并没有什么卵用:
1 2 3 4 5 |
$ luajit/bin/luajit test.lua 3232238592 ipValueNum = 3232238657 , ipRangeMin = -1073741824, ipRangeMax = -1056964609 192.168.12.65 is not in the range of 192.168.12.0/8 |
貌似我每次只要执行下面这一步之后,ipRangeMin就变成负数了。
1 |
local ipRangeMin = bit.band(ipRangeMin, bit.lshift(0xFFFFFFFF, 32 - ip_mask)) |
我很郁闷,最后发现原因在于,0xFFFFFFFF左移动24位的结果是负的,并且在左移之后变成了64位:
1 2 3 4 5 6 |
$ luajit > print(bit.lshift(0xFFFFFFFF, 24)) -16777216 > > print(string.format("%x", bit.lshift(0xFFFFFFFF, 24))) ffffffffff000000 |
这让我突然觉得束手无策了,我开始怀疑luajit的bit库的处理逻辑了,于是我做了如下实验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
> require "bit" > <strong>当我左移之后,如果最高位不是1,则结果正常</strong> > print(string.format("%x", bit.lshift(0x12345678,24))) 78000000 > > <strong>当我左移之后,如果最高位是1,则它会默认的将前面的32位全部记为1</strong> > print(string.format("%x", bit.lshift(0x12345688,24))) ffffffff88000000 > > 这个值按理来说应该是一个很大的负值,像下面这样 > print(string.format("%d", 0xffffffff88000000)) -9223372036854775808 > > 但结果却是这样 > print(string.format("%d", bit.lshift(0x12345688,24))) -2013265920 > > > 而且它自动补全的这些1无法参与其他的位运算 > print(string.format("%x", bit.band(0x00000000ffffffff, 0xffffffff88000000))) fffffffffff88000 > |
无语,也就是说luajit的这个bit库它只能运算32位的操作数,而且当你使用它的左移函数时,如果移动的结果将最高位变成1,它会在前面再补32个1,且这些1没有实际的算术意思。
唉,既然luajit它本身的bit库不好用,那我用C语言的位操作可好? 但是又想到C语言的左移和右移是符号操作,没有实际的函数载体,而且即使我可以用那样代码写起来可用性也不高了。
于是我退而求其次,不使用位操作了,单纯的使用lua语言来实现这一功能,只是抛弃了IP地址段的最小值验证功能,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
require "bit" function ipRangeCheck(ip ,range_mask) local ip_r1, ip_r2, ip_r3, ip_r4 = ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)") local ip_s1,ip_s2,ip_s3,ip_s4,ip_mask = range_mask:match("(%d+)%.(%d+)%.(%d+)%.(%d+)%/(%d+)") -- 以乘积的形式得出ip值和IP段的最小值 local ipValueNum = ip_r1*2^24 + ip_r2*2^16 +ip_r3*2^8 +ip_r4 local ipRangeMin = ip_s1*2^24 + ip_s2*2^16 +ip_s3*2^8 +ip_s4 -- IP段的最小值加上主机数的可能性即为ip段的最大值 local temp = 32 - ip_mask local ipRangeMax = ipRangeMin + 2^temp if (ipRangeMin < ipValueNum and ipValueNum < ipRangeMax) then print(string.format("%s is in the range of %s", ip, range_mask)) else print(string.format("%s is not in the range of %s", ip, range_mask)) end end ipRangeCheck("192.168.12.65", "192.168.12.0/8") |
但是,这样写其实有一个问题,就是IP地址段的写法必须标准,比如:
1 2 |
正确的写法: 10.25.3.0/24 错误的写法: 10.25.3.4/24 |
到此,这个函数的功能才勉强实现了,感觉心好累,可能是我自己第一次接触Lua,上面的分析或者思路或许会有漏洞或错误,但今天确实没有找到其他方法了。
总结来说,其实这个功能算法很简单,但是耗费了我很多精力的原因是对Lua的不熟悉,以及对代码的掌控和分析能力较弱,后续还得提高。