基于白名单的 GitHub / npm 反向代理服务,专为 Tencent EO 全站加速优化。
- 白名单控制 - 仅代理已配置的上游资源,防止滥用
- 多路由支持 - GitHub Releases、Raw 文件、头像、npm/unpkg、自定义镜像
- 地理位置过滤 - 可选的国家/地区访问控制
- 缓存控制 - 每种路由类型可配置独立 TTL
- 远程配置同步 - 定时从远程拉取白名单配置,自动热更新
- 安全加固 - 路径遍历防护、输入验证、大小限制
- 现代技术栈 - Rust 后端(Axum + Tokio)、Vue 3 前端
CI 构建产物目录结构(二进制 + 前端 + 配置均在根目录):
.
├── mirror # 二进制
├── webui/dist/ # 前端静态文件
└── config/ # 配置文件(首次运行自动生成)
# 安装部署(CI 已将二进制、webui/dist、config/ 打包到同一目录)
tar -xzf mirror.tar.gz -C /opt/mirror/
cd /opt/mirror
chmod +x mirror
# 启动
nohup ./mirror > /dev/null 2>&1 &
echo $! > mirror.pid
echo "started (PID $(cat mirror.pid))"
# 查看状态
if kill -0 $(cat mirror.pid) 2>/dev/null; then
echo "running (PID $(cat mirror.pid))"
else
echo "stopped"
fi
# 重启
kill -TERM $(cat mirror.pid) 2>/dev/null
sleep 2
nohup ./mirror > /dev/null 2>&1 &
echo $! > mirror.pid
echo "restarted (PID $(cat mirror.pid))"
# 停止
kill -TERM $(cat mirror.pid) 2>/dev/null
rm -f mirror.pid
echo "stopped"
# 查看日志
tail -f logs/mirror.log# 直接运行(前台,Ctrl+C 停止)
.\mirror.exe生产环境建议配合 systemd(Linux)或 NSSM / 任务计划程序(Windows)实现进程守护与开机自启。
让 CDN(如腾讯 EdgeOne)在边缘终止客户端 HTTPS,回源走 HTTP 到你的源站;源站用 Caddy 在 :80 反代到只监听本地的 app。这样 app 不直接对公网暴露,TLS 全交给 CDN。
客户端 ──HTTPS──▶ EO/CDN(边缘终止TLS) ──HTTP回源──▶ Caddy(:80) ──▶ app(127.0.0.1:7878)
1. app 只监听本地 —— config/config.json 里设 "host": "127.0.0.1",这样只有 Caddy 能访问它,:7878 不对公网开放。
2. systemd 守护 app(/etc/systemd/system/mirror.service):
[Unit]
Description=mirror-karinjs
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/mirror
ExecStart=/opt/mirror/mirror
Restart=on-failure
RestartSec=3
LimitNOFILE=65536
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload && systemctl enable --now mirror3. 安装 Caddy 并反代(/etc/caddy/Caddyfile)。因为域名 DNS 指向 CDN(不指向源站),源站签不了证书,所以 auto_https off、只服务 HTTP:
{
auto_https off
}
http://mirror.example.com {
reverse_proxy 127.0.0.1:7878
}systemctl reload caddy4. CDN 回源配置(关键):在 EO/CDN 控制台把源站设为你的服务器公网 IP、回源协议 HTTP、端口 80。
⚠️ 若回源走 HTTPS/“协议跟随”,会打到源站:443(没东西)一直等超时,表现为访问极慢(几十秒)。务必用 HTTP:80。
5.(可选)只让 CDN 回源进来,挡掉爬虫直连源站
EdgeOne 回源时会自动带 Cdn-Loop: TencentEdgeOne 头,直连/爬虫没有。据此放行:
{
auto_https off
}
:80 {
@eo header Cdn-Loop *TencentEdgeOne*
@health path /healthz
handle @health { reverse_proxy 127.0.0.1:7878 } # 健康检查放行
handle @eo { reverse_proxy 127.0.0.1:7878 } # 经 CDN 的流量放行
handle { respond 403 } # 其余(直连/扫描)一律 403
}
Cdn-Loop非加密、可伪造,能挡住全网扫描/爬虫,但挡不住“已知源站 IP 且知道你用 EO 还故意伪造头”的针对性绕过。要更强:
- 密钥回源头:CDN 规则引擎给回源加一个秘密请求头,把上面
@eo改成header X-Origin-Secret "<秘密值>";- 源站防火墙:用
originProtection自动拉 EO 回源 IP 段写进 nftables,从网络层只放行 CDN 回源 IP。
- Rust 1.70+ (后端)
- pnpm 8+ (前端)
pnpm install
pnpm dev # 同时启动前后端
# 或分别启动
pnpm dev:backend # Rust 后端,cargo-watch 热重载
pnpm dev:frontend # Vite 开发服务器pnpm build # 构建所有
pnpm build:backend # → target/release/mirror
pnpm build:frontend # → webui/dist/配置分成两个文件,都在 config/ 下,首次运行缺哪个就自动生成哪个:
| 文件 | 内容 | 是否含密钥 | 版本库 |
|---|---|---|---|
config.json |
应用设置(host/port/auth/geo/cacheTTL/cors/configSync/originProtection) | 可能有(auth 等) | 已 gitignore,勿提交 |
config.mirror.json |
全部白名单(avatar/raw/releases/unpkg/mirror) | 无 | 可提交 / 可公开 / 作同步源 |
两者分开,是为了让"可公开共享的白名单"和"私有的应用配置(含密钥)"互不混淆——同步源只需发布
config.mirror.json。
顶层为应用运行参数:
| 字段 | 类型 | 默认值 | 单位 | 说明 |
|---|---|---|---|---|
host |
string | "0.0.0.0" |
— | 监听地址 |
port |
number | 7878 |
— | 监听端口 |
publicOrigin |
string | "https://mirror.karinjs.com" |
— | 对外公开的访问地址(用于重定向) |
trustProxyHeaders |
bool | true |
— | 是否信任反向代理转发的 X-Forwarded-* 头 |
logLevel |
string | "info" |
— | 日志级别:trace / debug / info / warn / error |
geo.mode |
string | "off" |
— | 地理位置过滤模式:off / allow / deny |
geo.headerName |
string | "EO-Client-IPCountry" |
— | 携带国家代码的 HTTP 请求头名称 |
geo.countries |
string[] | ["CN","HK","MO","TW"] |
— | 需过滤的国家/地区代码列表 |
cacheTTL.raw |
number | 300 |
秒 | /raw/ 路由的缓存 TTL |
cacheTTL.avatar |
number | 300 |
秒 | /avatar/ 路由的缓存 TTL |
cacheTTL.unpkg |
number | 300 |
秒 | /unpkg/ 路由的缓存 TTL |
mirror.defaultTTL |
number | 0 |
秒 | /mirror/ 路由下未明确指定 TTL 的 URL 的默认值 |
mirror.defaultMaxSize |
number | 52428800 |
字节 | 默认响应体大小上限(50 MB) |
mirror.absoluteMaxSize |
number | 1073741824 |
字节 | 响应体的硬上限(1 GB) |
mirror.fetchTimeoutMs |
number | 30000 |
毫秒 | 上游回源超时时间 |
mirror.fetchRetries |
number | 2 |
次 | 上游回源失败(连接错误/超时/5xx)时的最大重试次数,总尝试次数 = 重试次数 + 1,采用指数退避(约 200ms、400ms…),仅在收到响应头之前重试 |
cors.enabledRoutes |
string[] | ["raw","unpkg","mirror"] |
— | 启用 CORS 响应头的路由列表 |
auth.enabled |
bool | false |
— | 是否启用请求头鉴权 |
auth.key |
string | "" |
— | 鉴权请求头名称 |
auth.value |
string | "" |
— | 鉴权请求头的期望值 |
TTL 语义(适用于所有 TTL 字段):
| TTL 值 | Cache-Control 响应头 |
说明 |
|---|---|---|
-2 |
透传上游 | 原样转发上游的 Cache-Control 和 ETag |
-1 |
public, max-age=31536000, immutable |
1 年强制缓存(适合带版本号的静态资源) |
0 |
no-store |
禁止缓存 |
> 0 |
public, max-age=<ttl> |
自定义缓存时长(单位:秒) |
整个文件就是白名单,5 个顶层子键:avatar / raw / releases / unpkg / mirror。各子键省略时默认为空(即该路由拒绝全部请求)。
{
"avatar": ["karinjs", "NapNeko"],
"raw": {
"karinjs": {
"karin": [
{ "branch": "HEAD", "file": "package.json" },
{ "branch": "main", "file": "README.md" }
]
}
},
"releases": {
"NapNeko": {
"NapCatQQ": ["NapCat.Framework.zip", "NapCat.linux-amd64"]
}
},
"unpkg": {
"karin": ["package.json", "dist/karin.umd.js"]
},
"mirror": {
"https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json": 0,
"https://example.com/stable/asset.zip": -1,
"https://example.com/dynamic/data.json": { "ttl": 60, "maxSize": 1048576 }
}
}字符串数组,每个元素是一个允许代理头像的 GitHub 用户名。请求 /avatar/<user>.png 时检查 <user> 是否命中,命中则代理 https://github.com/<user>.png。
三层嵌套结构 {owner: {repo: [{branch, file}]}}。请求 /raw/<owner>/<repo>/<branch>/<file> 时精确匹配;branch 为 "HEAD" 表示接受任何分支。
三层嵌套结构 {owner: {repo: [asset_filename]}}。请求 /gh/<owner>/<repo>/releases/download/<tag>/<file> 时校验 <file> 是否存在于对应仓库的允许列表中。
{package_name: [file_paths]}。请求 /unpkg/<pkg>[@version]/<file> 时校验文件路径是否在白名单中,支持版本号或版本范围。
URL 到规则(TTL 或 {ttl, maxSize?})的映射。
- 简写形式:值是数字时,即 TTL
- 完整形式:
ttl指定缓存策略,maxSize可选,覆盖全局defaultMaxSize
配置同步功能(配置在 config.json 的 configSync 里)可以定时从一个远程直链 URL 拉取白名单文件(即 config.mirror.json 那种纯白名单 JSON),通过 SHA-256 比对检测变更,自动热更新内存中的白名单并写回本地 config.mirror.json,无需重启服务。
安全边界:同步只涉及白名单。
config.json里的auth/host/port/geo/configSync等应用设置完全不参与同步,远程永远无法关闭鉴权、改监听地址或改写同步目标。即使同步源被攻陷,最坏也只能改白名单(而白名单仍受各路由的 SSRF / 路径校验约束)。同步源是纯白名单,本身也不含任何应用配置。
{
"configSync": {
"enabled": false,
"intervalSeconds": 300,
"url": "https://example.com/config.mirror.json"
}
}| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
configSync.enabled |
bool | false |
是否启用远程同步 |
configSync.intervalSeconds |
number | 300 |
检查间隔(秒),最小值为 1 |
configSync.url |
string | "" |
远程白名单文件(纯 config.mirror.json)的直链 URL |
工作流程:
- 每隔
intervalSeconds秒,对配置的url发起一次 HTTP GET 请求 - 校验响应
Content-Type:接受 JSON 类型(application/json/text/json/*+json)以及text/plain(GitHub raw 等静态托管会把.json返回成text/plain);text/html(被劫持的登录页)、二进制等直接拒绝。这只是粗筛,响应体随后仍会被完整按白名单结构解析 - 计算响应体的 SHA-256,与上次成功采用的哈希比对;相同则跳过
- 若不同 → 按白名单解析校验 → 写入本地
config.mirror.json→ 热更新内存白名单 - 若请求失败 / 非 JSON / 校验失败 → 记录告警日志,本地白名单与应用配置均不受影响,下个周期重试
安全特性:
- 同步只影响白名单;
config.json里的应用设置(auth/host/port/geo/configSync…)完全不参与 Content-Type粗筛(拒绝 HTML/二进制)+ 白名单结构校验,无效或被劫持(如返回登录页)的响应一律拒绝,绝不写入磁盘- 网络错误 / 非 2xx 状态码 / 超出 10 MB 体积上限均记录告警,不影响服务正常运行
- 同步 URL 经过 SSRF / DNS 重绑定防护校验(必须 https、禁止内网/环回/userinfo)
托管提示:GitHub raw(
raw.githubusercontent.com,返回text/plain)、jsDelivr、GitHub Pages、Cloudflare、对象存储等均可。注意 jsDelivr 有 ~12h 缓存;GitHub raw 实时但Content-Type为text/plain(已被接受)。同步源就是config.mirror.json(纯白名单),本身不含任何应用配置。
只让 EdgeOne 回源能进来:定时调用 EO DescribeOriginACL 拉取回源 IP 段,写进 nftables,只放行 EO + loopback 到指定端口,其余到这些端口的连接一律丢弃(扫描者看到端口"关闭")。SSH 等其它端口不受影响。
{
"originProtection": {
"enabled": false,
"zoneId": "zone-xxxxxxxx",
"secretId": "",
"secretKey": "",
"intervalSeconds": 259200,
"ports": [80, 443]
}
}| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
originProtection.enabled |
bool | false |
是否启用 |
originProtection.zoneId |
string | "" |
EO 站点 ID(zone-xxxx) |
originProtection.secretId |
string | "" |
腾讯云 API SecretId(建议最小权限 CAM,仅 teo:DescribeOriginACL) |
originProtection.secretKey |
string | "" |
腾讯云 API SecretKey |
originProtection.intervalSeconds |
number | 259200 |
拉取间隔(秒),EO 建议约 3 天 |
originProtection.ports |
number[] | [80, 443] |
受保护端口(只放行 EO 回源到这些端口) |
前提与说明:
- 仅 Linux + root +
nft可用;非 Linux 会记告警并跳过。 - 仅
zoneId不够 ——DescribeOriginACL需要 API 密钥签名(TC3-HMAC-SHA256)。 - 单独建表
inet origin_guard(input hook,policy accept),只对配置端口做丢弃,不会影响 SSH。 - 凭据敏感:
config.json已默认 gitignore,请勿提交。 - 与"密钥回源头部 + app
auth"互补:前者网络层挡扫描,后者应用层校验。
| 路由 | 格式 | 示例 |
|---|---|---|
| GitHub Releases | /gh/<owner>/<repo>/releases/download/<tag>/<file> |
/gh/NapNeko/NapCatQQ/releases/download/v4.18.0/NapCat.Framework.zip |
| GitHub Raw | /raw/<owner>/<repo>/<branch>/<path> |
/raw/karinjs/karin/main/package.json |
| GitHub 头像 | /avatar/<user>.png |
/avatar/karinjs.png |
| npm/unpkg | /unpkg/<pkg>[@version]/<file> |
/unpkg/karin/package.json |
| 通用镜像 | /mirror/<host>/<path> |
/mirror/example.com/file.zip |
核心防护:
- 路径遍历验证(所有路由)
- 白名单优先设计(默认拒绝)
- 输入清理(拒绝
..、//、\\) - 查询参数拒绝(带
?的请求直接 404,防缓存绕过) - 流式大小限制
- 地理位置阻断(Fail-Closed)
- 请求头鉴权(可选)
- 无 SQL/命令注入风险
后端返回正确的 Cache-Control 头,适配 CDN 集成:
- Releases(
ttl: -1):public, max-age=31536000, immutable - Raw / Avatar / unpkg:由
config.json中cacheTTL对应字段控制 - Mirror:由
config.mirror.json中mirror下按 URL 配置的 TTL 控制 - 不缓存(
ttl: 0):no-store
- 关闭 EO 全站缓存
- 后端缓存头将控制缓存行为
- EO 会尊重
Cache-Control指令 - 在 EO 中配置
EO-Client-Country请求头注入
.
├── src/ # Rust 后端
│ ├── main.rs # 入口
│ ├── server.rs # Axum 路由与中间件
│ ├── config.rs # 配置结构定义与加载
│ ├── sync.rs # 远程配置同步后台任务
│ ├── proxy.rs # 上游代理逻辑
│ ├── routes/ # 路由处理器
│ │ ├── releases.rs # GitHub Releases
│ │ ├── raw.rs # GitHub Raw
│ │ ├── avatar.rs # GitHub 头像
│ │ ├── unpkg.rs # npm/unpkg
│ │ ├── mirror.rs # 通用镜像
│ │ └── mod.rs
│ ├── geo.rs # 地理位置检查
│ ├── stats.rs # 请求统计
│ ├── http_utils.rs # HTTP 工具函数
│ └── error.rs # 错误类型
├── webui/ # Vue 3 前端
│ ├── src/
│ ├── public/
│ └── dist/
├── config/ # JSON 配置文件(运行时生成)
├── Cargo.toml
├── package.json
└── pnpm-workspace.yaml
- 在
src/routes/创建新文件 - 实现路由处理函数
- 在
src/routes/mod.rs中导出 - 在
src/server.rs中注册路由
- 更新
src/config.rs中的类型定义 - 更新
config/目录下的默认配置生成逻辑 - 更新本文档
cd webui
pnpm dev
# Vite 开发服务器,API 请求代理到 http://127.0.0.1:3000MIT