# 自部署 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` 分开配置，克隆地址才能显示正确：

```ini
[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 黑名单和重型页面限流继续兜底。站点块里三条规则按顺序执行：

```caddyfile
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：

```ini
[server]
ENABLE_GZIP = true
```

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

## 参考链接

- [caddy-ratelimit](https://github.com/mholt/caddy-ratelimit)
- [Gitea 配置手册（SSH_DOMAIN、ENABLE_GZIP）](https://docs.gitea.com/administration/config-cheat-sheet)
- [Caddy handle 指令](https://caddyserver.com/docs/caddyfile/directives/handle)
- [Cloudflare 支持的端口（22 端口需灰云或 Spectrum）](https://developers.cloudflare.com/fundamentals/reference/network-ports/)


---

> 作者: Nite  
> URL: https://www.nite07.com/zh-cn/posts/gitea-scraper-mitigation/  

