自部署 Gitea 被爬虫打爆流量的缓解措施

目录

现象

自部署的 Gitea 某天流量开始异常。服务器商控制面板里,流量额度以肉眼可见的速度消耗,月流量包很快见底。访问日志按响应大小聚合后,git 域名占了绝大多数流量,其中 /owner/repo/compare/ 这类版本对比页面是大头,单页最大响应能到 10MB 级别。

爬虫特征明显:Amazonbot、Lightpanda,以及一批伪造旧版 Chrome/Firefox UA 的轮换 IP,反复抓取 blame、compare 页面。大量独立 IP 各自只请求一两次,典型的分布式爬取。

原因

  1. 爬虫 UA 复杂,既有知名爬虫(Amazonbot、Lightpanda),也有伪造浏览器 UA 的轮换 IP。
  2. 重型页面单页体积大,compare 页面最大超过 10MB,且完全不压缩,爬虫每抓一页都是大流量。
  3. 单 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 = 22

2. 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 = true

ENABLE_GZIP 只压缩运行时生成的内容,静态资源不受影响。放在源站而不是反代层,是因为链路是「源站 → 反代 → 访客」两段,双向计费下源站压缩两段都省;只在反代压只省一段。Caddy 的 reverse_proxy 会替缺省客户端补 Accept-Encoding: gzip 并原样透传上游的压缩响应,不需要在 Caddy 侧再配 encode。

参考链接

编辑此页

目录