自部署 Gitea 被爬虫打爆流量的缓解措施
现象
自部署的 Gitea 某天流量开始异常。服务器商控制面板里,流量额度以肉眼可见的速度消耗,月流量包很快见底。访问日志按响应大小聚合后,git 域名占了绝大多数流量,其中 /owner/repo/compare/ 这类版本对比页面是大头,单页最大响应能到 10MB 级别。
爬虫特征明显:Amazonbot、Lightpanda,以及一批伪造旧版 Chrome/Firefox UA 的轮换 IP,反复抓取 blame、compare 页面。大量独立 IP 各自只请求一两次,典型的分布式爬取。
原因
- 爬虫 UA 复杂,既有知名爬虫(Amazonbot、Lightpanda),也有伪造浏览器 UA 的轮换 IP。
- 重型页面单页体积大,compare 页面最大超过 10MB,且完全不压缩,爬虫每抓一页都是大流量。
- 单 IP 限流对轮换 IP 池无效,绝大多数 IP 只请求一两次就换下一个。
方法
1. 分离 HTTP 与 SSH 域名
HTTP 走 Cloudflare 代理后,SSH 没法跟着走:代理层只转发 HTTP(S) 端口,22 端口要么灰云直连,要么上付费的 Spectrum。拆成两个域名:
- git.example.com:Web 流量,DNS 开启代理(橙云),全部进 Cloudflare。
- ssh.example.com:SSH 流量,DNS 仅解析(灰云),直连服务器。
Gitea 里用 DOMAIN 和 SSH_DOMAIN 分开配置,克隆地址才能显示正确:
[server]
DOMAIN = git.example.com
SSH_DOMAIN = ssh.example.com
SSH_PORT = 222. Cloudflare WAF:重资源路径上下发交互式质询
轮换 IP 让按 IP 计数的限流全部失效,但不管 IP 怎么换,爬虫抓的都是同一批页面。把这些路径交给 Cloudflare 边缘,可疑请求直接出质询(Turnstile),过不了的进不了源站。规则建在 Cloudflare 控制台 → 域名 → 安全 → WAF → 自定义规则:
(http.request.uri.path contains "/compare/"
or http.request.uri.path contains "/archive/"
or (http.request.uri.path contains "/search"
and not starts_with(http.request.uri.path, "/repo/search")
and not starts_with(http.request.uri.path, "/api/v1/repos/search"))
or http.request.uri.path contains "/blame/")
→ 交互式质询- 路径选择:compare 要算 diff、blame 逐行追溯,都是 CPU 重活;archive 现场生成压缩包、search 走数据库查询,响应体积和开销都不小,这四条正是流量大头。
- 表达式用
contains而不用正则:matches运算符要 Business/Enterprise 套餐,免费和 Pro 只有字符串匹配可用。 - git 命令不受影响:smart-HTTP 的请求路径是
/info/refs、/git-upload-pack这类,不含上面任何一段。
contains "/search" 有个坑:它同时匹配 Gitea 前端的 XHR 端点 /repo/search 和 /api/v1/repos/search。登录后首页右侧的仓库列表靠 fetch("/repo/search?count_only=1") 拉数据,请求被质询拦成 403,fetch 拿到质询页后静默失败,列表直接显示「目前还没有仓库」,页面没有任何报错,看起来像 Gitea 出了 bug。整页导航遇到质询可以人工点过,XHR 没有这个机会。所以表达式把这两个端点排除了;给 Gitea 配路径质询前,先过一遍它前端依赖的 XHR 路径(/repo/search、/api/v1/*、/user/events)。
交互式质询对命中的请求一律要求通过 Turnstile 验证,不存在按风险放行的自适应逻辑。搜索引擎的友好爬虫走 CF 的自动校验,不受质询影响。
3. Caddy 配置
Web 流量收进 Cloudflare 后,Caddy 变成第二道防线,UA 黑名单和重型页面限流继续兜底。站点块里三条规则按顺序执行:
git.example.com {
# 日志配置:按域名输出访问日志
import log git.example.com
# 1. UA 黑名单:命中的爬虫直接 403
@block_bots header_regexp User-Agent "(?i)(SemrushBot|Lightpanda|GPTBot|ClaudeBot|Bytespider|AhrefsBot|MJ12bot|DataForSeoBot|Amazonbot|DotBot|CCBot|cohere-ai|Diffbot|Google-Extended|Meta-ExternalAgent|Meta-ExternalFetcher|Applebot-Extended|Timpibot|ImagesiftBot|Crawl4AI|Scrapy|ChatGLM-Spider|DeepSeekBot|cohere-training-data-crawler|AI2Bot|TikTokSpider|omgili|BLEXBot|Exabot|360Spider|80legs|MegaIndex|Barkrowler|DataCha0s|Nikto|Nmap|Nessus|OpenVAS|Sqlmap|WPScan|Nuclei|Masscan|Shodan|Acunetix|Dirbuster|Whatweb|Havij|Fimap|Jbrofuzz|Zgrab|Censys)"
handle @block_bots {
respond 403
}
# 2. 重型页面(compare/blame 等)单 IP 限流
@heavy path_regexp heavy_path ^/[^/]+/[^/]+/(?:compare|blame|commit|commits|archive|pulls|issues)(?:/|$)
handle @heavy {
rate_limit {
zone dynamic_zone {
key {client_ip}
events 2
window 10s
}
log_key
}
reverse_proxy <internal-ip>:3000
}
# 3. robots.txt:礼貌爬虫先读规则
handle /robots.txt {
respond `User-agent: *
Disallow: /blame/
Disallow: /compare/
Disallow: /commit/
Disallow: /commits/
Disallow: /archive/
Disallow: /pulls/
Disallow: /issues/`
}
# 4. 其余请求反代到 Gitea
reverse_proxy <internal-ip>:3000
}4. Gitea 开启源站 gzip
改 app.ini 的 [server] 段,加一行后重启 Gitea:
[server]
ENABLE_GZIP = trueENABLE_GZIP 只压缩运行时生成的内容,静态资源不受影响。放在源站而不是反代层,是因为链路是「源站 → 反代 → 访客」两段,双向计费下源站压缩两段都省;只在反代压只省一段。Caddy 的 reverse_proxy 会替缺省客户端补 Accept-Encoding: gzip 并原样透传上游的压缩响应,不需要在 Caddy 侧再配 encode。