Skip to content

Repository files navigation

mirror

基于白名单的 GitHub / npm 反向代理服务,专为 Tencent EO 全站加速优化。

特性

  • 白名单控制 - 仅代理已配置的上游资源,防止滥用
  • 多路由支持 - GitHub Releases、Raw 文件、头像、npm/unpkg、自定义镜像
  • 地理位置过滤 - 可选的国家/地区访问控制
  • 缓存控制 - 每种路由类型可配置独立 TTL
  • 远程配置同步 - 定时从远程拉取白名单配置,自动热更新
  • 安全加固 - 路径遍历防护、输入验证、大小限制
  • 现代技术栈 - Rust 后端(Axum + Tokio)、Vue 3 前端

部署

CI 构建产物目录结构(二进制 + 前端 + 配置均在根目录):

.
├── mirror # 二进制
├── webui/dist/ # 前端静态文件
└── config/ # 配置文件(首次运行自动生成)

Linux(nohup)

# 安装部署(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))"# 查看状态ifkill -0 $(cat mirror.pid)2>/dev/null;thenecho"running (PID $(cat mirror.pid))"elseecho"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

Windows

# 直接运行(前台,Ctrl+C 停止)
.\mirror.exe

生产环境建议配合 systemd(Linux)或 NSSM / 任务计划程序(Windows)实现进程守护与开机自启。

EO / CDN 回源 + Caddy 反代(生产推荐)

让 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.target
systemctl daemon-reload && systemctl enable --now mirror

3. 安装 Caddy 并反代/etc/caddy/Caddyfile)。因为域名 DNS 指向 CDN(不指向源站),源站签不了证书,所以 auto_https off、只服务 HTTP

{
	auto_https off
}
http://mirror.example.com {
	reverse_proxy127.0.0.1:7878
}
systemctl reload caddy

4. 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

config.json — 应用设置

顶层为应用运行参数:

字段类型默认值单位说明
hoststring"0.0.0.0"监听地址
portnumber7878监听端口
publicOriginstring"https://mirror.karinjs.com"对外公开的访问地址(用于重定向)
trustProxyHeadersbooltrue是否信任反向代理转发的 X-Forwarded-*
logLevelstring"info"日志级别:trace / debug / info / warn / error
geo.modestring"off"地理位置过滤模式:off / allow / deny
geo.headerNamestring"EO-Client-IPCountry"携带国家代码的 HTTP 请求头名称
geo.countriesstring[]["CN","HK","MO","TW"]需过滤的国家/地区代码列表
cacheTTL.rawnumber300/raw/ 路由的缓存 TTL
cacheTTL.avatarnumber300/avatar/ 路由的缓存 TTL
cacheTTL.unpkgnumber300/unpkg/ 路由的缓存 TTL
mirror.defaultTTLnumber0/mirror/ 路由下未明确指定 TTL 的 URL 的默认值
mirror.defaultMaxSizenumber52428800字节默认响应体大小上限(50 MB)
mirror.absoluteMaxSizenumber1073741824字节响应体的硬上限(1 GB)
mirror.fetchTimeoutMsnumber30000毫秒上游回源超时时间
mirror.fetchRetriesnumber2上游回源失败(连接错误/超时/5xx)时的最大重试次数,总尝试次数 = 重试次数 + 1,采用指数退避(约 200ms、400ms…),仅在收到响应头之前重试
cors.enabledRoutesstring[]["raw","unpkg","mirror"]启用 CORS 响应头的路由列表
auth.enabledboolfalse是否启用请求头鉴权
auth.keystring""鉴权请求头名称
auth.valuestring""鉴权请求头的期望值

TTL 语义(适用于所有 TTL 字段):

TTL 值Cache-Control 响应头说明
-2透传上游原样转发上游的 Cache-ControlETag
-1public, max-age=31536000, immutable1 年强制缓存(适合带版本号的静态资源)
0no-store禁止缓存
> 0public, max-age=<ttl>自定义缓存时长(单位:秒

config.mirror.json — 白名单

整个文件就是白名单,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 }
}
}

avatar — GitHub 头像白名单

字符串数组,每个元素是一个允许代理头像的 GitHub 用户名。请求 /avatar/<user>.png 时检查 <user> 是否命中,命中则代理 https://github.com/<user>.png

raw — GitHub Raw 文件白名单

三层嵌套结构 {owner: {repo: [{branch, file}]}}。请求 /raw/<owner>/<repo>/<branch>/<file> 时精确匹配;branch"HEAD" 表示接受任何分支。

releases — GitHub Releases 白名单

三层嵌套结构 {owner: {repo: [asset_filename]}}。请求 /gh/<owner>/<repo>/releases/download/<tag>/<file> 时校验 <file> 是否存在于对应仓库的允许列表中。

unpkg — npm/unpkg 白名单

{package_name: [file_paths]}。请求 /unpkg/<pkg>[@version]/<file> 时校验文件路径是否在白名单中,支持版本号或版本范围。

mirror — 自定义镜像白名单

URL 到规则(TTL 或 {ttl, maxSize?})的映射。

  • 简写形式:值是数字时,即 TTL
  • 完整形式ttl 指定缓存策略,maxSize 可选,覆盖全局 defaultMaxSize

configSync — 远程配置自动同步

配置同步功能(配置在 config.jsonconfigSync 里)可以定时从一个远程直链 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.enabledboolfalse是否启用远程同步
configSync.intervalSecondsnumber300检查间隔(秒),最小值为 1
configSync.urlstring""远程白名单文件(纯 config.mirror.json)的直链 URL

工作流程:

  1. 每隔 intervalSeconds 秒,对配置的 url 发起一次 HTTP GET 请求
  2. 校验响应 Content-Type:接受 JSON 类型(application/json / text/json / *+json以及 text/plain(GitHub raw 等静态托管会把 .json 返回成 text/plain);text/html(被劫持的登录页)、二进制等直接拒绝。这只是粗筛,响应体随后仍会被完整按白名单结构解析
  3. 计算响应体的 SHA-256,与上次成功采用的哈希比对;相同则跳过
  4. 若不同 → 按白名单解析校验 → 写入本地 config.mirror.json → 热更新内存白名单
  5. 若请求失败 / 非 JSON / 校验失败 → 记录告警日志,本地白名单与应用配置均不受影响,下个周期重试

安全特性:

  • 同步只影响白名单;config.json 里的应用设置(auth/host/port/geo/configSync…)完全不参与
  • Content-Type 粗筛(拒绝 HTML/二进制)+ 白名单结构校验,无效或被劫持(如返回登录页)的响应一律拒绝,绝不写入磁盘
  • 网络错误 / 非 2xx 状态码 / 超出 10 MB 体积上限均记录告警,不影响服务正常运行
  • 同步 URL 经过 SSRF / DNS 重绑定防护校验(必须 https、禁止内网/环回/userinfo)

托管提示:GitHub rawraw.githubusercontent.com,返回 text/plain)、jsDelivr、GitHub Pages、Cloudflare、对象存储等均可。注意 jsDelivr 有 ~12h 缓存;GitHub raw 实时但 Content-Typetext/plain(已被接受)。同步源就是 config.mirror.json(纯白名单),本身不含任何应用配置。

originProtection — EO 源站保护自动同步(仅源码功能,默认关闭)

只让 EdgeOne 回源能进来:定时调用 EO DescribeOriginACL 拉取回源 IP 段,写进 nftables,只放行 EO + loopback 到指定端口,其余到这些端口的连接一律丢弃(扫描者看到端口"关闭")。SSH 等其它端口不受影响。

{
"originProtection": {
"enabled": false,
"zoneId": "zone-xxxxxxxx",
"secretId": "",
"secretKey": "",
"intervalSeconds": 259200,
"ports": [80, 443]
}
}
字段类型默认值说明
originProtection.enabledboolfalse是否启用
originProtection.zoneIdstring""EO 站点 ID(zone-xxxx
originProtection.secretIdstring""腾讯云 API SecretId(建议最小权限 CAM,仅 teo:DescribeOriginACL
originProtection.secretKeystring""腾讯云 API SecretKey
originProtection.intervalSecondsnumber259200拉取间隔(秒),EO 建议约 3 天
originProtection.portsnumber[][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 集成:

  • Releasesttl: -1):public, max-age=31536000, immutable
  • Raw / Avatar / unpkg:由 config.jsoncacheTTL 对应字段控制
  • Mirror:由 config.mirror.jsonmirror 下按 URL 配置的 TTL 控制
  • 不缓存ttl: 0):no-store

EO CDN 配置建议

  1. 关闭 EO 全站缓存
  2. 后端缓存头将控制缓存行为
  3. EO 会尊重 Cache-Control 指令
  4. 在 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

开发指南

添加新路由

  1. src/routes/ 创建新文件
  2. 实现路由处理函数
  3. src/routes/mod.rs 中导出
  4. src/server.rs 中注册路由

修改配置结构

  1. 更新 src/config.rs 中的类型定义
  2. 更新 config/ 目录下的默认配置生成逻辑
  3. 更新本文档

前端开发

cd webui
pnpm dev
# Vite 开发服务器,API 请求代理到 http://127.0.0.1:3000

许可证

MIT

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages