Go HTTP Server默认不防Slowloris,必须手动设置ReadTimeout、WriteTimeout和IdleTimeout三个超时参数;仅用http.TimeoutHandler无效,需在连接层限流并调优系统资源。
Go HTTP server 默认不防 Slowloris,必须手动设超时
go 的 net/http.server 默认对连接和请求都没有硬性超时限制。一个只发 get / http/1.1\r\nhost: 就停住的 slowloris 连接,会一直卡在 readrequest 阶段,持续占用 goroutine 和文件描述符,直到客户端断开或系统 kill。这不是 bug,是设计选择:go 把超时策略交给使用者决定。
关键参数只有三个,漏掉任一个都可能被耗尽连接:
-
ReadTimeout:从连接建立到读完完整请求头(含 body)的总时间,**必须设**,建议 ≤ 5s -
WriteTimeout:从开始写响应到写完的总时间,防止大响应阻塞,建议 ≤ 10s -
IdleTimeout:连接空闲(无读写)的最大持续时间,**最常被忽略**,建议 ≤ 30s
示例:
srv := &http.Server{
Addr: ":8080",
Handler: myHandler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
用 http.TimeoutHandler 包裹 handler 不解决根本问题
http.TimeoutHandler 只控制 handler 执行时间,对「连接已建、请求头没读完」的 Slowloris 完全无效。它在 Server.Serve 后才介入,而 Slowloris 卡在之前。
常见错误写法:
http.Handle("/", http.TimeoutHandler(myHandler, 3*time.Second, "timeout"))
这只会让 handler 超时,但那个恶意连接仍挂在 net.Listener 上,持续消耗资源。真正要拦截的位置在连接建立后的读阶段,只能靠 ReadTimeout 和 IdleTimeout。
高并发下 ulimit 和 goroutine 泄露风险
即使设了超时,若攻击连接数远超预期,仍可能触发系统级瓶颈:
- Linux 默认
ulimit -n是 1024,net/http.Server每连接至少占 1 个 fd,超限后新连接直接被内核拒绝(accept: too many open files) - goroutine 泄露:如果 handler 内部启动协程但没随请求上下文取消,超时后 goroutine 仍运行,内存缓慢增长
- 日志打满磁盘:慢速连接频繁超时,若每个都
log.Printf,I/O 成瓶颈
实操建议:
- 启动前执行
ulimit -n 65536(需 root 或配置/etc/security/limits.conf) - handler 内所有异步操作必须接收
req.Context()并监听取消信号 - 关闭默认日志或重定向到缓冲 writer,避免阻塞主循环
想彻底防住?得加连接层限流
超时只是兜底,不能替代连接准入控制。Go 原生不提供 per-IP 连接数限制,需自行封装 net.Listener:
核心思路:在 Accept() 后立即检查来源 IP 的当前连接数,超限则 conn.Close() 并丢弃。
要点:
- 用
sync.Map计数,避免锁竞争 - 计数器需绑定
conn.Close(),推荐用net.Conn.SetDeadline触发清理 - 阈值建议 ≤ 50(单 IP),过高失去意义,过低误伤正常用户
注意:这个逻辑必须放在 http.Server.Serve() 之前,否则已进入 HTTP 解析流程,再拒绝就晚了。
最后提醒一句:Slowloris 攻击流量特征极淡——每秒几个字节,TCP 连接状态正常,Wireshark 看不出异常。靠日志或带宽监控根本发现不了,唯一可靠指标是服务器 netstat -an | grep :80 | wc -l 持续接近 ulimit -n 设置值。


















