本文来自依云's Blog,转载请注明。
本来我是用 iptables 来屏蔽恶意IP地址的。之所以不使用 ipset,是因为我不想永久屏蔽这些 IP。iptables 规则有命中计数,所以我可以根据最近是否命中来删除「已经变得正常、或者分配给了正常人使用」的 IP。但 iptables 规则有个问题是,它是 O(n) 的时间复杂度。对于反 spam 来说,几千上万条规则问题不大,而且很多 spam 来源是机房的固定 IP。但是以文件下载为主、要反刷下行流量的用途,一万条规则能把下载速率限制在 12MiB/s 左右,整个 CPU 核的时间都消耗在 softirq 上了。perf top 一看,时间都消耗在 ipt_do_table 函数里了。
行吧,临时先加补丁先:
iptables -I INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
这样让已建立的连接跳过后边上万条规则,就可以让正常的下载速度快起来了。
此时性能已经够用了。但是呢,还是时不时需要我手动操作一下,删除计数为零的规则、清零计数、合并恶意 IP 太多的网段。倒不是这些工作自动化起来有困难(好吧,让我用 Python 3.3 来实现可能是有些不便以至于至今我都没有动手),但是这台服务器上有新工具 nftables 可用,为什么不趁机试试看呢?
于是再次读了读 nft 的手册页,意外地发现,它竟然有个东西十分契合我的需求:它的 set 支持超时!于是开虚拟机对着文档调了半天规则,最终得到如下规则定义:
destroy table inet blocker table inet blocker { set spam_ips { type ipv4_addr timeout 2d flags timeout, dynamic } set spam_ips6 { type ipv6_addr timeout 2d flags timeout, dynamic } chain input { type filter hook input priority 0; policy accept; ct state established,related accept ip saddr @spam_ips tcp dport { 80, 443 } update @spam_ips { ip saddr timeout 2d } drop ip6 saddr @spam_ips6 tcp dport { 80, 443 } update @spam_ips6 { ip6 saddr timeout 2d } drop } }
nftables 是自己创建 table 的,不用和别人「共用一张桌子然后打架」啦。然后定义了两个动态的、支持超时的、默认超时时间是两天的 set。nftables 的 table 可以同时支持 IPv4 和 IPv6,但是规则和 set 不行,所以得写两份。在 chain 定义中设置 hook,就跟 iptables 的默认 chain 一样可以拿到包啦。然后,已建立的连接不用检查了,因为恶意 IP 还没学会连接复用。接下来,如果源 IP 位于 set 内并且是访问 HTTP(S) 的话,就更新 set 的超时时间,然后丢弃包。限制端口是为了避免万一哪天把自己给屏蔽掉了。nftables 的规则后边可以写多个操作,挺直观、易于理解的。
然后让自己的恶意 IP 识别脚本用 nft add element inet blocker spam_ips "{ $IP }"
这样的命令向 set 里添加要屏蔽的 IP 就可以啦。两天不再有请求过来的 IP 会被自动解除屏蔽,很适合国内的三大运营商的动态 IP 呢。
跑了几天,被屏蔽的 IP 数量稳定在 26k—28k 之间。有昼夜周期,凌晨零点多和早上六七点是爆发期,晚间是静默期。性能非常好,softirq 最高占用不到 10%。
nftables 也很好用。虽然 nft 的手册页有点难懂,多看几遍、了解其写作结构之后就好很多了。不过要是支持 IP 地址到 counter 的动态 map 就好了——我想统计各 IP 的流量。nftables 还自带 Python 绑定,虽说这 API 走 JSON 感觉怪怪的,libnftables-json(5) 这文档没有超链接也很难使用,但至少弄明白之后能用。我用来写了个简单的统计脚本:
#!/usr/bin/python3 import os from math import log10 from itertools import groupby import nftables def show_set(nft, name): ret, r, error = nft.json_cmd({'nftables': [{'list': {'set': {'family': 'inet', 'table': 'blocker', 'name': name}}}]}) if ret != 0: raise Exception(ret, error) try: elements = r['nftables'][1]['set']['elem'] except KeyError: # empty set return ips = [(x['elem']['val'], x['elem']['expires']) for x in elements] ips.sort(key=lambda x: x[1]) histo = [] total = len(ips) for k, g in groupby(ips, key=lambda x: x[1] // 3600): count = sum(1 for _ in g) histo.append((k, count)) max_count = max(x[1] for x in histo) w_count = int(log10(max_count)) + 1 w = os.get_terminal_size().columns - 5 - w_count count_per_char = max_count / w # count_per_char = total / w print(f'>> Histogram for {name} (total {total}) <<') for hour, count in histo: print(f'{hour:2}: {f'{{:{w_count}}}'.format(count)} {'*' * int(round(count / count_per_char))}') print() if __name__ == '__main__': nft = nftables.Nftables() show_set(nft, 'spam_ips6') show_set(nft, 'spam_ips')
最后,我本来想谴责用无辜开源设施来刷下行流量的行为的,但俗话说「人为财死」,算了。还是谴责一下运营商不顾社会责任、为了私利将压力转嫁给无辜群众好了。自私又短视的人类啊,总有一天会将互联网上的所有好东西都逼死,最后谁也得不到好处。
Aug 28, 2024 09:27:49 AM
py 脚本是否多余?https://wiki.nftables.org/wiki-nftables/index.php/Rate_limiting_matchings
Aug 28, 2024 09:31:16 AM
https://wiki.archlinux.org/title/Nftables#Dynamic_blackhole
Aug 28, 2024 09:52:49 AM
我自己屏蔽 IP 是通过 BGP 实现的,比较好进行多台机器的分发,将恶意 IP 的下一跳指向一个黑洞地址,目前屏蔽了 7w 个 IP,数据源是从网上拉的恶意 IP 列表,不过确实我这个就不好统计屏蔽次数了
Aug 28, 2024 10:21:14 AM
这种脚本有没有试过用 claude 写,我一般会用
```
请参考下面的文档,编写一个脚本,脚本需要支持 blah blah
> man page 的内容
> man page 的内容
> man page 的内容
```
这样基本不用自己写脚本了
Aug 28, 2024 10:48:16 AM
我搞不到 claude 的账号。不过我有打算往 nftables 上迁移,还是自己读文档比较好啦。
Aug 28, 2024 10:50:12 AM
这个是频率限制啊,我这个需求用不了的。
Aug 28, 2024 11:00:47 AM
可以用 www.poe.com, 每天有一定免费额度。
Aug 28, 2024 11:10:02 AM
那看你怎么定义恶意 ip
Aug 28, 2024 11:18:40 AM
翻了日志,发现攻击者有同机房多个 ip(例如 x.x.x.1-x.x.x.60,有钱不理解还攻击我小破机)。我该怎么操作 nftables,屏蔽网段而不是单独 ip。sshguard 有这个功能,但只限 ssh 端口。
Aug 28, 2024 12:26:09 PM
nft看起来真不错啊,也考虑把ipset改过来
Aug 28, 2024 02:31:32 PM
对于 nftables 来说, firewalld 支持不是很好(至少是部分 lts 发行版). --direct 更倾向于对 iptables 的支持. 部分 nft 的特性, 不能被转义.
对于子网 SETS 的 flags 类型为 interval, 可能存在 bug,
比如对于 IP 10.A.B.C 属于子网 10.11.0.0/13, 希望添加到指定的 SETS 里, 而 SETS 已经有 {10.11.0.0/14, 10.20.0.0-10.25.0.0}, 使用 nft get element... 会提示该子网已存在, 实际
是不存在的, 这时可以强制执行 nft add element... 是会成功, 然后再用 nft get element... 查看, 会发现 range 被重整了, 这时该 IP 才被包含.
Aug 28, 2024 05:24:13 PM
我不用 firewalld 倒是不会受影响。interval 原来还有这种问题啊,幸好我还没用上。
Aug 28, 2024 05:26:42 PM
是另外的脚本来判断的。
Aug 30, 2024 03:37:43 PM
看到这篇文章后决定学习下 nftables, 尝试用 nft --check 这段脚本时一直报错, 稍后才意识到 destory 命令是 nftables 1.0.7 才加入的新指令, 而 Debian 12 上的 nftables 是 1.0.6
https://www.netfilter.org/projects/nftables/files/changes-nftables-1.0.7.txt
Aug 31, 2024 10:47:33 AM
啊,我都在用 1.1.0 了!
Sep 06, 2024 07:44:31 PM
可以从淘宝^1或者你的朋友^2那里弄一张英国的 giffgaff 卡, 保号套餐很便宜, 如此以来注册外网账号就简单了.
^1: 可以从官网申请, 但是需要你有国际邮寄地址, 这个有些麻烦
^2: 你的朋友一定有做 giffgaff 推广的, 他推广出去一张后, 你和彵都可以得到 5$ 话费. 我自己花了大力气弄了国际地址后才知道做这种推广的人很多
Sep 10, 2024 09:47:02 AM
nft add element … 添加的 CIDR 子网长度是 /32,例如 10.1.2.3/32。添加可以,但是删除会报错,称找不到该地址。需要转出为文本后,手动删除,然后导入。
Oct 07, 2024 09:34:03 AM
ipset本身就支持timeout啊
Oct 07, 2024 10:25:36 AM
诶真的耶,为什么我的印象里没有呢……
Oct 07, 2024 04:12:24 PM
然后:
$ ipset
The program 'ipset' is currently not installed. To run 'ipset' please ask your administrator to install the package 'ipset'
QAQ
Oct 07, 2024 04:17:24 PM
ipset 编译安装还需要对应的内核源码,这内核是 VPS 供应商提供的,时常升级,就算勉强安装上了也根本跟不过来。
Oct 10, 2024 09:30:09 PM
github上的这个项目用iptable + ipset 实现了类似的功能?
只不过这个是屏蔽端口扫描的
https://github.com/EtherDream/anti-portscan
Oct 10, 2024 10:55:06 PM
是啊。
Nov 09, 2024 09:58:34 PM
我现在还是很懒的在用ipset配iptables。