最近发现老集群上的 fail2ban 把我的办公电脑IP给禁了,无奈只能设置忽略该ip,然后在集群上运行 lastb 可以发现大量来自我电脑的尝试 ssh 登录,各种用户名。真是奇怪,难道我的办公电脑被入侵了?查找了很久也没有发现问题所在。今天重启了这台电脑,在集群上可以发现重启的那几分钟没有了尝试登录,可是当电脑重启后又出现了尝试登录,看来问题确实在这台电脑上。由于是刚重启,我重复执行命令 netstat -nap | grep 集群ip
进行搜索,时不时出现的只有一个 frpc 进程。最初我一直以为这个进程是正常的,因为它就是为登录集群而设的,所以与集群有连接最初并没有引起我的注意。但是我突然想到,如果外部没有人在通过 frp 连接我的集群的话,是不会出现这个连接的。于是恍然大悟,这些尝试登录的连接都是通过暴露在外网的 frps 端口连进来的!但是对集群来说,它并不知道 frp 的存在,它只知道是从我的电脑上过来的连接。于是去查看 frps 的日志,果然有来自各处的许多 ip 频繁的在尝试登录!
问题找到了,接下来如何解决该问题呢?通过搜索发现了这个帖子,这位仁兄写了一个 frptables 程序,https://github.com/zngw/frptables,其功能是分析 frps 的日志文件(0.36版本之后才有日志文件),然后统计连接 frps 的 ip 数据,设定规则当给定时间内连接数超过设定值时则设置防火墙禁止该 ip,防火墙支持 iptables 和 firewalld。Github 上下载下来的zip压缩包里就是一个单独的 linux 可执行文件frptables
,只需要自己设好相应 .yml 配置文件即可使用了。
一、安装 iptables
我的 frps 是在 nas 上的 docker 容器里运行的(snowdreamtech/frps),进入容器终端运行 iptables -L 发现根本没有该命令,cat /etc/issue
发现容器是 alpine linux,其包管理器是 apk,于是通过如下命令方便的安装 iptables
apk update
apk add iptables
装完后发现运行 iptables -L 依然不行,提示如下
iptables v1.8.7 (legacy): can't initialize iptables table `filter': Permission denied (you must be root)
Perhaps iptables or your kernel needs to be upgraded.
通过搜索发现是容器的权限问题,只需要给容器 NET_RAW 和 NET_ADMIN 权限即可,而默认情况下前者是有的,只需要再加上后者即可。当然如果图省事打开所有权限也行。
二、手动运行 frptables
把 frptables 拷到 nas 上的 /mypath/to/frps 并把该路径挂载为容器的 /etc/frp,设置 frptables-config.yml 如下
# frps日志文件
frps_log: /etc/frp/frps.log
# frptables 的输出日志目录
logs: /etc/frp/banlog/
# frps 名字端口对应配置
name_port:
"myssh1": 12345
"myssh2": 12346
"ssh00": 12347
# 启用防火墙类型 iptables / firewall / md (Microsoft Defender)
tables_type: iptables
# ip白名单:
allow_ip:
- 127.0.0.1
# 端口白名单
allow_port:
- 7500
# - 443
# 规则访问
rules:
# 按数组顺序来,匹配到了就按匹配的规则执行,跳过此规则。
# 地区 country-国家, regionName-省名,名字中不带省字, city-市名,名字中也不带市字
# 端口: -1 所有端口
# time: 时间区间
# count: 访问次数,-1不限,0限制。其他为 time时间内访问count次,超出频率就限制
- # 20分钟内访问15次则加入防火墙
port: -1
country:
regionName:
city:
time: 1200
count: 15
# - # 中国上海IP允许
# port: -1
# country: 中国
# regionName: 上海
# city: 上海
# time: 1
# count: -1
#
# - # 中国地区IP 10分钟3次,超出这频率添加防火墙
# port: -1
# country: 中国
# regionName: 浙江
# city:
# time: 600
# count: 3
#
# - # 其他地区IP 直接加入防火墙
# port: -1
# country:
# regionName:
# city:
# time: 1
# count: 0
然后手动运行 ./frptables -c ./frptables-config.yml
,一开始并不成功。原来是程序内部是通过 bash 来调用命令行的,然而容器默认没有装 bash,只有 sh,所以需要先安装 bash(只需命令 apk add bash
即可)。安装完 bash,一开始运行时还是出错,发现问题竟然是 "ssh00": 12347
前多了一个空格。最初的时候为了让它与上一行的冒号对其,"ssh00": 12347
前有三个空格,结果出错,改成两个空格就正常了。然后查看日志 banlog/frptables.info.2022-08-15,会有类似如下输出
2022-08-15 23:05:25.192 [TRACE] - [Tag:link] [allow: [username.myssh1]206.189.226.38:-1 ->1, 美国,XX,XX]
2022-08-15 23:10:06.952 [TRACE] - [Tag:link] [allow: [username.myssh1]206.189.226.38:-1 ->2, 美国,XX,XX]
2022-08-15 23:11:10.475 [TRACE] - [Tag:link] [allow: [username.myssh1]180.69.254.177:-1 ->1, 韩国,XX,XX]
2022-08-15 23:11:21.461 [TRACE] - [Tag:link] [allow: [username.myssh1]206.189.226.38:-1 ->3, 美国,XX,XX]
2022-08-15 23:12:38.525 [TRACE] - [Tag:link] [allow: [username.myssh1]206.189.226.38:-1 ->4, 美国,XX,XX]
当 -1 -> 4
这类的计数超过设定的 15 后,allow 会变为 refuse,再查看 iptables -L 则会发现该 ip 被禁止了。
三、网络设为 macvlan
最初的 frps 容器的网络设的是 host,当在容器中运行 iptables -L 时发现输出的结果和在宿主机(nas)里运行 iptables -L 的结果一样,可见容器在 host 模式下与宿主机共享网络设置,所以如果容器里的 frptables 修改了 iptables 那宿主机的应该也同样改变了,当然也可以直接在宿主机里运行 frptables。但是我不想修改宿主机的 iptables 设置,那就不能对该容器是 host 网络。
换成 bridge 网络试试,发现映射 7000 端口时 tcp 和 udp 都得打开才行,否则连不上,此外还要映射 7500 端口以及需要穿透的端口(比如上面例子中的 12345、12346、12347)。映射好端口并能够正常连接后运行 frptables,为了测试,最初的设置是所有连接直接禁掉。结果发现 frps.log 里的 ip 都成了 172.17.0.1,也就是和容器桥接的宿主机ip,这时由于进行了一次封装,容器里已无法区分与 frps 建立连接的真实 ip 了,看到的只有 172.17.0.1。可见该方案也不行。
最后只能使用 macvlan 网络模式,因为这样会分配给容器一个与宿主机同网段 192.168.0.xxx 的 ip,而此时容器的的网络和宿主机的网络在逻辑上以及完全无关,相当于两台独立的电脑,这时便可以在容器里设置 iptables 而不影响宿主机的 iptables 配置,而容器也能识别真实的连接 ip。
四、设置 frptables 自启动
1、尝试 openrc 失败
最初尝试在 /etc/rc.local 中设置运行 frptables 的命令,然而容器启动时该文件并不生效,与是否设成可执行属性无关。由于容器用的是 alpine linux,网上说 alpine linux 不是通过 /etc/rc.local 来设置开机启动,而是通过 local 服务运行 /etc/local.d/ 下的脚本来实现,而服务则是通过 openrc 来控制,于是尝试如下操作
apk add openrc # 安装 openrc,容器里默认是没有安装的
rc-update add local default
# 把运行 frptables 的命令写成脚本,命名为 xxx.start 并放到 /etc/local.d/ 目录下
rc-service local start
在执行完 rc-service local start
之后,xxx.start 脚本确实执行了,但问题是我尝试重启容器后却并不执行 xxx.start,而且重启后查看 rc-service local status
依然是 started。尝试了很多次都不行,容器启动时 xxx.start 不会自动运行,只有手动执行 rc-service local start
或 restart
时才会执行,我猜或许是 docker 对其进行了限制,比较这是容器里的 alpine 系统而不是实体机上的。
2、尝试修改 config.v2.json 也失败
在 nas 里运行 docker ps
找到 frps 容器所对应的 id,进而进到其目录 /volume1/@docker/containers/56b415b0a5xxxxx
,其目录字符串 56b415b0a5xxxxx 的前面部分即该容器的 id。编辑 config.v2.json,修改如下两部分
"Path":"/bin/sh", "Args":["-c","/usr/bin/frps -c /etc/frp/frps.ini"]
"Entrypoint":["/bin/sh","-c","/usr/bin/frps -c /etc/frp/frps.ini"]
为
"Path":"/bin/sh", "Args":["-c","/etc/frp/mystart.sh"]
"Entrypoint":["/bin/sh","-c","/etc/frp/mystart.sh"]
结果并没用,重启容器后又自动恢复原来的样子了。即使我用了 systemctl restart pkg-Docker-dockerd
重启了 docker 服务(当然所有容器也都重启了)也不行。
3、无奈只能修改 /usr/bin/frps
既然默认的启动命令是 /usr/bin/frps -c /etc/frp/frps.ini
,那干脆通过文件映射把自己写的启动脚本 mystart.sh 映射为容器里的 /usr/bin/frps,这样就可以在容器启动时执行 mystart.sh 里的内容了。
五、保存被禁 ip 列表
首先是 iptables 设置的导入导出,标准做法是用 iptables-save > save.txt
进行保存设置,用 iptables-restore < save.txt
来恢复设置。那么我想到的是通过 cron 任务定义保存设置,而恢复设置的命令则应写在开机运行脚本里。谁知默认的 crontab -e 里指定的 run-parts /etc/periodic/hourly
等命令并不管用。/etc/periodic/ 目录下有 15min、hourly、daily、weekly、monthly目录,默认都是空的,我把可执行脚本放入其中一个,比如15min里,结果并不管用。直接手动运行 run-parts /etc/periodic/15min
也会报错。干脆不用 run-parts 相关的目录设置,直接在 crontab -e 里设置运行命令,发现依然不运行!原来是 crond 守护进程根本没有启动! 只需运行一下 crond
命令即可启动,且自动后台运行了。于是按上面方案保存和恢复 iptables 设置,为测试,事先将已经有三个被禁ip的设置手动保存,结果重启后发现在容器里运行 iptables -L 时相当慢,三个被禁的ip半天出一个。而且发现 frptables 的日志也似乎不正常,本来 -1 -> 3
中 ->
后面的计数都成了 0
,按说 0 是强制封禁,但是也没有对相应 ip 封禁,且日志中相应 ip 后的地区识别也没有,都是空的。
于是尝试不使用 iptables-restore 恢复,因为 iptables-save 的输出中除了含有 DROP 命令的相关行是真正要封禁的 ip 外还有好多其他设置,而这些其他的设置中的有些数值在每次重启容器时还会有所不同。于是想到只提取 iptables-save 里含 DROP 的行(下面 -A 及之后的部分),并在前面加上 iptables 就成了可以运行的导入命令,如
iptables -A INPUT -s 119.167.99.194/32 -j DROP
iptables -A INPUT -s 149.129.250.12/32 -j DROP
iptables -A INPUT -s 154.221.19.143/32 -j DROP
然后在启动脚本中运行上述命令即可导入。实测表明这样导出导入没有出现问题,一切运行正常。
六、最终设置
容器没有设时区,导致默认时间差着八小时。于是把宿主机的 /etc/localtime 拷为 /mypath/to/frps/etc-localtime,并映射为容器的 /etc/localtime,然后时间就正常了。容器里的 /usr/bin/frps 是 0.43 版本,把它拷为宿主机的 /mypath/to/frps/frps-0.43。/mypath/to/frps 目录下 ls -F
有如下内容(其中*为可执行文件):
banlog/ etc-localtime frps-0.43* frps.ini frps.log frptables*
frptables-config.yml iprules.txt* mystart.sh* save-iprules.cron*
其中启动脚本 mystart.sh 内容为
#!/bin/sh
/bin/sh /etc/frp/iprules.txt
/usr/sbin/crond
/etc/frp/frptables -c /etc/frp/frptables-config.yml &
/etc/frp/frps-0.43 -c /etc/frp/frps.ini
save-iprules.cron 内容如下
# map this file to /var/spool/cron/crontabs/root
# min hour day month weekday command
*/15 * * * * /sbin/iptables-save |grep DROP|sort -u|sed 's/^/iptables /' >/etc/frp/iprules.txt
最后是容器的文件映射,如下:
# 宿主机 容器装载路径
/mypath/to/frps/ /etc/frp/
/mypath/to/frps/etc-localtime /etc/localtime
/mypath/to/frps/mystart.sh /usr/bin/frps
/mypath/to/frps/save-iprules.cron /var/spool/cron/crontabs/root
最后提示
如果某个 ip 被误封禁,只能手动修改 iptables,可参考如下命令:
iptables -L --line-numbers #以编号形式列出所有规则
iptables -D INPUT 3 #删除编号为3的规则