在网络安全测试和信息收集中,端口扫描、目录扫描、漏洞探测等操作往往需要对目标发起大量请求。如果没有合理的限流控制,不仅会导致网络带宽、系统资源占用过高,还可能触发防火墙/IDS 的检测与阻断。为了在高效与隐蔽之间取得平衡,限流速制就显得尤为重要。其中,令牌桶(Token Bucket)是一种常见的限流算法,常常被用来做在网关出做流量限制,其实也可以使用在扫描器中。
令牌桶算法原理
令牌桶的基本思想:
- 系统以固定速率往桶里放入令牌
- 桶中最多存放一定数量的令牌
- 每次请求前必须先取一个令牌
- 如果桶中没有令牌,请求要么等待,要么被丢弃
这样可以实现平滑限流,既能保证整体速率受控,又允许短时间的突发流量。
flowchart TD
A[开始请求] --> B{桶中是否有令牌?}
B -- 否 --> C[等待或丢弃请求]
B -- 是 --> D[取出一个令牌]
D --> E[请求被执行]
E --> F[结束]
subgraph TokenBucket[令牌桶机制]
T1[以固定速率加入令牌]
T2[桶最大容量限制]
T1 --> T2
T2 --> B
end
在扫描器中的应用场景
1. 端口扫描限速
- 避免因短时间大量探测包被目标防火墙识别为拒绝服务攻击
- 保持较低但稳定的速率,提高扫描成功率
2. 目录/URL 爆破限速
- 避免对 Web 服务造成明显负载
- 保证每秒固定数量请求,提高检测的隐蔽性
3. 多线程调度控制
- 扫描器常采用多线程并发
- 令牌桶算法可作为全局调度器,保证所有线程总速率不超过设定阈值
ProjectDiscovery扫描器中的实践
ProjectDiscovery的一些列扫描器中的限流算法,实际上是自己实现的令牌桶限流算法,针对经典的令牌桶做了一些优化。see:https://github.com/projectdiscovery/ratelimit。 下面是golang官方实现的经典的令牌桶:
// Every converts a minimum time interval between events to a Limit.
func Every(interval time.Duration) Limit {
if interval <= 0 {
return Inf
}
return 1 / Limit(interval.Seconds())
}
ProjectDiscovery 中的实现如下:
func (limiter *Limiter) run(ctx context.Context) {
defer close(limiter.tokens)
for {
if limiter.count.Load() == 0 {
<-limiter.ticker.C
limiter.count.Store(limiter.maxCount.Load())
}
...
...
可以看到,官方的实现非常平滑。而ProjectDiscovery中的实现是,为了能够应对突发大量请求做了优化。
题外话:masscan的限速原理
masscan以高速扫描著称,但是同样是有限速的参数的。masscan限速使用的发包间隔控制。因为masscan是一个线程只负责发包。因此,只需要限制发包间隔为1/rate就可以了。