iptables利用hashlimit限速

“端口限速”的方法,以限制流量中转的速率,通常是使用iptablestc结合,使用iptables做路由控制,使用tc做流量整型。这种方法复杂且难以维护。

而高效、方便且灵活(远不只是端口限速)的限速方式,可以使用iptableshashlimit模块。

什么是hashlimit

hashlimitiptables的一个拓展模块,其本身是一个匹配模块,不具有其他功能,需要结合其他模块来完成复杂任务。

什么是令牌桶

hashlimit使用的算法叫做令牌桶算法。

令牌桶(token bucket),顾名思义,可以理解为有一个桶,里面装满了令牌(token)。当请求来到时,可理解为人群来到,只有拿到令牌的人才能参加活动(例如购买某种物品)。令牌桶的容量是一个固定值,且令牌桶也会在不满时以一定速率生成新的令牌,令牌的生成速率也是一个固定值

所以,例如使用一个令牌桶来限制人群购买商品的速率,例如假设令牌桶的容量是50,生成速率是每秒100个,人群的购买欲望是无穷的(尽可能达到大的购买速率)。于是,在开放购买的瞬间,人群瞬间抢完了50个令牌(突发速率),而之后人群以每秒100人的速率购买,正好与令牌生成速度相同(限制的最高速率),同时令牌桶容量一直为0,这样一来就成功将人群购买速率限制为令牌生成速率。

实操

看过了上面什么是令牌桶的解释,相信你也意识到了,令牌桶生成速率就是我们限制的网络速率。

下面以一个简单的场景来讲解:

A(一个多个不同IP的客户端)→ B(流量中转机器)→ C(流量目标机器)

假设A、B、C的IP地址分别为1.1.1.12.2.2.23.3.3.3

在此场景中,要求限制A通过B的网络速率的总合为上下对等100Mbps。(无论多用户还是单用户,多线程还是单线程,总速率限制为100Mbps)

在B上操作

限制C→B→A速率

iptables -A INPUT -p tcp -s 3.3.3.3 -m hashlimit \
–hashlimit-name limitDownLink \
–hashlimit-upto 12500kb/s \
–hashlimit-mode srcip \
–hashlimit-burst 12500kb \
-j ACCEPT
iptables -A INPUT -p tcp -s 3.3.3.3 -j DROP

部分解释:

  • --hashlimit-name 不仅是个“名字”,他是令牌桶独一无二的标志,如果两条命令用了一个名字,他们将共享这个令牌桶(可以知道速率也会共享),所以请保证使用独一无二的名字。
  • --hashlimit-upto 指定了令牌生成速率。(这里kb/s是kByte/s)
  • --hashlimit-mode 是匹配模式,这里指定srcip模式则是每个源IP一个令牌桶,因为这里源IP只有C的3.3.3.3,所以令牌桶只有一个。模式有:srcipsrcportdstipdstport
  • --hashlimit-burst 指定了令牌桶的大小(iptables规定了令牌桶大小必须大于等于生成速率(设置的不对iptables会告诉你最小值,最小值也不一定是生成速率))。
  • 为什么要两条命令?被第一条命令匹配成功的数据报(拿到令牌的)被ACCEPT,其余的数据报被第二条匹配而丢弃。如果没有第二条规则,则数据包被INPUT链默认策略匹配(绝大多数情况以及默认情况下是ACCEPT),故而根本没有起到限速作用。

限制A→B→C速率

iptables -A OUTPUT -p tcp -d 3.3.3.3 -m hashlimit \
–hashlimit-name limitUpLink \
–hashlimit-upto 12500kb/s \
–hashlimit-mode dstip \
–hashlimit-burst 12500kb \
-j ACCEPT
iptables -A OUTUT -p tcp -d 3.3.3.3 -j DROP

    其他

    • 灵活的限速参照实操部分的例子,你可以灵活的设置自己的规则,例如加入匹配端口等其他匹配规则
    • 更多设置关于hashlimit的更多设置可以参考文档:iptables-extensionsctrl+f搜索hashlimit即可。

    某机器有一条防DNS攻击的规则:

    iptables -t raw -I dns_limit -m string --algo bm --icase \
        --hex-string "|${hex_domain}|"                       \
        -m hashlimit                                         \
        --hashlimit-name DNS                                 \
        --hashlimit-mode srcip                               \
        --hashlimit-above 1/second                           \
        --hashlimit-burst 1                                  \
        --hashlimit-htable-max 1000000                       \
        --hashlimit-htable-expire 180000                     \
        --hashlimit-htable-gcinterval 30000                  \
        --hashlimit-srcmask 28                               \
        -m comment --comment "${domain}" -j DROP

    当时机器上测试dig查询, 发现某个域名被完全封禁了, 而不是预想中的限速.

    查看iptables的规则链dns_limit, 发现这个域名有两条这样的规则, 删除一条后则和预想一致, 实现了限速.

    先说下最主要的, hashlimit 模块的核心是令牌桶算法(Token Bucket), 这个模块的作用是匹配, 限速是根据匹配结果以及target操作而实现的功能. 当时了解到这个后, 问题就迎刃而解了.

    几个参数:

    • --hashlimit-name: 定义这条hashlimit规则的名称, 所有的条目(entry)都存放在/proc/net/ipt_hashlimit/{hashlimit-name}
    • --hashlimit-mode: 限制的类型,可以是源地址/源端口/目标地址/目标端口
    • --hashlimit-srcmask: 当mode设置为srcip时, 配置相应的掩码表示一个网段
    • --hashlimit-above: mount/quantum, 允许进来的包速率(令牌恢复速率)
    • --hashlimit-burst: 允许突发的个数(其实就是令牌桶最大容量)
    • --hashlimit-htable-max: hash的最大条目数
    • --hashlimit-htable-expire: hash规则失效时间, 单位毫秒(milliseconds)
    • --hashlimit-htable-gcinterval: 垃圾回收器回收的间隔时间, 单位毫秒

    上面是man手册比较正式的解释.

    关于 expire 和 gcinterval, 如果在这个时间内没有再次触发规则, 则时间逐渐减为0, 进而负数, 但是并不会从hash中删除, 直到垃圾回收器执行后, 才会删除.

    gcinterval 一般设置会比 expire 小, 这个值应配合 expire 选取合适值, 太小会导致频繁占用资源, 太大会导致封禁条目达到失效时间后还需要等待很久才会被删除.

    失效时间到达后未被删除, 还是会被封禁.

    查看 /proc/net/ipt_hashlimit/DNS 文件:

    $ cat /proc/net/ipt_hashlimit/DNS180 X.X.X.X:0->0.0.0.0:0 32000 32000 32000

    这里第一个字段是expire倒计时时间(单位是秒), 比如这里设置180000毫秒, 即180s, 如果180s内没有再次触发这个规则, 则会一直减到0 (见上面关于expire解释); 如果触发则再次变为180.

    第二个字段是 srcip:port->dstip:port, 这里mode只设置了srcip

    第三个字段是当前剩余的令牌数

    第四个字段是令牌桶最大容量, 是一个定值

    第五个字段是一次触发使用的令牌数, 也是令牌产生速率, 也是一个定值

    一秒(second)有32000个令牌(TODO 这里没有找到相关说明, 源码也没翻到… 猜测应该是每jiffy(毫秒) 32个令牌), 如果限制是 1req/sec, 则令牌产生速率是 32000/1 = 32000, 如果是 2req/sec, 则第五个字段就是 32000/2 = 16000.

    而最大的令牌数就是 令牌产生速率 * {hashlimit-burst}, 比如 2req/sec, burst是5, 则第四个字段就是 32000/2*5 = 80000

    第三个字段每触发一次规则, 都会减去 令牌产生速率 * 1个令牌, 并以这个速率恢复. 如果长时间没有触发, 会一直处于和最大令牌数一样的值.

    关于hashlimit的匹配结果: 当查询包进来时, 如果令牌足够, 则会减去一次令牌数, 接着恢复, 且接着去下一条规则; 如果在剩余的令牌不足以减去一次查询的令牌, 则匹配这条hashlimit规则, target是DROP时, 则丢弃这个包.

    模拟DNS攻击, 查看第三个字段的值, 发现两条规则时, 就是减少两次令牌, 因为一次会减少32000个令牌, 两次减少64000个, 而令牌桶的最大数目是32000, 也就是说这是一个永远无法完成的操作, 当然也就会造成一种完全封禁的情况.

    实验测试中, 比如把速率改为 2seq/sec, burst改为3, 一遍dig一遍抓包并查看/proc/net/ipt_hashlimit/DNS文件, 可以看到当令牌不够时, 匹配这个域名后的包确实丢掉了.

    关于Zeno Chen

    本人涉及的领域较多,杂而不精 程序设计语言: Perl, Java, PHP, Python; 数据库系统: MySQL,Oracle; 偶尔做做电路板的开发,主攻STM32单片机
    此条目发表在Linux分类目录。将固定链接加入收藏夹。