“端口限速”的方法,以限制流量中转的速率,通常是使用iptables
与tc
结合,使用iptables
做路由控制,使用tc
做流量整型。这种方法复杂且难以维护。
而高效、方便且灵活(远不只是端口限速)的限速方式,可以使用iptables
的hashlimit
模块。
什么是hashlimit
hashlimit
是iptables
的一个拓展模块,其本身是一个匹配模块,不具有其他功能,需要结合其他模块来完成复杂任务。
什么是令牌桶
hashlimit
使用的算法叫做令牌桶算法。
令牌桶(token bucket),顾名思义,可以理解为有一个桶,里面装满了令牌(token)。当请求来到时,可理解为人群来到,只有拿到令牌的人才能参加活动(例如购买某种物品)。令牌桶的容量是一个固定值,且令牌桶也会在不满时以一定速率生成新的令牌,令牌的生成速率也是一个固定值。
所以,例如使用一个令牌桶来限制人群购买商品的速率,例如假设令牌桶的容量是50,生成速率是每秒100个,人群的购买欲望是无穷的(尽可能达到大的购买速率)。于是,在开放购买的瞬间,人群瞬间抢完了50个令牌(突发速率),而之后人群以每秒100人的速率购买,正好与令牌生成速度相同(限制的最高速率),同时令牌桶容量一直为0,这样一来就成功将人群购买速率限制为令牌生成速率。
实操
看过了上面什么是令牌桶的解释,相信你也意识到了,令牌桶生成速率就是我们限制的网络速率。
下面以一个简单的场景来讲解:
A(一个多个不同IP的客户端)→ B(流量中转机器)→ C(流量目标机器)
假设A、B、C的IP地址分别为1.1.1.1
,2.2.2.2
,3.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
,所以令牌桶只有一个。模式有:srcip
、srcport
、dstip
、dstport
。--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-extensions。ctrl+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文件, 可以看到当令牌不够时, 匹配这个域名后的包确实丢掉了.