bearDropper-add support for IPv6 and nftables set

折腾bearDropper:使其支持IPv6和使用nftables来封禁IP

‘TLDR’: 直接跳至总结可获取代码仓库地址

关于dropbear和bearDropper

dropbear是一个用于嵌入式系统的轻量级SSH服务器和客户端。它特别适合资源受限的设备,如路由器和小型物联网设备,通常用于 OpenWrt 等嵌入式操作系统。而bearDropper是通过读取系统日志,获取一定时间内频繁登录失败SSH服务的IP地址,将其添加到防火墙封禁列表中,以达到保护SSH服务免受爆破等影响。尽管在关闭了密码登录SSH后,使用key-based authentication已经极大程度保护了SSH服务不被爆破,但是在系统日志中频繁出现登录失败的日志甚至刷屏,这对于系统日志的维护也存在较大不良影响。

版本变更及现存问题

在OpenWrt系统(本人使用ImmortalWrt分支)更新到23.05版本后,原来防火墙iptables和ip6tables已被弃用,而转而使用nftables。最开始版本由@robzr编写的bearDropper仅支持ip(6)tables语句,经由@marjancinober修改后转为支持nftables的bearDropper

近年来IPv6的推广已得到极大支持,家宽基本已经实现IPv4和IPv6双栈,而上述修改版bearDropper仅支持IPv4地址的识别,在有IPv6地址访问失败的记录下,大概率出现malformat错误:
authpriv.notice bearDropper[xxxxx]: processLogLine(,171xxxxxxx) malformed line (authpriv.info dropbear[xxxxx]: Exit before auth from <2a06:4880:d000::e8:34201>: Exited normally)

另一个本人觉得不够简洁的地方在于,修改版bearDropper会在input_wan和forward_wan链插入一个名为bearDropper链的引用,而bearDropper链下是一条条封禁IP的drop规则,当封禁IP数量较大时可能存在一定的性能问题(由于需要由上至下一条条匹配IP规则),并且在系统防火墙界面下也相应地会有很长一段的drop规则。

另外不知道是由于什么原因,在follow模式下的bearDropper经常出现无响应的情况,表现为尽管有IP达到封禁条件,但是bearDropper无反应,猜测与出现的malformat错误有关。于是开始折腾之旅:

折腾目标

  1. 支持IPv6地址的识别和封禁
  2. 使用nftables set来存储封禁的IP地址

改写过程:IP相关函数

bearDropper采用shell语言编写,可直接阅读分析源码。

函数:getLogIP

首先分析函数:getLogIP

1
2
3
4
5
6
7
getLogIP () { 
local logLine="$1"
local ebaPID=$(echo "$logLine" | sed -n 's/^.*authpriv.info \(dropbear\[[0-9]*\]:\) Exit before auth.*/\1/p')
[ -n "$ebaPID" ] && logLine=$($cmdLogreadEba | grep -F "${ebaPID} Child connection from ")
echo "$logLine" | sed -n 's/^.*[^0-9]\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*$/\1/p'
}
# Args: $1=log line

里面关于ebaPID的2行代码是根据dropbear的进程ID来获取对应的Child connection from xxx.xxx.xxx.xxxExit before auth from <xxx.xxx.xxx.xxx>2行日志,如果是一个登录失败,这2行日志是成对出现的。而最后一行代码中的sed语句则是提取传入变量$logLine中的IPv4地址的正则表达式。
要使其识别IPv6地址很简单,只需要把对应的表达式换成识别IPv6的即可,在网上查询一下很快便找到了一个相关讨论:

IPv6 addresses:
zero compressed IPv6 addresses (section 2.2 of rfc5952)
link-local IPv6 addresses with zone index (section 11 of rfc4007)
IPv4-Embedded IPv6 Address (section 2 of rfc6052)
IPv4-mapped IPv6 addresses (section 2.1 of rfc2765)
IPv4-translated addresses (section 2.1 of rfc2765)
IPv6 Regular Expression:

(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}| ::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))

好长一串,毕竟IPv6地址比较复杂且有多种形式,具体到实现上,先将getLogIP 函数改写成:

1
2
3
4
5
6
7
getLogIP () {
local logLine="$1"
local ip=$(getLogIPv6 "$logLine")
[ -z "$ip" ] && ip=$(getLogIPv4 "$logLine")
[ -z "$ip" ] && logLine 1 "Error: getLogIp() malformed line ($logLine)"
echo "$ip"
}

并添加2个函数getLotIPv4getLotIPv6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
getLogIPv4 () { 
local logLine="$1"
local ebaPID=$(echo "$logLine" | sed -n 's/^.*authpriv.info \(dropbear\[[0-9]*\]:\) Exit before auth.*/\1/p')
[ -n "$ebaPID" ] && logLine=$($cmdLogreadEba | grep -F "${ebaPID} Child connection from ")
local ip=$(echo "$logLine" | sed -n 's/^.*[^0-9]\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*$/\1/p')
echo "$ip"
}

getLogIPv6 () {
local logLine="$1"
local ebaPID=$(echo "$logLine" | sed -n 's/^.*authpriv.info \(dropbear\[[0-9]*\]:\) Exit before auth.*/\1/p')
[ -n "$ebaPID" ] && logLine=$($cmdLogreadEba | grep -F "${ebaPID} Child connection from ")
local ip=$(echo "$logLine" | grep -o -E '(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))')
echo "$ip"
}

这样可以在不改变其他地方对于getLogIP函数的调用情况下完成对IPv6的支持,这里面也偷了一点小懒,首先直接用IPv6的regex表达式进行匹配,如果没有匹配到再进一步使用IPv4的表达式进行匹配,如果均无法匹配,则先提示报错吧~~

添加getIPtype函数

对于获取到的IP地址,我们需要一个getIPtype 函数来判断IP_Type是IPv4还是IPv6:

1
2
3
4
5
6
7
8
9
10
# determine if IP is IPv4 or IPv6, Args: $1=IP
getIPtype () {
local ip="$1"
local ipv4_regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$"
local ipv6_regex="(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"
if [[ $ip =~ $ipv4_regex ]]; then echo "ipv4"
elif [[ $ip =~ $ipv6_regex ]]; then echo "ipv6"
else echo "invalid"
fi
}

函数:banIP

下面是对主要函数banIP 的分析和修改:

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
# Args: $1=IP
banIP () {
local ip="$1"
if ! nft list chain inet fw4 $firewallChain >/dev/null 2>/dev/null ; then
logLine 1 "Creating nft chain $firewallChain"
nft add chain inet fw4 $firewallChain
fi
for x in $firewallHookChains ; do
chain="${x%:*}" ; position="${x#*:}"
if [ $position -ge 0 ] && ! nft -a list chain inet fw4 $chain 2>/dev/null | grep -qE "\\t+jump $firewallChain\>" ; then
logLine 1 "Inserting hook into nft chain $chain"
if [ $position = 0 ] ; then
_hl=$(nft -a list chain inet fw4 $chain | sed -En 's/\t*jump .* # handle //p' | tail -n1)
if [ -z "$_hl" ] ; then nft add rule inet fw4 $chain jump $firewallChain
else nft insert rule inet fw4 $chain handle $_hl jump $firewallChain ; fi
else
nft insert rule inet fw4 $chain index $((position-1)) jump $firewallChain
fi ; fi
done
if ! nft list chain inet fw4 $firewallChain | grep -q "saddr $ip .*$firewallTarget" 2>/dev/null ; then
logLine 1 "Inserting ban rule for IP $ip into nft chain $firewallChain"
nft add rule inet fw4 $firewallChain ip saddr $ip "$firewallTarget"
else
logLine 3 "banIP() rule for $ip already present in nft chain"
fi
}

默认配置里,$firewallChain值为bearDropper, $firewallHookChains为列表[input_wan:1, forward_wan:1],每个元素:前为防火墙chain,后为添加的position(1表示insert,0表示append到最后,-1表示不添加),函数里第一个if判断bearDropper链是否存在,不存在则新建该链,在for循环里根据$firewallHookChains列表里chain和position来确定添加到哪个链及位置。最后一个if判断是否已经存在该IP的ban链了,无则添加,有则log提示。

那我们如何修改使其支持IPv6,并且不再使用nft add rule inet fw4 $firewallChain ip saddr $ip "$firewallTarget"添加drop链的形式,而转用set的形式呢?
首先,新建set的nft语句是:

1
2
nft create set inet fw4 $set_name_1 { type ipv4_addr \; flags interval \; auto-merge \; } 
nft create set inet fw4 $set_name_2 { type ipv6_addr \; flags interval \; auto-merge \; }

可以看到,nft的set是IPv4和IPv6分开的,两种IP地址不能存在一个set中。auto-merge是相邻IP添加进去后会被自动存为IP Range格式:x.x.x.1-x.x.x.5,甚至更多的地址会融合成IP CIDR格式。

将IP添加到set的nft语句是:

1
nft add element inet fw4 $set_name { $ip } 

注意根据$ip的类型,存到各自类型的set中,这时候,前面写的getIPtype 函数就能发挥用处了。我们还是根据上面的想法,添加2个函数来分别处理IPv4和IPv6:

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
# Args: $1=IP
banIPv4 () {
local ip="$1"
if ! nft list set inet fw4 ${firewallChain}_v4 >/dev/null 2>/dev/null ; then
logLine 1 "Creating nft set ${firewallChain}_v4"
nft create set inet fw4 ${firewallChain}_v4 { type ipv4_addr \; flags interval \; auto-merge \; }
fi

for x in $firewallHookChains ; do
chain="${x%:*}" ; position="${x#*:}"
if [ $position -ge 0 ] && ! nft -a list chain inet fw4 $chain 2>/dev/null | grep -qE "\\t+ip saddr @${firewallChain}_v4 $firewallTarget" ; then
logLine 1 "Inserting IPv4 hook into nft chain $chain"
if [ $position = 0 ] ; then
_hl=$(nft -a list chain inet fw4 $chain | sed -En 's/\t*ip saddr @${firewallChain}_v4 $firewallTarget # handle //p' | tail -n1)
if [ -z "$_hl" ] ; then nft add rule inet fw4 $chain ip saddr @${firewallChain}_v4 $firewallTarget
else nft insert rule inet fw4 $chain handle $_hl ip saddr @${firewallChain}_v4 $firewallTarget ; fi
else
nft insert rule inet fw4 $chain index $((position-1)) ip saddr @${firewallChain}_v4 $firewallTarget
fi ; fi
done
if [ -z "$(nft list set inet fw4 ${firewallChain}_v4 | grep ${ip})" ] ; then
logLine 1 "Inserting IP $ip into nft set ${firewallChain}_v4"
nft add element inet fw4 ${firewallChain}_v4 {$ip}
else
logLine 2 "banIPv4() IP $ip already in nft set ${firewallChain}_v4"
fi
}

思路很清楚了,主要修改点如下:
1. 第一个if里,把原函数的add chain inet fw4 $firewallChain改为create set inet fw4 ${firewallChain}_v4,v4后缀代表这个set储存IPv4地址
2. for循环里,把nft add rule inet fw4 $chain jump $firewallChain的链引用及跳转改为符合source addr在set内条件下直接$firewallTarget, 即nft add rule inet fw4 $chain ip saddr @${firewallChain}_v4 $firewallTarget,把附近的几处判断和grep语句也进行相应的修改。这样调整后ban的IPv4将进入名为${firewallChain}_v4(默认为bearDropper_v4)的set中,通过在chain里添加条件符合ip saddr @${firewallChain}_v4的IP进行$firewallTarget(默认为drop)。
3. 把v4换成v6,写一个几乎一样的banIPv6 函数:

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
banIPv6 () {
local ip="$1"
if ! nft list set inet fw4 ${firewallChain}_v6 >/dev/null 2>/dev/null ; then
logLine 1 "Creating nft set ${firewallChain}_v6"
nft create set inet fw4 ${firewallChain}_v6 { type ipv6_addr \; flags interval \; auto-merge \; }
fi

for x in $firewallHookChains ; do
chain="${x%:*}" ; position="${x#*:}"
if [ $position -ge 0 ] && ! nft -a list chain inet fw4 $chain 2>/dev/null | grep -qE "\\t+ip6 saddr @${firewallChain}_v6 $firewallTarget" ; then
logLine 1 "Inserting IPv6 hook into nft chain $chain"
if [ $position = 0 ] ; then
_hl=$(nft -a list chain inet fw4 $chain | sed -En 's/\t*ip6 saddr @${firewallChain}_v6 $firewallTarget # handle //p' | tail -n1)
if [ -z "$_hl" ] ; then nft add rule inet fw4 $chain ip6 saddr @${firewallChain}_v6 $firewallTarget
else nft insert rule inet fw4 $chain handle $_hl ip6 saddr @${firewallChain}_v6 $firewallTarget ; fi
else
nft insert rule inet fw4 $chain index $((position-1)) ip6 saddr @${firewallChain}_v6 $firewallTarget
fi ; fi
done
if [ -z "$(nft list set inet fw4 ${firewallChain}_v6 | grep ${ip})" ] ; then
logLine 1 "Inserting IP $ip into nft set ${firewallChain}_v6"
nft add element inet fw4 ${firewallChain}_v6 {$ip}
else
logLine 2 "banIPv6() IP $ip already in nft set ${firewallChain}_v6"
fi
}

最后在改写原来的banIP 函数,调用getIPtype来决定使用banIPv4 还是banIPv6

1
2
3
4
5
6
7
8
9
banIP () {
local ip="$1"
local type=$(getIPtype "$ip")
case $type in
ipv4) banIPv4 "$ip" ;;
ipv6) banIPv6 "$ip" ;;
*) logLine 1 "Error: banIP() invalid IP address ($ip)" ;;
esac
}

函数unBanIP

下面是对主要函数unBanIP 的分析和修改:

1
2
3
4
5
6
7
8
9
10
11
# Args: $1=IP
unBanIP () {
local ip="$1"
_hl=$(nft -a list chain inet fw4 $firewallChain 2>/dev/null | sed -n "/ saddr $ip /{s|\t*ip saddr $ip .*$firewallTarget # handle ||p;q}")
if [ "$_hl" ] ; then
logLine 1 "Removing ban rule for IP $ip from nft"
nft delete rule inet fw4 bearDropper handle $_hl
else
logLine 3 "unBanIP() Ban rule for $ip not present in nft"
fi
}

默认配置里,$firewallChain值为bearDropper, $firewallTarget值为drop。函数第二行是为了获取nftables里bearDropper链下$ip对应的handle(即位置标识符),如果存在则删掉该handle,不存在则抛出一个日志,提示not present
添加IPv6支持,如法炮制,分出2个函数unBanIPv4unBanIPv6 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Args: $1=IP
unBanIPv4 () {
local ip="$1"
if ! nft delete element inet fw4 ${firewallChain}_v4 {$ip} >/dev/null 2>/dev/null ; then
logLine 3 "unBanIP() $ip not present in nft set ${firewallChain}_v4"
else
logLine 1 "Removing IP $ip from nft set"
fi
}

unBanIPv6 () {
local ip="$1"
if ! nft delete element inet fw4 ${firewallChain}_v6 {$ip} >/dev/null 2>/dev/null ; then
logLine 3 "unBanIP() $ip not present in nft"
else
logLine 1 "Removing IP $ip from nft set ${firewallChain}_v6"
fi
}

函数wipeFirewall

对主要函数wipeFirewall 的分析和修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
wipeFirewall () {
local x chain position
for x in $firewallHookChains ; do
chain="${x%:*}" ; position="${x#*:}"
if [ $position -ge 0 ] ; then
_hl=$(nft -a list chain inet fw4 $chain 2>/dev/null | sed -nE "s/\\t+jump $firewallChain # handle //p")
if [ "$_hl" ] ; then
logLine 1 "Removing hook from nft chain $chain"
nft delete rule inet fw4 $chain handle $_hl
fi ; fi
done
if nft list chain inet fw4 $firewallChain >/dev/null 2>/dev/null ; then
logLine 1 "Flushing and removing nft chain $firewallChain"
nft flush chain inet fw4 $firewallChain 2>/dev/null
nft delete chain inet fw4 $firewallChain 2>/dev/null
fi
}

这个函数主要是在程序升级,退出,或者手动执行wipe的情况下,对已有链和IP drop规则的清楚,改起来也是针对相应的chain和rule进行修改,改为我们的set和delete elment即可,我们需要注意原来的每个chain对应一个$firewallChain,而我们修改后是有v4和v6两个set的,需要分别获取对应的handle,然后delete rule by handle,最后flush set,delete set即可。修改后的函数:

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
wipeFirewall () {
local x chain position
for x in $firewallHookChains ; do
chain="${x%:*}" ; position="${x#*:}"
if [ $position -ge 0 ] ; then
_hl4=$(nft -a list chain inet fw4 $chain 2>/dev/null | sed -nE "s/\\t+ip saddr @${firewallChain}_v4 $firewallTarget # handle //p")
_hl6=$(nft -a list chain inet fw4 $chain 2>/dev/null | sed -nE "s/\\t+ip6 saddr @${firewallChain}_v6 $firewallTarget # handle //p")
if [ "$_hl4" ] ; then
logLine 1 "Removing IPv4 hook from nft chain $chain"
nft delete rule inet fw4 $chain handle $_hl4
fi
if [ "$_hl6" ] ; then
logLine 1 "Removing IPv6 hook from nft chain $chain"
nft delete rule inet fw4 $chain handle $_hl6
fi
fi
done
if nft list set inet fw4 ${firewallChain}_v4 >/dev/null 2>/dev/null ; then
logLine 1 "Flushing and removing nft set ${firewallChain}_v4"
nft flush set inet fw4 ${firewallChain}_v4 2>/dev/null
nft delete set inet fw4 ${firewallChain}_v4 2>/dev/null
fi
if nft list set inet fw4 ${firewallChain}_v6 >/dev/null 2>/dev/null ; then
logLine 1 "Flushing and removing nft setß ${firewallChain}_v6"
nft flush set inet fw4 ${firewallChain}_v6 2>/dev/null
nft delete set inet fw4 ${firewallChain}_v6 2>/dev/null
fi
}

小结:关于IP的函数操作

上面的几个函数均是关于日志中IP的提取和对应nft规则的添加或者删除。而bearDropper还内置了一套简易的数据库系统,以记录ban的时间,如果达到config中设置的benlength时长,对于这些“过期”的IP地址,我们还是要放出来的,也即使用unBanIP函数,但是关于这个建议数据库对于IPv6的支持和修改也是需要继续折腾的。当初我还以为修改完这几个函数就可以运行了,但是在虚拟机的测试中发现,IP地址要先进入它的数据库系统中,进行次数和时长的判断,程序才会进一步决定是否达到ban的标准的,还是太年轻。继续折腾吧!~

改写过程:bddb数据库相关函数

简易bddb数据库

在原版及修改版的bearDropper运行过程中,会在/tmp目录下生成bearDropper.bddb文件,使用cat命令可以直接打开,发现它是如下的文件格式:

bddb_101_43_93_18=1,1719324346
bddb_116_140_208_227=0,1719380201,1719380202
bddb_139_9_71_115=1,1719378933
bddb_141_98_11_82=1,1719345816
bddb_14_103_20_212=1,1719356376

很明显,每行以bddb_开头,IPv4地址的.全部转换成了下划线_=号连接了0/1,[timestamp1],[timestamp2],… 再简单翻看了bddb相关函数后,发现数字0,代表暂未达到封禁条件,后面跟的一系列数字是%s化后的时间戳,每在syslog里有一次失败登录,bearDropper就会在后面加上一次对应的时间戳。规定时间内如果达到了一定次数count($stamp), 程序就回调用banIP函数,并且在bddb中修改0 => 1, 同时更新timestamp为封禁的时间戳。

下面我们来看一下bddb相关的重要函数:

函数bddbClear

1
2
3
4
5
6
# Clear bddb entries from environment
bddbClear () {
local bddbVar
for bddbVar in `set | grep -E '^bddb_[0-9_]*=' | cut -f1 -d= | xargs echo -n` ; do eval unset $bddbVar ; done
bddbStateChange=1
}

这是bearDropper里的第一个函数,一次看还一头雾水,现在明白了,set是获取当前bash环境下所有的变量set,然后用grep获取以bddb开头的条目,'^bddb_[0-9_]*='显然就是获取形如bddb_xxx_xxx_xxx_xxx的条目,那如果我们要使其变为支持IPv6,在不改变bddb数据组织结构的情况下,我们的IPv6条目一定是形如bddb_2a06_19af_19af_19af_19af_19af_19af_1234=0/1,timestamp1,...的,在这里我们只需要关注前面的部分即可,把'^bddb_[0-9_]*='改为'^bddb_[a-fA-F0-9_]*='应该就可以匹配上了,因为IPv6不光有数字0-9,还有字母a-f,A-F,把它们加到正则里就行了。修改后函数:

1
2
3
4
5
bddbClear () { 
local bddbVar
for bddbVar in `set | grep -E '^bddb_[a-fA-F0-9_]*=' | cut -f1 -d= | xargs echo -n` ; do eval unset $bddbVar ; done
bddbStateChange=1
}

函数bddbCount

1
bddbCount () { set | grep -E '^bddb_[0-9_]*=' | wc -l ; }

一个和上面类似的函数,闭着眼睛修改为:

1
bddbCount () { set | grep -E '^bddb_[a-fA-F0-9_]*=' | wc -l ; }

函数bddbLoadbddbSave

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Loads existing bddb file into environment
# Arg: $1 = file, $2 = type (bddb/bddbz), $3 =
bddbLoad () {
local loadFile="$1.$2" fileType="$2"
if [ "$fileType" = bddb -a -f "$loadFile" ] ; then
. "$loadFile"
elif [ "$fileType" = bddbz -a -f "$loadFile" ] ; then
local tmpFile="`mktemp`"
zcat $loadFile > "$tmpFile"
. "$tmpFile"
rm -f "$tmpFile"
fi
bddbStateChange=0
}

仔细看了之后,发现没有修改的点,这个函数应该是完美支持我们的IPv6数据格式的,它的作用就只是把/tmp/bearDropper.bddb文件按行读取到bash环境中,因为里面正好是类似于var=content的格式,这也与上面的bddbClear直接从环境里清除变量的操作对上了。bearDropper就是把这个文件里的所有行当作变量储存到bash环境里,然后在进行一系列的操作的。

1
2
3
4
5
6
7
8
9
10
# Saves environment bddb entries to file, Arg: $1 = file to save in
bddbSave () {
local saveFile="$1.$2" fileType="$2"
if [ "$fileType" = bddb ] ; then
set | grep -E '^bddb_[a-f,A-F,0-9_]*=' | sed s/\'//g > "$saveFile"
elif [ "$fileType" = bddbz ] ; then
set | grep -E '^bddb_[a-f,A-F,0-9_]*=' | sed s/\'//g | gzip -c > "$saveFile"
fi
bddbStateChange=0
}

这个save函数同理,把环境中的bddb变量纯存到文件中去,应该是一系列操作最后的保存工作。仅需修改bddb的正则部分即可。

函数bddbEnableStatus

1
2
3
4
5
6
7
8
# Set bddb record status=1, update ban time flag with newest
# Args: $1=IP Address $2=timeFlag
bddbEnableStatus () {
local record=`echo $1 | sed -e 's/\./_/g' -e 's/^/bddb_/'`
local newestTime=`bddbGetTimes $1 | sed 's/.* //' | xargs echo $2 | tr \ '\n' | sort -n | tail -1 `
eval $record="1,$newestTime"
bddbStateChange=1
}

解释的很清楚,就是把达到ban标准的IP status改为1,timestamp更新为封禁时timestamp。我们需要修改的点主要在于函数体内第一条,注意它的作用是把IP中的.转换为_,并在开头添加bddb_,我们的IPv6则应该是把:转为_即可。只要在sed后再添加一个-e 's/:/_/g'即可,修改如下:

1
2
3
4
5
6
bddbEnableStatus () {
local record=`echo $1 | sed -e 's/\./_/g' -e 's/:/_/g' -e 's/^/bddb_/'`
local newestTime=`bddbGetTimes $1 | sed 's/.* //' | xargs echo $2 | tr \ '\n' | sort -n | tail -1 `
eval $record="1,$newestTime"
bddbStateChange=1
}

注意到下一行还使用了bddbGetTimes 函数,我们就在接下来看一看这个函数:

函数bddbGetTimesbddbGetStatusbddbGetRecord

1
2
3
4
5
6
7
8
# Args: $1=IP Address
bddbGetTimes () {
bddbGetRecord $1 | cut -d, -f2-
}

bddbGetStatus () {
bddbGetRecord $1 | cut -d, -f1
}

又调用了bddbGetRecord 函数:

1
2
3
4
5
6
# retrieve single IP record, Args: $1=IP
bddbGetRecord () {
local record
record=`echo $1 | sed -e 's/\./_/g' -e 's/^/bddb_/'`
eval echo \$$record
}

结合起来看,bddbGetRecord是获取了当前bash环境里$bddb_ip的值作为函数返回,在bddbGetTimes里用cut函数,获取了status后面的一系列timestamps。我们也只要增加替换ip地址中的.和:即可:只要在sed后再添加一个-e 's/:/_/g'

1
2
3
4
5
bddbGetRecord () {
local record
record=`echo $1 | sed -e 's/\./_/g' -e 's/:/_/g' -e 's/^/bddb_/'`
eval echo \$$record
}

这样回到上一个函数bddbGetTimes ,$newestTime也很达意了,就是获取最后一个timestamp, 也即封禁IP时的timestamp

函数bddbAddRecord

1
2
3
4
5
6
7
8
9
10
# Args: $1 = IP address, $2 [$3 ...] = timestamp (seconds since epoch)
bddbAddRecord () {
local ip="`echo "$1" | tr . _`" ; shift
local newEpochList="$@" status="`eval echo \\\$bddb_$ip | cut -f1 -d,`"
local oldEpochList="`eval echo \\\$bddb_$ip | cut -f2- -d, | tr , \ `"
local epochList=`echo $oldEpochList $newEpochList | xargs -n 1 echo | sort -un | xargs echo -n | tr \ ,`
[ -z "$status" ] && status=0
eval "bddb_$ip"\=\"$status,$epochList\"
bddbStateChange=1
}

具体没看太明白,但是肯定是在每个record后面添加上新的timestamp用到的函数。尝试修改第一行,也是把ip地址中的.和:修改为_,这里用到了 tr 命令,不知道为什么不用sed了 😂。类似的修改方式,只动了第一行,其他行没太明白,但应该也无需修改:

1
2
3
4
5
6
7
8
9
bddbAddRecord () {
local ip="`echo "$1" | tr . _ | tr : _`" ; shift
local newEpochList="$@" status="`eval echo \\\$bddb_$ip | cut -f1 -d,`"
local oldEpochList="`eval echo \\\$bddb_$ip | cut -f2- -d, | tr , \ `"
local epochList=`echo $oldEpochList $newEpochList | xargs -n 1 echo | sort -un | xargs echo -n | tr \ ,`
[ -z "$status" ] && status=0
eval "bddb_$ip"\=\"$status,$epochList\"
bddbStateChange=1
}

函数bddbRemoveRecord

1
2
3
4
5
6
# Args: $1 = IP address
bddbRemoveRecord () {
local ip="`echo "$1" | tr . _`"
eval unset bddb_$ip
bddbStateChange=1
}

和上面一样,闭着眼修改:

1
2
3
4
5
6
# Args: $1 = IP address
bddbRemoveRecord () {
local ip="`echo "$1" | tr . _ | tr : _`"
eval unset bddb_$ip
bddbStateChange=1
}

函数bddbGetAllIPs

1
2
3
4
5
6
7
8
9
10
# Returns all IPs (not CIDR) present in records
bddbGetAllIPs () {
local ipRaw record
set | grep -E '^bddb_[0-9_]*=' | tr \' \ | while read record ; do
ipRaw=`echo $record | cut -f1 -d= | sed 's/^bddb_//'`
if [ `echo $ipRaw | tr _ \ | wc -w` -eq 4 ] ; then
echo $ipRaw | tr _ .
fi
done
}

驾轻就熟了,就是把bash环境里的bddb变量全部转换为原始的IP地址,那么看里面的if语句,按照_分割后,如果数量为4,那就把_替换回.,这里明显是IPv4地址的替换,要添加到IPv6,只需添加一个else语句,里面写上echo $ipRaw | tr _ :。原作者这里用4来确定,肯定是为了健壮性,因为不是4的情况,有可能是前面的IP地址获取不正确,那样有问题的IP地址就不会进入到后续的处理中,我们驾到else里其实是存在潜在问题的,但是管他,先干了再说。有bug再修吧。。。。 😂
修改后:

1
2
3
4
5
6
7
8
9
10
11
bddbGetAllIPs () { 
local ipRaw record
set | grep -E '^bddb_[a-f,A-F,0-9_]*=' | tr \' \ | while read record ; do
ipRaw=`echo $record | cut -f1 -d= | sed 's/^bddb_//'`
if [ `echo $ipRaw | tr _ \ | wc -w` -eq 4 ] ; then
echo $ipRaw | tr _ .
else
echo $ipRaw | tr _ :
fi
done
}

总结

修改以上这些函数就达到我们的2个折腾目的了,可以看出,bearDropper写的水平还是很高的,仔细阅读就能发现,我们从未涉及到关于封禁条件的逻辑判断,因为这部分函数和主体过程写的也很巧妙。不过考虑到这些函数和后面的处理过程也主要是对后面的timestamp进行取值和判断,这些部分我们都是未进行修改的。关于bearDropper的逻辑判断和主体程序部分,有空在另一片文章中在解读吧。。。

我修改好的bearDropper也上传至我的代码仓库, 有需要的可以直接在仓库首页README里的链接一键安装使用。