8
27
2024
17

使用 nftables 屏蔽大量 IP

本文来自依云'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')

最后,我本来想谴责用无辜开源设施来刷下行流量的行为的,但俗话说「人为财死」,算了。还是谴责一下运营商不顾社会责任、为了私利将压力转嫁给无辜群众好了。自私又短视的人类啊,总有一天会将互联网上的所有好东西都逼死,最后谁也得不到好处。

Category: Linux | Tags: linux 网络 iptables 中国特色 nftables | Read Count: 2123
reader 说:
Aug 28, 2024 09:27:49 AM

py 脚本是否多余?https://wiki.nftables.org/wiki-nftables/index.php/Rate_limiting_matchings

reader 说:
Aug 28, 2024 09:31:16 AM

https://wiki.archlinux.org/title/Nftables#Dynamic_blackhole

imlonghao 说:
Aug 28, 2024 09:52:49 AM

我自己屏蔽 IP 是通过 BGP 实现的,比较好进行多台机器的分发,将恶意 IP 的下一跳指向一个黑洞地址,目前屏蔽了 7w 个 IP,数据源是从网上拉的恶意 IP 列表,不过确实我这个就不好统计屏蔽次数了

MatheMatrix 说:
Aug 28, 2024 10:21:14 AM

这种脚本有没有试过用 claude 写,我一般会用

```
请参考下面的文档,编写一个脚本,脚本需要支持 blah blah

> man page 的内容
> man page 的内容
> man page 的内容
```

这样基本不用自己写脚本了

Avatar_small
依云 说:
Aug 28, 2024 10:48:16 AM

我搞不到 claude 的账号。不过我有打算往 nftables 上迁移,还是自己读文档比较好啦。

Avatar_small
依云 说:
Aug 28, 2024 10:50:12 AM

这个是频率限制啊,我这个需求用不了的。

jiyin 说:
Aug 28, 2024 11:00:47 AM

可以用 www.poe.com, 每天有一定免费额度。

reader 说:
Aug 28, 2024 11:10:02 AM

那看你怎么定义恶意 ip

reader 说:
Aug 28, 2024 11:18:40 AM

翻了日志,发现攻击者有同机房多个 ip(例如 x.x.x.1-x.x.x.60,有钱不理解还攻击我小破机)。我该怎么操作 nftables,屏蔽网段而不是单独 ip。sshguard 有这个功能,但只限 ssh 端口。

druggo 说:
Aug 28, 2024 12:26:09 PM

nft看起来真不错啊,也考虑把ipset改过来

nft 说:
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 才被包含.

Avatar_small
依云 说:
Aug 28, 2024 05:24:13 PM

我不用 firewalld 倒是不会受影响。interval 原来还有这种问题啊,幸好我还没用上。

Avatar_small
依云 说:
Aug 28, 2024 05:26:42 PM

是另外的脚本来判断的。

#ak1ra 说:
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

Avatar_small
依云 说:
Aug 31, 2024 10:47:33 AM

啊,我都在用 1.1.0 了!

myuan 说:
Sep 06, 2024 07:44:31 PM

可以从淘宝^1或者你的朋友^2那里弄一张英国的 giffgaff 卡, 保号套餐很便宜, 如此以来注册外网账号就简单了.

^1: 可以从官网申请, 但是需要你有国际邮寄地址, 这个有些麻烦

^2: 你的朋友一定有做 giffgaff 推广的, 他推广出去一张后, 你和彵都可以得到 5$ 话费. 我自己花了大力气弄了国际地址后才知道做这种推广的人很多

nft 说:
Sep 10, 2024 09:47:02 AM

nft add element … 添加的 CIDR 子网长度是 /32,例如 10.1.2.3/32。添加可以,但是删除会报错,称找不到该地址。需要转出为文本后,手动删除,然后导入。


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com