Go 1.28 的 Range 请求超过 200 段就整段忽略:断点续传与分片下载会退回整包
Go 1.28 改了 net/http 里处理 Range 请求的一组入口:FileServer、FileServerFS、ServeContent、ServeFile、ServeFileFS。当请求头里的 Range 段数超过 httpservecontentmaxranges 时,整个 Range 头被忽略,服务器按普通请求返回 200 和完整 Content-Length,而不是 206 Partial Content。默认值是 httpservecontentmaxranges=200,设为 0 关闭限制。该设置和默认值已回溯到 Go 1.27.2 与 Go 1.26.9。
限制的是什么
限制对象是 Range 头里的段数,不是单段字节范围的大小。一个请求写 bytes=0-99 是一段;写 bytes=0-99,200-299,400-499 是三段。超过 200 段(例如 201 段)时,net/http 不返回 416、不返回 400,也不截断到前 200 段,而是直接忽略整个 Range 头。
设 httpservecontentmaxranges=0 恢复旧行为,即不限制段数。
为什么这是 DoS 缓解
多段 Range 的成本在服务端,不在请求体。一个很小的请求头可以带几千段范围,服务端要逐段解析、校验、排序合并,再为每段定位文件偏移、写出边界和头部。响应体变成 multipart/byteranges,内存占用和 CPU 时间随段数增长,而客户端发的字节数几乎不变。这是典型的放大面:攻击者用低成本请求换服务端高成本响应。
上限把单次请求的处理成本截断在 200 段,超过就退回整包。整包响应虽然字节多,但服务端路径简单,不会因为段数膨胀而消耗额外解析和拼接资源。
表现:客户端看到 200,而不是报错
忽略 Range 头的结果是“看起来正常”的 200:
- 响应状态行是
HTTP/1.1 200 OK; Content-Length是文件全量大小,不是分片长度;- 没有
Content-Range,也没有multipart/byteranges。
对下载器来说,这比报错更隐蔽。断点续传逻辑通常发 Range: bytes=- 期望 206,拿到 200 后有的实现会从头覆盖写,有的会认为服务端不支持 Range 而放弃续传,最终同一个文件被整包重复下载。分片下载器如果一次拼接大量小段,超过上限后每一段请求都退化成整包,流量和耗时都会上去。
怎么判断自己中招
几个可观察信号:
- 访问日志里同一个文件在短时间内被多次整包下载,响应字节数等于文件大小,而不是请求的范围长度;
- CDN 回源流量上升,源站看到的分片请求变成全量请求;
- 日志或监控里
206的比例下降,200的比例上升,尤其是大文件路径; - 下载类客户端反馈“续传不生效”“每次都从头下”。
这些信号同时出现时,先确认客户端是否在单个 Range 头里塞了超过 200 段。
验证命令
先起一个静态文件服务:
package main
import (
"log"
"net/http"
)
func main() {
// Go 1.28 默认 httpservecontentmaxranges=200
// 超过 200 段的 Range 头会被整段忽略,返回 200 全量
log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("./static"))))
}
单段请求应当是 206:
curl -s -o /dev/null -D - -r 0-99 http://127.0.0.1:8080/big.bin
构造 201 段请求,观察是否退回 200:
RANGES=$(python3 - <<'EOF'
print(",".join(f"{i*100}-{i*100+9}" for i in range(201)))
EOF
)
curl -s -o /dev/null -D - -H "Range: bytes=$RANGES" http://127.0.0.1:8080/big.bin
只看状态码:
curl -s -o /dev/null -w '%{http_code}\n' -H "Range: bytes=$RANGES" http://127.0.0.1:8080/big.bin
抓包确认响应行是 200 还是 206:
sudo tcpdump -i any -A -s 0 'tcp port 8080' | grep -E 'HTTP/1\.[01] (200|206)'
把段数降到 200 以内再跑一次,状态码应回到 206,这样可以排除文件不存在、路径错误等干扰。
设置方式与取舍
httpservecontentmaxranges 是 GODEBUG 设置,三种固定方式:
环境变量:
GODEBUG=httpservecontentmaxranges=0 ./server
go.mod 的 godebug 块:
godebug (
httpservecontentmaxranges=0
)
或源码里的 //go:debug 指令:
//go:debug httpservecontentmaxranges=0
package main
设为 0 关闭限制,适合内网、客户端可信、或确实需要单请求大量分片的场景。代价是重新暴露多段 Range 的放大面:任何能发请求的客户端都可以用几千段的范围头推高服务端 CPU 和内存。公网文件服务保留默认 200 更稳妥,需要大量分片的客户端改成分多次请求,每次控制在 200 段以内。
回溯意味着升级到 Go 1.27.2 或 1.26.9 也会带上这个限制,不需要改代码,但行为会变。观察是否走到非默认行为,可以读 runtime/metrics 里的 /godebug/non-default-behavior/httpservecontentmaxranges:events 计数器;显式设置过该值时,这个计数器会记录事件。