在网络安全测试和信息收集中,端口扫描、目录扫描、漏洞探测等操作往往需要对目标发起大量请求。如果没有合理的限流控制,不仅会导致网络带宽、系统资源占用过高,还可能触发防火墙/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就可以了。