docs(ops): record proxy-ng deployment hardening
Add deployment traces for the JPpro proxy node, RackNerd origin hosts, catproxy boundary changes, local speed checks, and Sub2API rate-limit behavior. Keep remote secrets excluded while tracking sanitized nginx, compose, plan, and operational memory documents.
This commit is contained in:
parent
bd3d4d9a24
commit
6814fa2a62
10
.gitignore
vendored
10
.gitignore
vendored
@ -119,10 +119,13 @@ tests
|
||||
CLAUDE.md
|
||||
.claude
|
||||
scripts
|
||||
!proxy-ng/scripts/
|
||||
!proxy-ng/scripts/*.sh
|
||||
.code-review-state
|
||||
#openspec/
|
||||
code-reviews/
|
||||
AGENTS.md
|
||||
!AGENTS.md
|
||||
backend/cmd/server/server
|
||||
deploy/docker-compose.override.yml
|
||||
.gocache/
|
||||
@ -131,6 +134,13 @@ docs/*
|
||||
!docs/PAYMENT.md
|
||||
!docs/PAYMENT_CN.md
|
||||
!docs/ADMIN_PAYMENT_INTEGRATION_API.md
|
||||
!docs/agent-specs/
|
||||
!docs/agent-specs/*.md
|
||||
!docs/memorys/
|
||||
!docs/memorys/*.md
|
||||
!docs/superpowers/
|
||||
!docs/superpowers/plans/
|
||||
!docs/superpowers/plans/*.md
|
||||
.serena/
|
||||
.codex/
|
||||
frontend/coverage/
|
||||
|
||||
211
AGENTS.md
Normal file
211
AGENTS.md
Normal file
@ -0,0 +1,211 @@
|
||||
# AGENTS.md — Sub2API 部署与发版
|
||||
|
||||
## 项目概述
|
||||
|
||||
Sub2API — AI API 网关平台,Go + Vue 3 前后端一体,Ent ORM,支持多账户管理、API 密钥分发、Token 级计费、智能调度、支付集成。
|
||||
|
||||
- 上游: `https://github.com/Wei-Shaw/sub2api`
|
||||
- 本地: `/Users/qingse/Sync/local-git/sub2api`
|
||||
- 当前稳定版: `v0.1.126`
|
||||
- 许可证: LGPL v3
|
||||
|
||||
## 部署留痕规则
|
||||
|
||||
所有远程部署、迁移、域名切换、证书、反代、Compose、数据库、Redis、Mihomo 或代理配置变化,都必须在当前仓库留痕。
|
||||
|
||||
- 根级 `AGENTS.md` 只记录规则、索引和通用流程。
|
||||
- 每台服务器必须在 `ops/remote/` 下单独一个子目录。
|
||||
- 服务器目录必须记录域名、IP、服务器名、Tabby profile、部署方式、部署架构、运行检查和操作记录。
|
||||
- 当前实际生效配置副本必须放在对应服务器目录的 `current/` 下,例如 `docker-compose.yml`、`nginx-sub2api.conf` 或 `Caddyfile`。
|
||||
- 计划但尚未部署的配置放在 `planned/` 下,不能写成当前事实。
|
||||
- `.env`、数据库密码、JWT/TOTP 密钥、Mihomo 订阅和真实代理配置只允许放在被 `.gitignore` 排除的 `secrets/` 或 `backups/` 中,禁止提交。
|
||||
- 通过 Tabby 操作远程后,必须同步更新对应服务器目录,避免远程真实状态和项目记录漂移。
|
||||
|
||||
当前远程部署索引见 `ops/remote/README.md`。每个服务器目录至少包含:
|
||||
|
||||
```text
|
||||
ops/remote/<server>-sub2api/
|
||||
├── README.md # 服务器级部署记录
|
||||
├── current/ # 当前远程生效配置副本
|
||||
├── planned/ # 计划配置,未部署时使用
|
||||
├── secrets/README.md # 敏感配置保存说明,真实文件不提交
|
||||
├── backups/.gitkeep # 远程备份说明或占位,真实备份不提交
|
||||
└── .gitignore # 排除密钥、订阅和备份
|
||||
```
|
||||
|
||||
## 远程部署索引
|
||||
|
||||
| 目录 | Tabby 连接 | 公网入口 | 状态 |
|
||||
|------|------------|----------|------|
|
||||
| `ops/remote/jppro-sub2api/` | `个人-JPpro-akile-0504-1c1g` | `https://proxy.api.lilifamily.com` / `https://jp.proxy.api.lilifamily.com` | proxy-ng 公开入口节点,回源到 3c4g origin |
|
||||
| `ops/remote/us-racknerd-0425-sub2api/` | `个人-US-racknerd-0425-1c1g` | `https://pai.cyan2000.uk` | 旧机,迁移来源和回滚来源 |
|
||||
| `ops/remote/us-racknerd-0526-sub2api/` | `个人-US-racknerd-0526-3c4g` | `https://origin.proxy.api.lilifamily.com` / `https://catproxy.lilifamily.com` | Sub2API 受限 origin 和公开直连主站入口 |
|
||||
|
||||
## JPpro 当前 proxy-ng 记录
|
||||
|
||||
| 项目 | 详情 |
|
||||
|------|------|
|
||||
| 服务器 | JP Pro VPS `151.242.164.72`,Debian Trixie 1C1G20G |
|
||||
| 域名 | `proxy.api.lilifamily.com`、`jp.proxy.api.lilifamily.com` |
|
||||
| Tabby 连接 | `个人-JPpro-akile-0504-1c1g` |
|
||||
| 部署方式 | apt 安装 nginx + Certbot,作为 proxy-ng 入口中转 |
|
||||
| 回源目标 | `https://origin.proxy.api.lilifamily.com` |
|
||||
| 访问 | `https://proxy.api.lilifamily.com/`、`https://jp.proxy.api.lilifamily.com/` |
|
||||
| 详细记录 | `ops/remote/jppro-sub2api/` |
|
||||
|
||||
当前 JPpro 不运行 Sub2API、PostgreSQL、Redis、Caddy 或账号出口代理;只运行 nginx 反向代理。当前已启用独立 origin 回源、3c4g 来源 IP 白名单和 `X-Proxy-Ng-Token` 校验。
|
||||
|
||||
## JPpro 历史部署记录
|
||||
|
||||
| 项目 | 详情 |
|
||||
|------|------|
|
||||
| 服务器 | JP Pro VPS `151.242.164.72`,Debian Trixie 1C1G20G |
|
||||
| 域名 | `proxy.api.cyan2000.uk` |
|
||||
| Tabby 连接 | `个人-JPpro-akile-0504-1c1g` |
|
||||
| 部署目录 | `/opt/sub2api/`,已于 2026-05-26 清理 |
|
||||
| 访问 | `https://proxy.api.cyan2000.uk/`,当前未在 JPpro 上提供服务 |
|
||||
| 详细记录 | `ops/remote/jppro-sub2api/` |
|
||||
|
||||
### 远程目录结构
|
||||
|
||||
```
|
||||
/opt/sub2api/
|
||||
├── docker-compose.yml # 官方 docker-compose.yml + Caddy
|
||||
├── .env # 密钥配置 (chmod 600)
|
||||
└── Caddyfile # Caddy 反向代理配置
|
||||
```
|
||||
|
||||
数据存储在 Docker 命名卷:`sub2api_data`、`postgres_data`、`redis_data`、`caddy_data`、`caddy_config`。
|
||||
|
||||
### 历史 Docker 服务架构
|
||||
|
||||
```
|
||||
Browser :443 (HTTPS) → Caddy → sub2api :8080 (内部) → postgres :5432 (内部)
|
||||
:80 → 301 redirect → redis :6379 (内部)
|
||||
```
|
||||
|
||||
| 容器 | 镜像 | 角色 |
|
||||
|------|------|------|
|
||||
| `sub2api-caddy` | `caddy:2-alpine` | HTTPS 自动证书 + 反向代理 |
|
||||
| `sub2api` | `weishaw/sub2api:latest` | Go 后端 + 内嵌 Vue 前端 |
|
||||
| `sub2api-postgres` | `postgres:18-alpine` | 主数据库 |
|
||||
| `sub2api-redis` | `redis:8-alpine` | 缓存 |
|
||||
|
||||
### Caddy 配置
|
||||
|
||||
```
|
||||
proxy.api.cyan2000.uk {
|
||||
reverse_proxy sub2api:8080
|
||||
encode gzip
|
||||
}
|
||||
```
|
||||
|
||||
Caddy 自动从 Let's Encrypt 获取和管理 TLS 证书,HTTP(:80) 自动 301 跳转到 HTTPS(:443)。
|
||||
|
||||
## 发版流程
|
||||
|
||||
远程服务器可直接拉取 Docker Hub,常规升级只需 pull + restart。
|
||||
|
||||
### 常规升级(官方镜像)
|
||||
|
||||
```bash
|
||||
cd /opt/sub2api
|
||||
docker compose pull sub2api
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 本地编译部署(二开修改时使用)
|
||||
|
||||
当 GitHub 不可达需要国内 GOPROXY,或需要本地修改代码后部署时:
|
||||
|
||||
```bash
|
||||
# 1. 拉取代码
|
||||
cd /Users/qingse/Sync/local-git/sub2api
|
||||
git fetch ea-ali
|
||||
git checkout <tag>
|
||||
|
||||
# 2. 构建前端
|
||||
cd frontend && pnpm install --frozen-lockfile && pnpm run build
|
||||
|
||||
# 3. 交叉编译 Go
|
||||
cd ../backend
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
GOPROXY=https://goproxy.cn,direct \
|
||||
go build -tags embed -ldflags="-s -w -X main.Version=<版本>" -trimpath \
|
||||
-o /tmp/sub2api ./cmd/server
|
||||
|
||||
# 4. 构建最小 Docker 镜像
|
||||
mkdir -p /tmp/sub2api-build/{deploy,backend/resources}
|
||||
cp /tmp/sub2api /tmp/sub2api-build/
|
||||
cp -r ../backend/resources/* /tmp/sub2api-build/backend/resources/
|
||||
cp ../deploy/docker-entrypoint.sh /tmp/sub2api-build/deploy/
|
||||
|
||||
cat > /tmp/sub2api-build/Dockerfile << 'EOF'
|
||||
FROM alpine:latest
|
||||
RUN apk add --no-cache ca-certificates tzdata su-exec libpq wget
|
||||
RUN addgroup -g 1000 sub2api && adduser -u 1000 -G sub2api -s /bin/sh -D sub2api
|
||||
COPY sub2api /app/sub2api
|
||||
COPY backend/resources /app/resources
|
||||
COPY deploy/docker-entrypoint.sh /app/docker-entrypoint.sh
|
||||
RUN chmod +x /app/docker-entrypoint.sh && mkdir -p /app/data && chown sub2api:sub2api /app/data
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD wget -q -T 5 -O /dev/null http://localhost:8080/health || exit 1
|
||||
WORKDIR /app
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
CMD ["/app/sub2api"]
|
||||
EOF
|
||||
|
||||
docker build --platform linux/amd64 -t sub2api:<版本> /tmp/sub2api-build
|
||||
|
||||
# 5. 导出上传
|
||||
docker save sub2api:<版本> | gzip > /tmp/sub2api-<版本>.tar.gz
|
||||
# Tabby SFTP: open_profile(个人-JPpro-akile-0504-1c1g) → sftp_upload → /tmp/
|
||||
|
||||
# 6. 远程部署
|
||||
docker load < /tmp/sub2api-<版本>.tar.gz
|
||||
cd /opt/sub2api
|
||||
sed -i 's|image:.*|image: sub2api:<版本>|' docker-compose.yml
|
||||
docker compose up -d
|
||||
|
||||
# 7. 清理
|
||||
rm -rf /tmp/sub2api-build /tmp/sub2api /tmp/sub2api-*.tar.gz
|
||||
```
|
||||
|
||||
## 运维命令
|
||||
|
||||
```bash
|
||||
# 查看服务状态
|
||||
cd /opt/sub2api && docker compose ps
|
||||
|
||||
# 查看日志
|
||||
docker logs sub2api --tail 50 -f
|
||||
|
||||
# 查询数据库
|
||||
docker exec sub2api-postgres psql -U sub2api -d sub2api -c "SELECT ..."
|
||||
|
||||
# 重启服务
|
||||
cd /opt/sub2api && docker compose up -d
|
||||
|
||||
# 停止服务
|
||||
cd /opt/sub2api && docker compose down
|
||||
|
||||
# 数据备份
|
||||
cd /opt/sub2api && tar czf /tmp/sub2api-backup-$(date +%Y%m%d).tar.gz data/ postgres_data/ redis_data/
|
||||
```
|
||||
|
||||
## Git 远程
|
||||
|
||||
| 远程 | URL | 角色 |
|
||||
|------|-----|------|
|
||||
| `origin` | `https://github.com/Wei-Shaw/sub2api.git` | 上游 |
|
||||
| `ea-ali` | `ssh://git@101.200.34.71:2222/zizi-al/sub2api.git` | 可写开发远程 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- Go 编译必须加 `-tags embed`,否则前端不会嵌入二进制
|
||||
- 前端 build 输出到 `backend/internal/web/dist`,Go 编译时通过 `//go:embed` 嵌入
|
||||
- GitHub 和 Docker Hub 在国内网络不可达时,Go proxy 用 `goproxy.cn`,Docker base image 用本地缓存
|
||||
- 远程服务器可访问 Docker Hub,postgres:18-alpine 和 redis:8-alpine 自动拉取
|
||||
- `.env` 文件权限必须是 600,包含数据库密码和 JWT 密钥
|
||||
- 首次启动会自动创建管理员账号(`AUTO_SETUP=true`)
|
||||
133
docs/agent-specs/2026-05-21-国际互联网络优化-审阅版.md
Normal file
133
docs/agent-specs/2026-05-21-国际互联网络优化-审阅版.md
Normal file
@ -0,0 +1,133 @@
|
||||
# 国际互联网络优化 — 审阅版
|
||||
|
||||
日期:2026-05-21 | 状态:待审阅
|
||||
|
||||
## 1. 背景与问题
|
||||
|
||||
sub2api 后端部署在高性能服务器(2C4G),计算资源充足,但国际入站网络质量不稳定。目标用户群体在中国大陆,需要利用轻量 VPS(1C/1G,国际网络优化线路)作为前置代理,提升国际用户访问质量。
|
||||
|
||||
**核心痛点**:
|
||||
- 后端服务器国际线路质量差,大陆用户延迟高、丢包
|
||||
- 直接暴露后端到公网,攻击面大
|
||||
- 缺少 DDoS 防护和地理访问控制
|
||||
|
||||
## 2. 目标与非目标
|
||||
|
||||
### 目标
|
||||
- 利用 1C/1G 轻量 VPS 优化线路,作为 nginx 反向代理入口
|
||||
- 仅允许中国大陆 IP 访问(通过 Cloudflare + nginx 双层过滤)
|
||||
- TLS 终止在边缘节点,回源链路加密
|
||||
- 1C/1G 被打爆后具备自动恢复和快速重建能力
|
||||
- 后端 sub2api 正确识别真实客户端 IP(限流、审计、ACL 不受代理影响)
|
||||
|
||||
### 非目标
|
||||
- 不对 sub2api 业务代码做架构变更
|
||||
- 不做多地域 CDN 分发或 Anycast
|
||||
- 不做后端高可用集群(当前单后端)
|
||||
- 不支持非中国 IP 访问
|
||||
|
||||
## 3. 核心决策
|
||||
|
||||
### 决策 1:前端 VPS 规格选择
|
||||
**选择**:1C/1G,国际优化线路。
|
||||
|
||||
理由:nginx 纯反向代理占用内存 50-100MB,1G 余量足够。单核 CPU 处理 TLS 握手和转发,无需多核。
|
||||
|
||||
### 决策 2:中国 IP 访问控制方案
|
||||
**选择**:Cloudflare WAF 规则 + nginx `CF-IPCountry` 头校验,双层过滤。
|
||||
|
||||
理由:
|
||||
- CF 免费计划自带 5 条 WAF 规则和自动添加 `CF-IPCountry` 头
|
||||
- WAF 层在边缘阻断,节省回源带宽
|
||||
- nginx 层防绕过 CF 直连源站的请求
|
||||
- 无需付费方案
|
||||
|
||||
### 决策 3:回源链路加密
|
||||
**选择**:自签证书 + HTTPS 回源。
|
||||
|
||||
理由:两台 VPS 走公网 IP 通信,明文 HTTP 会被同网段嗅探。自签证书零成本,nginx `proxy_ssl_verify off` 跳过验证。
|
||||
|
||||
### 决策 4:源站 IP 防护
|
||||
**选择**:iptables 白名单 Cloudflare IP 段,每天自动更新。
|
||||
|
||||
理由:即使 DNS 不公开源站 IP,攻击者可通过 IP 扫段发现。只允许 CF 回源 IP 入站,其余 443/80 流量到 iptables 就被 DROP。
|
||||
|
||||
### 决策 5:前端容灾策略
|
||||
**选择**:分层递进 — systemd 自动复活 → 内存超限自动降级 → fail2ban 自动封禁 → 一键重建脚本 → 备机 DNS 切换。
|
||||
|
||||
理由:1C/1G 被打爆是概率问题,必须做好多层自愈准备。
|
||||
|
||||
## 4. 架构
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[中国大陆用户] -->|HTTPS 443| B[Cloudflare CDN]
|
||||
B -->|"CF-IPCountry: CN<br/>仅允许中国IP"| C[轻量VPS 1C/1G<br/>nginx]
|
||||
C -->|HTTPS 公网回源<br/>自签证书| D[高性能服务器 2C4G<br/>sub2api :8080]
|
||||
D --> E[(PostgreSQL)]
|
||||
D --> F[(Redis)]
|
||||
|
||||
G["非中国IP"] -->|Block| B
|
||||
|
||||
style B fill:#f90,stroke:#333,color:#000
|
||||
style C fill:#39f,stroke:#333,color:#fff
|
||||
style D fill:#3c3,stroke:#333,color:#fff
|
||||
```
|
||||
|
||||
**防御链**(四层):
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[用户请求] --> B{CF WAF<br/>国家检查}
|
||||
B -->|非中国 IP| C[Block 403]
|
||||
B -->|中国 IP| D{iptables<br/>CF IP 白名单}
|
||||
D -->|非 CF IP| E[DROP]
|
||||
D -->|CF IP| F{nginx<br/>CF-IPCountry}
|
||||
F -->|空/非 CN| G[403]
|
||||
F -->|CN| H[sub2api<br/>业务处理]
|
||||
```
|
||||
|
||||
## 5. 影响范围
|
||||
|
||||
| 组件 | 变更类型 | 说明 |
|
||||
|---|---|---|
|
||||
| sub2api `config.yaml` | 配置变更 | 新增 `server.trusted_proxies` 含 nginx IP |
|
||||
| sub2api `config.yaml` | 配置变更 | 如启用 IP ACL:`security.trust_forwarded_ip_for_api_key_acl: true` |
|
||||
| 轻量 VPS | 新增部署 | nginx + fail2ban + iptables + 监控脚本 |
|
||||
| 后端高性能服务器 | 新增 | 自签 TLS 证书,可选本地 nginx sidecar 做 TLS 终止 |
|
||||
| DNS (`proxyapi.cyan2000.uk`) | 变更 | NS 切到 Cloudflare |
|
||||
| sub2api 代码 | 无变更 | `ip.GetClientIP()` 已支持 `CF-Connecting-IP` / `X-Real-IP` / `X-Forwarded-For` |
|
||||
|
||||
**兼容性**:sub2api 代码零变更。仅配置变化,回滚即删 `trusted_proxies` 项。
|
||||
|
||||
## 6. 风险与兼容性
|
||||
|
||||
| 风险 | 严重程度 | 缓解 |
|
||||
|---|---|---|
|
||||
| `trusted_proxies` 漏配导致限流器合并所有用户 IP | 高 | 验证清单必检项 |
|
||||
| CF IP 段变更,iptables 未同步 → 正常用户被挡 | 中 | 每日 cron 自动更新 |
|
||||
| 1C/1G 被 DDoS 打满连接 | 中 | CF 边缘 DDoS 防护 + 六层容灾 |
|
||||
| 回源链路明文被嗅探 | 中 | 自签证书 HTTPS |
|
||||
| 前端 VPS 成单点故障 | 低 | 备机 + DNS 快速切换 |
|
||||
| CF 域名接入期间 DNS 切换导致短暂不可用 | 低 | TTL 提前降为 60s |
|
||||
| nginx + sub2api 限流重复叠加 | 低 | nginx 限流是防攻击层(30r/s),sub2api 限流是业务层(auth 5-30/min),不冲突 |
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
- [ ] 中国大陆 IP 通过 `https://proxyapi.cyan2000.uk/health` 返回 200
|
||||
- [ ] 非中国 IP(或 VPN 测试)返回 403 / CF Block
|
||||
- [ ] sub2api 访问日志中 `client_ip` 是真实用户 IP,非 CF 或 nginx IP
|
||||
- [ ] 登录/注册接口限流:同 IP 超频返回 429,不同 IP 独立计数
|
||||
- [ ] 流式 API 请求 SSE 无缓冲延迟
|
||||
- [ ] 源站 IP 直连被 iptables DROP(公网 curl 源站 IP:443 超时)
|
||||
- [ ] `slowhttptest -c 50 -H` 后连接不耗尽,限流生效
|
||||
- [ ] nginx 进程被 kill 后 5s 内自动恢复(systemd)
|
||||
- [ ] 前端 error_log 无异常
|
||||
|
||||
## 8. 待确认项
|
||||
|
||||
- [ ] 轻量 VPS 月流量配额是否足够?每请求进出各一份,需估算日请求量
|
||||
- [ ] 是否需要支持香港/澳门 IP?(当前白名单仅 CN)
|
||||
- [ ] 后端防火墙是否支持按来源 IP 做入站限制?(除了 iptables,云厂商安全组也可做)
|
||||
- [ ] CF Full (strict) 模式下,源站证书用 Let's Encrypt 还是自签?Full (strict) 要求有效证书
|
||||
- [ ] sub2api 是否启用了 API Key 的 IP 白名单功能?(决定是否需要开 `trust_forwarded_ip_for_api_key_acl`)
|
||||
330
docs/agent-specs/2026-05-21-国际互联网络优化-执行版.md
Normal file
330
docs/agent-specs/2026-05-21-国际互联网络优化-执行版.md
Normal file
@ -0,0 +1,330 @@
|
||||
# 国际互联网络优化 — 执行版
|
||||
|
||||
日期:2026-05-21 | 前置:审阅版已通过
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
在 1C/1G 轻量 VPS 上部署 nginx 反向代理,通过 Cloudflare CDN 实现中国 IP 白名单访问控制,将流量转发到后端 2C4G 高性能服务器的 sub2api 服务。
|
||||
|
||||
## 2. 前置假设
|
||||
|
||||
- 轻量 VPS 已就绪,OS Debian/Ubuntu,有公网 IP,已配置 SSH 密钥登录
|
||||
- 高性能服务器已部署 sub2api(Docker Compose),监听 8080,`config.yaml` 可编辑
|
||||
- 域名 `proxyapi.cyan2000.uk` 的 NS 管理权限在手
|
||||
- 两台 VPS 之间通过公网 IP 通信(无内网)
|
||||
- 目标用户全部在中国大陆(+ 酌情香港/澳门)
|
||||
- Cloudflare 免费计划账户已注册
|
||||
|
||||
## 3. 涉及文件与路径
|
||||
|
||||
### 轻量 VPS(新部署)
|
||||
|
||||
| 路径 | 操作 | 说明 |
|
||||
|---|---|---|
|
||||
| `/etc/nginx/nginx.conf` | 写入 | 主配置,定义 upstream、限流区域 |
|
||||
| `/etc/nginx/ssl/fullchain.pem` | certbot 签发 | Let's Encrypt 证书 |
|
||||
| `/etc/nginx/ssl/privkey.pem` | certbot 签发 | 私钥 |
|
||||
| `/etc/systemd/system/nginx.service.d/override.conf` | 创建 | MemoryMax + Restart |
|
||||
| `/etc/fail2ban/jail.local` | 写入 | nginx-404 + nginx-limit-req 规则 |
|
||||
| `/etc/fail2ban/filter.d/nginx-404.conf` | 写入 | 404 扫描过滤 |
|
||||
| `/usr/local/bin/nginx-defense.sh` | 创建 | 内存压力自动降级 |
|
||||
| `/usr/local/bin/cf-iptables.sh` | 创建 | CF IP 白名单更新 |
|
||||
| `/usr/local/bin/rebuild-nginx.sh` | 创建 | 一键重建 |
|
||||
| `/etc/cron.d/cf-iptables` | 写入 | 每日 3:00 执行 |
|
||||
| `/etc/cron.d/nginx-defense` | 写入 | 每分钟执行 |
|
||||
|
||||
### 高性能服务器(配置变更)
|
||||
|
||||
| 路径 | 操作 | 说明 |
|
||||
|---|---|---|
|
||||
| `config.yaml` | 编辑 | `server.trusted_proxies` 加 nginx IP |
|
||||
| `config.yaml` | 编辑(条件) | `security.trust_forwarded_ip_for_api_key_acl: true` |
|
||||
| `/etc/ssl/private/sub2api-key.pem` | 生成 | 回源自签证书私钥 |
|
||||
| `/etc/ssl/certs/sub2api-cert.pem` | 生成 | 回源自签证书 |
|
||||
| `docker-compose.yml` | 编辑(可选) | nginx sidecar 做 TLS 终止 |
|
||||
|
||||
## 4. 数据流与依赖
|
||||
|
||||
```
|
||||
用户浏览器
|
||||
│ HTTPS 443 (Let's Encrypt)
|
||||
▼
|
||||
Cloudflare CDN
|
||||
│ 加头: CF-Connecting-IP, CF-IPCountry
|
||||
│ HTTPS 443 (Let's Encrypt)
|
||||
▼
|
||||
轻量VPS nginx (1C/1G)
|
||||
│ iptables: 仅接受 CF IP 段
|
||||
│ nginx: CF-IPCountry 非 CN → 403
|
||||
│ 加头: X-Real-IP, X-Forwarded-For
|
||||
│ HTTPS 8443 (自签证书)
|
||||
▼
|
||||
后端 nginx sidecar (TLS 终止)
|
||||
│ HTTP :8080
|
||||
▼
|
||||
sub2api (Gin)
|
||||
│ ip.GetClientIP() → CF-Connecting-IP → X-Real-IP → X-Forwarded-For
|
||||
▼
|
||||
PostgreSQL / Redis
|
||||
```
|
||||
|
||||
依赖:
|
||||
- 轻量 VPS → 后端公网 IP 可达(防火墙允许 8080 或 8443)
|
||||
- 后端 → Redis / PostgreSQL(已有)
|
||||
- 轻量 VPS → cloudflare.com(cron 拉取 IP 段)
|
||||
|
||||
## 5. 接口契约与字段变化
|
||||
|
||||
### sub2api 配置变化
|
||||
|
||||
```yaml
|
||||
# config.yaml 新增
|
||||
server:
|
||||
trusted_proxies:
|
||||
- "<轻量VPS公网IP>"
|
||||
- "173.245.48.0/20" # Cloudflare IP 段(如需)
|
||||
# ... 或信任所有 CF IP 段
|
||||
|
||||
# 如果启用了 API Key IP 白名单功能
|
||||
security:
|
||||
trust_forwarded_ip_for_api_key_acl: true
|
||||
```
|
||||
|
||||
### nginx 向下游传递的头
|
||||
|
||||
| 头名称 | 值来源 | 用途 |
|
||||
|---|---|---|
|
||||
| `X-Real-IP` | `$remote_addr`(CF 边缘 IP) | 传统兼容 |
|
||||
| `X-Forwarded-For` | `$proxy_add_x_forwarded_for` | 代理链 |
|
||||
| `X-Forwarded-Proto` | `$scheme`(https) | 协议感知 |
|
||||
| `CF-Connecting-IP` | `$http_cf_connecting_ip`(CF 原始头) | sub2api 优先读取 |
|
||||
| `Host` | `$host`(proxyapi.cyan2000.uk) | 虚拟主机 |
|
||||
|
||||
### sub2api IP 解析优先级(`ip.go` 已有,不变)
|
||||
|
||||
```
|
||||
CF-Connecting-IP → X-Real-IP → X-Forwarded-For(第一个公网IP) → c.ClientIP()
|
||||
```
|
||||
|
||||
`c.ClientIP()`(限流器使用)仅在 `trusted_proxies` 含来源 IP 时才信任转发头。**必须配置**。
|
||||
|
||||
## 6. 边界情况与失败路径
|
||||
|
||||
| 场景 | 预期行为 |
|
||||
|---|---|
|
||||
| 用户绕过 CF 直连源站 IP | iptables DROP,连接超时 |
|
||||
| 用户通过 CF 但伪造 CF-IPCountry 头 | CF 会覆盖此头或剥离,nginx 看到空 → 403 |
|
||||
| 用户通过 CF 但来自非中国 IP | CF WAF Block(第一层),nginx `$http_cf_ipcountry != CN` → 403(第二层) |
|
||||
| CF IP 段更新,iptables 未同步 | 受影响的新 CF IP 回源被 DROP,用户看到 502;每日 cron 最小化窗口 |
|
||||
| 轻量 VPS OOM | systemd MemoryMax=800M 杀 nginx 而非全机 OOM;Restart=always 5s 恢复 |
|
||||
| 轻量 VPS 彻底宕机 | 备机 DNS 切换;或一键重建脚本 |
|
||||
| 后端 502/504 | nginx 返回 502 给用户;`proxy_next_upstream error timeout http_502` 有限重试 |
|
||||
| sub2api `trusted_proxies` 未配 | 限流器把所有用户视为同一 IP(nginx IP),共享限流桶 |
|
||||
| CF-Connecting-IP 传递失败(nginx 没透传) | sub2api 回退到 X-Forwarded-For 解析,仍能拿到真实 IP |
|
||||
| Let's Encrypt 证书过期 | certbot renew 自动续期;需确保 80 端口可访问用于验证 |
|
||||
| 前端磁盘满 | access_log 生产环境关掉;error_log logrotate maxsize 50M |
|
||||
|
||||
## 7. 实施步骤
|
||||
|
||||
### 第一阶段:后端配置(零中断)
|
||||
|
||||
1. 登录高性能服务器
|
||||
2. 确认 `config.yaml` 路径,备份后编辑:
|
||||
```yaml
|
||||
server:
|
||||
trusted_proxies:
|
||||
- "<轻量VPS公网IP>"
|
||||
```
|
||||
3. 生成自签证书:
|
||||
```bash
|
||||
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||||
-keyout /etc/ssl/private/sub2api-key.pem \
|
||||
-out /etc/ssl/certs/sub2api-cert.pem \
|
||||
-subj "/CN=<后端公网IP>"
|
||||
```
|
||||
4. 在 `docker-compose.yml` 中添加 nginx sidecar 做 TLS 终止(监听 8443 → 转发 8080),或直接用 sub2api 的 HTTPS 配置
|
||||
5. 重启 sub2api / Docker Compose
|
||||
6. 验证:`curl --noproxy '*' -k https://<后端公网IP>:8443/health` 返回 200
|
||||
|
||||
### 第二阶段:Cloudflare DNS(可能中断 1-2 分钟)
|
||||
|
||||
1. 在 Cloudflare 添加站点 `proxyapi.cyan2000.uk`
|
||||
2. 将域名 NS 改为 Cloudflare 分配的 NS
|
||||
3. 等 NS 生效后,添加 DNS 记录:A `@` → 轻量 VPS 公网 IP,橙色云朵开启
|
||||
4. SSL/TLS 模式设为 Full (strict)
|
||||
5. WAF 规则:`(ip.geoip.country eq "CN")` → Allow,新建规则
|
||||
6. 验证:`curl -H "Host: proxyapi.cyan2000.uk" https://<轻量VPS_IP>/health --resolve proxyapi.cyan2000.uk:443:<轻量VPS_IP>` — 此时 nginx 还没配,先确认 CF 能到 VPS
|
||||
|
||||
### 第三阶段:轻量 VPS 部署
|
||||
|
||||
按顺序执行:
|
||||
|
||||
```bash
|
||||
# 1. 基础软件
|
||||
apt update && apt install -y nginx certbot fail2ban iptables-persistent
|
||||
|
||||
# 2. 获取证书(先关 nginx 80/443,或 certbot standalone 模式)
|
||||
certbot certonly --standalone -d proxyapi.cyan2000.uk
|
||||
|
||||
# 3. 写入 /etc/nginx/nginx.conf(见附录 A)
|
||||
|
||||
# 4. 写入 systemd override(见附录 B)
|
||||
mkdir -p /etc/systemd/system/nginx.service.d
|
||||
|
||||
# 5. 写入 fail2ban 配置(见附录 C)
|
||||
|
||||
# 6. 写入脚本(见附录 D)
|
||||
# - /usr/local/bin/cf-iptables.sh
|
||||
# - /usr/local/bin/nginx-defense.sh
|
||||
# - /usr/local/bin/rebuild-nginx.sh
|
||||
|
||||
# 7. 执行 iptables 白名单(先 accept 所有,确认 nginx 正常后再启用 DROP)
|
||||
bash /usr/local/bin/cf-iptables.sh
|
||||
|
||||
# 8. cron
|
||||
echo "0 3 * * * root /usr/local/bin/cf-iptables.sh" > /etc/cron.d/cf-iptables
|
||||
echo "* * * * * root /usr/local/bin/nginx-defense.sh" > /etc/cron.d/nginx-defense
|
||||
|
||||
# 9. 关闭 access_log(生产环境)
|
||||
# 编辑 nginx.conf: access_log off;
|
||||
# 或保留但 logrotate: maxsize 50M rotate 3
|
||||
|
||||
# 10. 启动
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now nginx fail2ban
|
||||
```
|
||||
|
||||
### 第四阶段:端到端验证
|
||||
|
||||
见第 8 节验证清单。
|
||||
|
||||
## 8. 测试与验证
|
||||
|
||||
### 自动化测试
|
||||
|
||||
```bash
|
||||
# 1. 基本连通性
|
||||
curl -I https://proxyapi.cyan2000.uk/health
|
||||
# 预期: 200 OK
|
||||
|
||||
# 2. 中国 IP 访问控制 (CF-IPCountry 头)
|
||||
curl -I https://proxyapi.cyan2000.uk/ -H "CF-IPCountry: CN"
|
||||
# 预期: 正常转发,非 403
|
||||
curl -I https://proxyapi.cyan2000.uk/ -H "CF-IPCountry: US"
|
||||
# 预期: 403 Forbidden
|
||||
curl -I https://proxyapi.cyan2000.uk/
|
||||
# 预期: 403 (CF-IPCountry 为空 = 直连)
|
||||
|
||||
# 3. 压力测试 — 限流
|
||||
ab -n 200 -c 50 https://proxyapi.cyan2000.uk/health
|
||||
# 预期: 部分请求返回 429 / 503
|
||||
|
||||
# 4. Slowloris 抗性
|
||||
slowhttptest -c 50 -H -u https://proxyapi.cyan2000.uk/
|
||||
# 预期: 连接不耗尽,正常请求仍能响应
|
||||
|
||||
# 5. 源站 IP 直连
|
||||
curl -I --connect-timeout 5 https://<轻量VPS_IP>/
|
||||
# 预期: 超时或被拒绝 (iptables DROP)
|
||||
|
||||
# 6. 限流验证 — 真实 IP
|
||||
for i in $(seq 1 35); do
|
||||
curl -s -o /dev/null -w "%{http_code}\n" https://proxyapi.cyan2000.uk/auth/login
|
||||
done
|
||||
# 预期: 前 20 个 200/401,后续 429
|
||||
```
|
||||
|
||||
### 验证清单
|
||||
|
||||
- [ ] `curl https://proxyapi.cyan2000.uk/health` → 200
|
||||
- [ ] CF 关闭橙色云朵(直连 VPS)→ `CF-IPCountry` 为空 → 403
|
||||
- [ ] `CF-IPCountry: US` → 403
|
||||
- [ ] `CF-IPCountry: CN` → 正常转发
|
||||
- [ ] sub2api 访问日志 `client_ip` = 真实用户 IP(查看日志验证)
|
||||
- [ ] auth 限流:同 IP 超频返回 429,不同 IP 独立计数
|
||||
- [ ] 流式 API SSE 逐块到达
|
||||
- [ ] 源站 IP 直连超时
|
||||
- [ ] `ab` 压测限流生效
|
||||
- [ ] `slowhttptest` 连接不耗尽
|
||||
- [ ] `fail2ban-client status` 正常
|
||||
- [ ] `systemctl show nginx | grep Restart` → `Restart=always`
|
||||
- [ ] `systemctl show nginx | grep MemoryMax` → `MemoryMax=838860800`
|
||||
- [ ] nginx error_log 无异常
|
||||
|
||||
## 9. 回滚 / 降级
|
||||
|
||||
### 快速回滚 DNS
|
||||
1. Cloudflare DNS 关闭 A 记录橙色云朵(变灰色 = DNS only)
|
||||
2. 或将 A 记录直接指向后端服务器 IP(绕过轻量 VPS)
|
||||
|
||||
### 回滚 sub2api 配置
|
||||
```bash
|
||||
# 注释掉 trusted_proxies 行,恢复原状
|
||||
# 重启 sub2api
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
### 轻量 VPS 紧急降级
|
||||
如果 nginx 出现异常但 iptables 正常:
|
||||
```bash
|
||||
# 紧急放行所有流量
|
||||
iptables -F CF-WHITELIST
|
||||
iptables -A CF-WHITELIST -j ACCEPT
|
||||
# 重启 nginx
|
||||
systemctl restart nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:nginx 主配置
|
||||
|
||||
见 `NETWORK_OPTIMIZATION.md` 3.2 节完整配置。
|
||||
|
||||
## 附录 B:systemd override
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/nginx.service.d/override.conf
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
OOMScoreAdjust=-500
|
||||
MemoryMax=800M
|
||||
MemoryHigh=700M
|
||||
```
|
||||
|
||||
## 附录 C:fail2ban 配置
|
||||
|
||||
```ini
|
||||
# /etc/fail2ban/jail.local
|
||||
[nginx-404]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = nginx-404
|
||||
logpath = /var/log/nginx/access.log
|
||||
maxretry = 30
|
||||
findtime = 60
|
||||
bantime = 600
|
||||
|
||||
[nginx-limit-req]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = nginx-limit-req
|
||||
logpath = /var/log/nginx/error.log
|
||||
maxretry = 5
|
||||
findtime = 60
|
||||
bantime = 600
|
||||
```
|
||||
|
||||
```ini
|
||||
# /etc/fail2ban/filter.d/nginx-404.conf
|
||||
[Definition]
|
||||
failregex = ^<HOST> -.*"(GET|POST|HEAD).*" 404
|
||||
ignoreregex =
|
||||
```
|
||||
|
||||
## 附录 D:脚本
|
||||
|
||||
见 `NETWORK_OPTIMIZATION.md` 3.4 节:
|
||||
- `cf-iptables.sh` — CF IP 白名单(含 IPv6)
|
||||
- `nginx-defense.sh` — 内存压力自动降级
|
||||
- `rebuild-nginx.sh` — 一键重建
|
||||
1028
docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md
Normal file
1028
docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md
Normal file
File diff suppressed because it is too large
Load Diff
42
docs/memorys/2026-05-25-proxy-ng职责边界.md
Normal file
42
docs/memorys/2026-05-25-proxy-ng职责边界.md
Normal file
@ -0,0 +1,42 @@
|
||||
# proxy-ng 职责边界
|
||||
|
||||
日期:2026-05-25
|
||||
|
||||
## 背景
|
||||
|
||||
本次讨论 `proxy-ng` 多节点中转方案:一个域名可配置多个 nginx 节点 IP,用于优化用户到 sub2api 的入口线路。过程中曾讨论是否让这些节点同时作为 sub2api 账号的出口代理,最终确认不应这样做。
|
||||
|
||||
## 已确认边界
|
||||
|
||||
- `proxy-ng` 只负责入口中转和线路优化。
|
||||
- `proxy-ng` 节点只运行 nginx 反向代理相关能力。
|
||||
- `proxy-ng` 节点不承担 sub2api 账号出口代理职责。
|
||||
- sub2api 的账号代理应由独立代理池解决,不放进 `proxy-ng` 设计。
|
||||
- `proxy-ng` 节点不应暴露 `1080`、`7890`、`mixed`、`socks` 等代理端口。
|
||||
- `proxy-ng` 节点不部署 sing-box、Clash、Xray 等出口代理服务。
|
||||
|
||||
## 推荐架构
|
||||
|
||||
```text
|
||||
用户 -> proxy-ng/nginx -> 后端入口网关 -> sub2api
|
||||
sub2api 账号代理 -> 独立代理池
|
||||
```
|
||||
|
||||
## 安全原因
|
||||
|
||||
- 避免中转节点变成公网开放代理。
|
||||
- 避免入口中转流量和账号出口流量互相抢带宽、连接数和 CPU。
|
||||
- 避免把“线路优化”和“账号代理池”两个生命周期不同的系统绑在一起。
|
||||
- 避免后续排障时无法区分入口链路问题、上游代理池问题和 sub2api 业务问题。
|
||||
|
||||
## 当前项目资产
|
||||
|
||||
- `proxy-ng/README.md`:记录多节点中转架构、职责边界、风险和落地顺序。
|
||||
- `proxy-ng/env.example`:只保留入口中转所需变量。
|
||||
- `proxy-ng/nginx/proxy-node.conf.template`:proxy-ng 节点 nginx 模板。
|
||||
- `proxy-ng/nginx/backend-gateway.conf.template`:后端入口网关模板。
|
||||
- `proxy-ng/scripts/install-proxy-node.sh`:proxy-ng 节点安装脚本骨架。
|
||||
|
||||
## 后续执行约束
|
||||
|
||||
以后修改 `proxy-ng/` 时,先检查本文件和 `proxy-ng/README.md`。除非用户明确重新改变架构边界,否则不要把账号出口代理、代理池、sing-box、Clash、Xray 或 SOCKS 端口重新放回 `proxy-ng`。
|
||||
29
docs/memorys/2026-05-26-catproxy访问边界变更.md
Normal file
29
docs/memorys/2026-05-26-catproxy访问边界变更.md
Normal file
@ -0,0 +1,29 @@
|
||||
# catproxy 访问边界变更
|
||||
|
||||
日期:2026-05-26
|
||||
|
||||
## 当前结论
|
||||
|
||||
- `catproxy.lilifamily.com` 不做 IP 白名单限制,是 3c4g 上可直接访问的主站域名和回滚入口。
|
||||
- `origin.proxy.api.lilifamily.com` 才是回源专用域名,继续限制 proxy-ng 节点来源 IP,并校验 `X-Proxy-Ng-Token`。
|
||||
- `proxy.api.lilifamily.com` 仍作为 proxy-ng 公开入口,目前指向 JPpro 节点。
|
||||
|
||||
## 已执行变更
|
||||
|
||||
- 服务器:`个人-US-racknerd-0526-3c4g`
|
||||
- 远程配置:`/etc/nginx/sites-available/catproxy-sub2api.conf`
|
||||
- 备份路径:`/etc/nginx/sites-available/catproxy-sub2api.conf.bak-20260526043650`
|
||||
- 删除内容:`allow 106.37.220.142` 与 `deny all`
|
||||
- 保留内容:`origin.proxy.api.lilifamily.com` 的 `allow 151.242.164.72`、`deny all` 和 token 校验
|
||||
|
||||
## 验证结果
|
||||
|
||||
- `nginx -t` 成功。
|
||||
- `systemctl reload nginx` 成功。
|
||||
- `https://catproxy.lilifamily.com/health` 返回 `200`。
|
||||
- 非白名单来源访问 `https://origin.proxy.api.lilifamily.com/health` 返回 `403`。
|
||||
- `https://proxy.api.lilifamily.com/health` 经 JPpro 返回 `200`,响应头包含 `X-Proxy-Ng-Node: jppro-akile-0504-1c1g`。
|
||||
|
||||
## 后续注意
|
||||
|
||||
不要再把 `catproxy.lilifamily.com` 配成管理 IP 白名单入口。后续新增 proxy-ng 节点时,只需要把新节点公网 IP 补到 `origin.proxy.api.lilifamily.com` 的 allowlist,并给节点配置同一个受控 token 文件。
|
||||
81
docs/memorys/2026-05-26-主站测速与限速核对.md
Normal file
81
docs/memorys/2026-05-26-主站测速与限速核对.md
Normal file
@ -0,0 +1,81 @@
|
||||
# 主站测速与限速核对
|
||||
|
||||
日期:2026-05-26
|
||||
|
||||
## 本机链路测速结论
|
||||
|
||||
从当前本机网络直连测试,`proxy.api.lilifamily.com` 经 JPpro 的 HTTPS 轻量请求比 `catproxy.lilifamily.com` 直连 3c4g 更快、更稳。
|
||||
|
||||
| 测试项 | 直连 `catproxy.lilifamily.com` | 经 JPpro `proxy.api.lilifamily.com` |
|
||||
|---|---:|---:|
|
||||
| `/health` 中位总耗时 | `0.763s` | `0.559s` |
|
||||
| `/health` 成功率 | `5/6`,1 次 8s 超时 | `6/6` |
|
||||
| 首页 `HEAD /` 中位耗时 | `1.817s` | `1.315s` |
|
||||
| ICMP 平均延迟 | `238ms`,10% 丢包 | `57ms`,30% 丢包 |
|
||||
|
||||
测试命令使用 `curl --noproxy '*' -4`,本机未检测到 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 环境变量。ICMP 结果只作参考,HTTPS curl 结果优先。
|
||||
|
||||
## nginx 限速边界
|
||||
|
||||
- `catproxy.lilifamily.com`:3c4g 主站直连入口,当前没有 nginx `limit_req` / `limit_conn`,也没有 IP 白名单。
|
||||
- `origin.proxy.api.lilifamily.com`:3c4g 回源入口,当前没有 nginx 速率限制;只允许 JPpro IP `151.242.164.72`,并校验 `X-Proxy-Ng-Token`。
|
||||
- `proxy.api.lilifamily.com` / `jp.proxy.api.lilifamily.com`:JPpro 入口节点,有 nginx 按客户端 IP 的速率和并发限制。
|
||||
|
||||
JPpro 当前入口限制:
|
||||
|
||||
```nginx
|
||||
limit_req_zone $binary_remote_addr zone=req_per_ip:20m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=auth_per_ip:10m rate=10r/m;
|
||||
limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m;
|
||||
|
||||
limit_conn conn_per_ip 50;
|
||||
limit_req zone=req_per_ip burst=30 nodelay;
|
||||
limit_req_status 429;
|
||||
```
|
||||
|
||||
认证相关路径更严格:
|
||||
|
||||
```nginx
|
||||
limit_req zone=auth_per_ip burst=3 nodelay;
|
||||
limit_conn conn_per_ip 10;
|
||||
```
|
||||
|
||||
## Sub2API 业务请求限速维度
|
||||
|
||||
Sub2API 的模型/API 网关业务请求不是按 IP 限速,而是按用户、用户+分组、API Key 额度做限制。
|
||||
|
||||
业务 RPM 计数 Redis key:
|
||||
|
||||
```text
|
||||
rpm:u:{userID}:{minute}
|
||||
rpm:ug:{userID}:{groupID}:{minute}
|
||||
```
|
||||
|
||||
含义:
|
||||
|
||||
- 用户级 `rpm_limit`:同一用户全局每分钟请求数上限,`0` 表示不限。
|
||||
- 分组级 `rpm_limit`:同一用户在某个分组内每分钟请求数上限,`0` 表示不限。
|
||||
- 用户-分组 `rpm_override`:管理员可给某个用户在某个分组设置专属 RPM。
|
||||
- API Key 的 `rate_limit_5h` / `rate_limit_1d` / `rate_limit_7d`:按费用消耗窗口限制,不是请求次数。
|
||||
|
||||
登录、注册、验证码等公开认证接口另有应用内 Redis 限速,维度是 `c.ClientIP()`。
|
||||
|
||||
## 生产快照
|
||||
|
||||
通过 Tabby profile `个人-US-racknerd-0526-3c4g` 查询 PostgreSQL 的非敏感限速字段:
|
||||
|
||||
- 用户 `zizi2000email@gmail.com`:`rpm_limit = 0`。
|
||||
- 分组 `briqt`:`rpm_limit = 40`。
|
||||
- 分组 `omm`:`rpm_limit = 100`。
|
||||
- 分组 `free日卡-20刀`、`plus号池-使用余额`、`free周卡-每天50刀`、`free月卡-每天60刀`:`rpm_limit = 20`。
|
||||
- 分组 `pro号池-使用余额`:`rpm_limit = 30`。
|
||||
- API Key `briqt`:`rate_limit_5h = 300`,`rate_limit_1d = 500`。
|
||||
- API Key `free`:`rate_limit_1d = 50`。
|
||||
- API Key `omm`:窗口额度均为 `0`,表示不限。
|
||||
|
||||
后续如果用户反馈“主站请求被限速”,先判断入口:
|
||||
|
||||
1. 走 `proxy.api.lilifamily.com` 时,先看 JPpro nginx 是否返回 `429`。
|
||||
2. 走 `catproxy.lilifamily.com` 时,跳过 nginx IP 限速排查,直接看 Sub2API 业务层错误。
|
||||
3. 如果错误是 `rate_limit_exceeded`,优先查用户/分组 RPM 或 API Key 窗口额度。
|
||||
4. 如果错误来自上游模型账号 `rate_limit_error`,那是账号池或上游限流,不是用户/IP 限流。
|
||||
1041
docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md
Normal file
1041
docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md
Normal file
File diff suppressed because it is too large
Load Diff
31
ops/remote/README.md
Normal file
31
ops/remote/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Remote Deployment Inventory
|
||||
|
||||
本目录记录 Sub2API 远程部署事实。每台服务器必须单独一个子目录,目录名建议使用:
|
||||
|
||||
```text
|
||||
ops/remote/<provider-or-region>-<host-date>-sub2api/
|
||||
```
|
||||
|
||||
## 当前目录
|
||||
|
||||
| 目录 | Tabby profile | 公网入口 | 状态 |
|
||||
|------|---------------|----------|------|
|
||||
| `jppro-sub2api/` | `个人-JPpro-akile-0504-1c1g` | `https://proxy.api.lilifamily.com` / `https://jp.proxy.api.lilifamily.com` | proxy-ng 公开入口节点,回源到 3c4g origin |
|
||||
| `us-racknerd-0425-sub2api/` | `个人-US-racknerd-0425-1c1g` | `https://pai.cyan2000.uk` | 旧机,迁移来源和回滚来源 |
|
||||
| `us-racknerd-0526-sub2api/` | `个人-US-racknerd-0526-3c4g` | `https://origin.proxy.api.lilifamily.com` / `https://catproxy.lilifamily.com` | Sub2API 受限 origin 和公开直连主站入口 |
|
||||
|
||||
## 留痕规则
|
||||
|
||||
每个服务器目录至少包含:
|
||||
|
||||
- `README.md`:域名、IP、服务器名、Tabby profile、部署方式、部署架构、运行检查和操作记录。
|
||||
- `current/`:当前远程实际生效配置副本,例如 `docker-compose.yml`、`nginx-sub2api.conf` 或 `Caddyfile`。
|
||||
- `secrets/README.md`:说明哪些真实密钥可在本机私有保存,但不得提交。
|
||||
- `.gitignore`:排除 `.env`、密钥、订阅、临时备份和数据库备份。
|
||||
- `backups/`:只保留 `.gitkeep`、非敏感说明或 `*.SHA256SUMS` 校验记录;真实备份文件不得提交。
|
||||
|
||||
## 变更要求
|
||||
|
||||
- 迁移、部署、域名切换、证书、反代、Compose、数据库、Redis、Mihomo 或代理配置变化后,必须同步更新对应服务器目录。
|
||||
- 根 `AGENTS.md` 只保留规则和索引;具体运行事实沉到本目录的服务器子目录。
|
||||
- 未验证的信息必须标注为“目标”“计划”或“待确认”,不能写成当前事实。
|
||||
10
ops/remote/jppro-sub2api/.gitignore
vendored
Normal file
10
ops/remote/jppro-sub2api/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Remote runtime secrets and ad-hoc backups.
|
||||
secrets/*
|
||||
!secrets/README.md
|
||||
backups/*
|
||||
!backups/.gitkeep
|
||||
|
||||
# Never commit copied environment files.
|
||||
*.env
|
||||
.env
|
||||
.env.*
|
||||
147
ops/remote/jppro-sub2api/README.md
Normal file
147
ops/remote/jppro-sub2api/README.md
Normal file
@ -0,0 +1,147 @@
|
||||
# JPpro proxy-ng Remote Inventory
|
||||
|
||||
最后核对时间:2026-05-26 19:10 Asia/Shanghai
|
||||
|
||||
## 远程位置
|
||||
|
||||
- Tabby profile:`个人-JPpro-akile-0504-1c1g`
|
||||
- Hostname:`jppro2-20260504201455dc6fca`
|
||||
- 公网 IP:`151.242.164.72`
|
||||
- 公开入口:`https://proxy.api.lilifamily.com`
|
||||
- 节点入口:`https://jp.proxy.api.lilifamily.com`
|
||||
- DNS:`proxy.api.lilifamily.com -> 151.242.164.72`,`jp.proxy.api.lilifamily.com -> 151.242.164.72`
|
||||
- 回源目标:`https://origin.proxy.api.lilifamily.com`
|
||||
- 当前状态:proxy-ng 公开入口节点;不运行 Sub2API、PostgreSQL、Redis、Caddy 或账号出口代理
|
||||
|
||||
## 本地留痕
|
||||
|
||||
- 当前远程状态说明:`current/README.md`
|
||||
- 当前 nginx 站点配置副本:`current/nginx-jp-proxy-sub2api.conf`
|
||||
- 当前 nginx map 配置副本:`current/proxy-ng-map.conf`
|
||||
- 加固记录:`current/proxy-ng-hardening-notes.md`
|
||||
|
||||
`secrets/` 目录允许在本机私有保存历史 `.env`、临时 admin 密码或其他敏感材料,但这些文件已被 `.gitignore` 排除,不能提交,也不能视为当前远程生效配置。
|
||||
|
||||
## 当前部署架构
|
||||
|
||||
```text
|
||||
Client
|
||||
↓ HTTPS :443 proxy.api.lilifamily.com / jp.proxy.api.lilifamily.com
|
||||
JPpro nginx proxy-ng
|
||||
↓ HTTPS + X-Proxy-Ng-Token
|
||||
3c4g nginx origin.proxy.api.lilifamily.com
|
||||
↓ 127.0.0.1:8080
|
||||
Sub2API
|
||||
```
|
||||
|
||||
JPpro 节点职责边界:
|
||||
|
||||
- 只做入口反向代理和线路中转。
|
||||
- 不保存 Sub2API 业务状态。
|
||||
- 不持有 Sub2API 数据库密码、Redis 密码或业务 API Key。
|
||||
- 不部署 Clash、sing-box、Xray、SOCKS、mixed 等账号出口代理能力。
|
||||
- 持有 TLS 证书私钥和 proxy-ng 回源 token;token 仅在远程 root-only 文件中保存,不进 Git。
|
||||
|
||||
## 当前部署方式
|
||||
|
||||
- 安装方式:Debian apt 安装 nginx、certbot、python3-certbot-nginx、curl、ca-certificates、fail2ban、logrotate。
|
||||
- nginx 版本:`nginx/1.26.3`
|
||||
- Certbot 版本:`certbot 4.0.0`
|
||||
- 证书路径:`/etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem`
|
||||
- 私钥路径:`/etc/letsencrypt/live/proxy.api.lilifamily.com/privkey.pem`
|
||||
- 证书覆盖域名:`proxy.api.lilifamily.com`、`jp.proxy.api.lilifamily.com`
|
||||
- 证书到期时间:`2026-08-24 10:01:14 UTC`
|
||||
- 远程站点配置:`/etc/nginx/sites-available/jp-proxy-sub2api.conf`
|
||||
- 远程站点启用链接:`/etc/nginx/sites-enabled/jp-proxy-sub2api.conf`
|
||||
- 远程 map 配置:`/etc/nginx/conf.d/proxy-ng-map.conf`
|
||||
- 远程 token 配置:`/etc/nginx/proxy-ng/token.conf`,权限 `600 root root`
|
||||
|
||||
关键代理配置:
|
||||
|
||||
```text
|
||||
proxy_pass https://origin.proxy.api.lilifamily.com;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name origin.proxy.api.lilifamily.com;
|
||||
proxy_set_header Host origin.proxy.api.lilifamily.com;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g";
|
||||
proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
```
|
||||
|
||||
## DNS state after proxy-ng hardening
|
||||
|
||||
| Name | A records | Role |
|
||||
|---|---|---|
|
||||
| `proxy.api.lilifamily.com` | `151.242.164.72` | Public entry |
|
||||
| `jp.proxy.api.lilifamily.com` | `151.242.164.72` | JPpro proxy-ng node |
|
||||
| `catproxy.lilifamily.com` | `216.45.59.243` | Public direct main-site and rollback entry |
|
||||
| `origin.proxy.api.lilifamily.com` | `216.45.59.243` | Proxy-ng origin only |
|
||||
|
||||
## 运行检查
|
||||
|
||||
```bash
|
||||
nginx -t
|
||||
systemctl is-active nginx
|
||||
systemctl is-active fail2ban
|
||||
fail2ban-client status nginx-proxy-ng-scan
|
||||
openssl x509 -noout -enddate -in /etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem
|
||||
ss -tulpn
|
||||
curl --noproxy '*' -sS https://proxy.api.lilifamily.com/health
|
||||
curl --noproxy '*' -sS https://jp.proxy.api.lilifamily.com/health
|
||||
curl --noproxy '*' -i -X POST https://proxy.api.lilifamily.com/v1/responses \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data '{"model":"gpt-5.5","input":"ping"}'
|
||||
```
|
||||
|
||||
期望结果:
|
||||
|
||||
- `nginx -t` 返回配置语法正确。
|
||||
- nginx 服务状态为 `active`。
|
||||
- fail2ban 服务状态为 `active`,启用 `nginx-proxy-ng-scan` 和 `sshd` jail。
|
||||
- 监听端口包含 `22`、`80`、`443`。
|
||||
- `/health` 返回 `200 {"status":"ok"}`,响应头包含 `X-Proxy-Ng-Node: jppro-akile-0504-1c1g`。
|
||||
- 未授权 `/v1/responses` 返回 `401 API_KEY_REQUIRED`,证明请求已到达后端 API 层。
|
||||
|
||||
## 操作记录
|
||||
|
||||
### 2026-05-24 admin 密码重置
|
||||
|
||||
- 账号:`zizi2000email@gmail.com`
|
||||
- 结果:登录接口已验证成功。
|
||||
- 本地临时密码留痕:`secrets/admin-reset-20260524-165937.txt`
|
||||
- 远程旧密码 hash 恢复 SQL:`/opt/sub2api/backups/password-reset-20260524-170007/restore-admin-password.sql`
|
||||
|
||||
临时密码文件被 `.gitignore` 排除,且应保持 `600` 权限。登录后应尽快在后台修改为正式密码。
|
||||
|
||||
### 2026-05-26 清理 JPpro 部署
|
||||
|
||||
- 执行范围:只清理 `个人-JPpro-akile-0504-1c1g` 上的 Sub2API Compose 栈,不动 RackNerd 两台服务器。
|
||||
- 远程确认:hostname 为 `jppro2-20260504201455dc6fca`,清理前存在 `/opt/sub2api`,运行 `sub2api`、`sub2api-caddy`、`sub2api-postgres`、`sub2api-redis` 四个容器。
|
||||
- 已执行:`docker compose down --volumes --remove-orphans --rmi all --timeout 30`。
|
||||
- 已删除:四个容器、`sub2api_sub2api_data`、`sub2api_postgres_data`、`sub2api_redis_data`、`sub2api_caddy_data`、`sub2api_caddy_config`、`sub2api_sub2api-network`、四个服务镜像和 `/opt/sub2api`。
|
||||
- 验证结果:`docker ps -a` 无容器,匹配 `sub2api/postgres_data/redis_data/caddy_data/caddy_config` 的卷为空,匹配 Sub2API/Caddy/PostgreSQL/Redis 的镜像为空,宿主机监听端口只剩 `22`。
|
||||
|
||||
### 2026-05-26 部署 JPpro proxy-ng 入口中转
|
||||
|
||||
- 执行范围:只在 `个人-JPpro-akile-0504-1c1g` 上部署 nginx 入口中转,不在该节点恢复 Sub2API Compose 栈。
|
||||
- 域名:`jp.proxy.api.lilifamily.com`
|
||||
- DNS:`jp.proxy.api.lilifamily.com -> 151.242.164.72`
|
||||
- 初始回源:`https://proxy.api.lilifamily.com`
|
||||
- 已安装:`nginx/1.26.3`、`certbot 4.0.0`、`python3-certbot-nginx`、`curl`、`ca-certificates`。
|
||||
- 已配置:`/etc/nginx/sites-available/jp-proxy-sub2api.conf`、`/etc/nginx/conf.d/proxy-ng-map.conf`。
|
||||
- 已签发初始证书:`/etc/letsencrypt/live/jp.proxy.api.lilifamily.com/fullchain.pem`,到期时间 `2026-08-24 06:22:03 UTC`。
|
||||
- 公网验证:`https://jp.proxy.api.lilifamily.com/health` 返回 `200 {"status":"ok"}`,根页面返回 `200`,未授权 `POST /v1/responses` 返回 `401 API_KEY_REQUIRED`。
|
||||
|
||||
### 2026-05-26 proxy-ng origin hardening
|
||||
|
||||
- 3c4g 生成 proxy-ng token,JPpro 通过 root-only 文件 `/etc/nginx/proxy-ng/token.conf` 引用。
|
||||
- JPpro 回源从 `https://proxy.api.lilifamily.com` 改为 `https://origin.proxy.api.lilifamily.com`。
|
||||
- `proxy.api.lilifamily.com` DNS 切换到 `151.242.164.72`。
|
||||
- JPpro 使用 HTTP-01 签发覆盖 `proxy.api.lilifamily.com` 和 `jp.proxy.api.lilifamily.com` 的证书。
|
||||
- 远程备份:`/root/proxy-ng-hardening-backup-20260526-183749`。
|
||||
- 验证结果:`https://proxy.api.lilifamily.com/health` 返回 `200 {"status":"ok"}`,未授权 `POST /v1/responses` 返回 `401 API_KEY_REQUIRED`,响应头包含 `X-Proxy-Ng-Node: jppro-akile-0504-1c1g`。
|
||||
- 防滥用:启用 nginx 连接数/请求速率限制、扫描路径 444、fail2ban `nginx-proxy-ng-scan`、nginx logrotate。
|
||||
1
ops/remote/jppro-sub2api/backups/.gitkeep
Normal file
1
ops/remote/jppro-sub2api/backups/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
15
ops/remote/jppro-sub2api/current/README.md
Normal file
15
ops/remote/jppro-sub2api/current/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# JPpro Current State
|
||||
|
||||
最后核对时间:2026-05-26 19:10 Asia/Shanghai
|
||||
|
||||
JPpro 当前作为 `proxy-ng` 公开入口节点使用,只运行 nginx 反向代理,不运行 Sub2API、PostgreSQL、Redis、Caddy 或账号出口代理。
|
||||
|
||||
- 公开入口:`https://proxy.api.lilifamily.com`
|
||||
- 节点入口:`https://jp.proxy.api.lilifamily.com`
|
||||
- 回源目标:`https://origin.proxy.api.lilifamily.com`
|
||||
- nginx 站点配置:`/etc/nginx/sites-available/jp-proxy-sub2api.conf`
|
||||
- nginx map 配置:`/etc/nginx/conf.d/proxy-ng-map.conf`
|
||||
- token 文件:`/etc/nginx/proxy-ng/token.conf`,远程 root-only,不入 Git
|
||||
- 本地配置副本:`current/nginx-jp-proxy-sub2api.conf`、`current/proxy-ng-map.conf`
|
||||
- 监听端口:SSH `22`、HTTP `80`、HTTPS `443`
|
||||
- 证书:`/etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem`,覆盖 `proxy.api.lilifamily.com` 和 `jp.proxy.api.lilifamily.com`
|
||||
93
ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf
Normal file
93
ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf
Normal file
@ -0,0 +1,93 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name proxy.api.lilifamily.com jp.proxy.api.lilifamily.com;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
default_type text/plain;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name proxy.api.lilifamily.com jp.proxy.api.lilifamily.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/proxy.api.lilifamily.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
include /etc/nginx/proxy-ng/token.conf;
|
||||
|
||||
server_tokens off;
|
||||
underscores_in_headers on;
|
||||
client_max_body_size 100m;
|
||||
client_header_timeout 10s;
|
||||
client_body_timeout 30s;
|
||||
send_timeout 30s;
|
||||
keepalive_timeout 20s;
|
||||
keepalive_requests 100;
|
||||
|
||||
limit_conn conn_per_ip 50;
|
||||
limit_req zone=req_per_ip burst=30 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
add_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
|
||||
location ~ /\.(git|svn|hg|env|aws|ssh) {
|
||||
return 444;
|
||||
}
|
||||
|
||||
location ~* ^/(wp-admin|wp-login\.php|phpmyadmin|cgi-bin|vendor|boaform) {
|
||||
return 444;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/auth/(login|login/2fa|register|send-verify-code|forgot-password|reset-password) {
|
||||
limit_req zone=auth_per_ip burst=3 nodelay;
|
||||
limit_conn conn_per_ip 10;
|
||||
|
||||
proxy_pass https://origin.proxy.api.lilifamily.com;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name origin.proxy.api.lilifamily.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host origin.proxy.api.lilifamily.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g";
|
||||
proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass https://origin.proxy.api.lilifamily.com;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name origin.proxy.api.lilifamily.com;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host origin.proxy.api.lilifamily.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g";
|
||||
proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
13
ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md
Normal file
13
ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md
Normal file
@ -0,0 +1,13 @@
|
||||
# JPpro proxy-ng hardening notes
|
||||
|
||||
日期:2026-05-26
|
||||
|
||||
- Public host served: `proxy.api.lilifamily.com`, `jp.proxy.api.lilifamily.com`
|
||||
- Origin: `https://origin.proxy.api.lilifamily.com`
|
||||
- Token: stored on remote only at `/etc/nginx/proxy-ng/token.conf`, redacted from Git
|
||||
- Certificate: `/etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem`, SAN includes both public and node hostnames
|
||||
- Scan-path blocking: enabled for dotfiles, wp-login, phpmyadmin, cgi-bin, vendor, boaform
|
||||
- Rate limit: general `10r/s`, auth `10r/m`, connection limit `50` per IP on public server
|
||||
- fail2ban: `nginx-proxy-ng-scan` and `sshd` active
|
||||
- logrotate: `/etc/logrotate.d/nginx-proxy-ng`
|
||||
- Validation: public `/health` returned `200`, unauthenticated `/v1/responses` returned `401 API_KEY_REQUIRED`, scan paths returned closed connection/444 behavior
|
||||
8
ops/remote/jppro-sub2api/current/proxy-ng-map.conf
Normal file
8
ops/remote/jppro-sub2api/current/proxy-ng-map.conf
Normal file
@ -0,0 +1,8 @@
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
"" close;
|
||||
}
|
||||
|
||||
limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m;
|
||||
limit_req_zone $binary_remote_addr zone=req_per_ip:20m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=auth_per_ip:10m rate=10r/m;
|
||||
7
ops/remote/jppro-sub2api/secrets/README.md
Normal file
7
ops/remote/jppro-sub2api/secrets/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Secrets Directory
|
||||
|
||||
此目录用于本机保存远程运行所需密钥文件。
|
||||
|
||||
- `sub2api.env`:远程 `/opt/sub2api/.env` 的本地备份,包含真实密钥。
|
||||
- 该文件被上级 `.gitignore` 排除,且应保持 `600` 权限。
|
||||
- 不要提交真实 `.env`,不要把密钥写入 README 或 issue。
|
||||
13
ops/remote/us-racknerd-0425-sub2api/.gitignore
vendored
Normal file
13
ops/remote/us-racknerd-0425-sub2api/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Remote runtime secrets and ad-hoc backups.
|
||||
secrets/*
|
||||
!secrets/README.md
|
||||
backups/*
|
||||
!backups/.gitkeep
|
||||
!backups/*.SHA256SUMS
|
||||
|
||||
# Never commit copied environment files or proxy subscriptions.
|
||||
*.env
|
||||
.env
|
||||
.env.*
|
||||
mihomo-config.yaml
|
||||
airport.yaml
|
||||
114
ops/remote/us-racknerd-0425-sub2api/README.md
Normal file
114
ops/remote/us-racknerd-0425-sub2api/README.md
Normal file
@ -0,0 +1,114 @@
|
||||
# US RackNerd 0425 Sub2API Remote Inventory
|
||||
|
||||
最后核对时间:2026-05-26 12:10 Asia/Shanghai
|
||||
|
||||
## 远程位置
|
||||
|
||||
- Tabby profile:`个人-US-racknerd-0425-1c1g`
|
||||
- Hostname:`racknerd-042910c`
|
||||
- 公网 IP:`107.173.86.201`
|
||||
- 当前公网入口:`https://pai.cyan2000.uk`
|
||||
- 迁移后角色:旧机,短期回滚来源
|
||||
- 部署目录:`/opt/sub2api`
|
||||
- Compose project:`sub2api`
|
||||
- 系统:已验证为 Linux 服务器;本次未读取完整 `/etc/os-release`
|
||||
|
||||
## 本地留痕
|
||||
|
||||
- 当前远程 Compose 副本:`current/docker-compose.yml`
|
||||
- 当前远程 nginx 站点副本:`current/nginx-sub2api.conf`
|
||||
- 迁移包 SHA256 记录:`backups/migrate-to-racknerd-0526-20260525-205103.SHA256SUMS`
|
||||
- 密钥目录说明:`secrets/README.md`
|
||||
|
||||
真实 `.env`、Mihomo 配置、订阅文件和临时备份不得提交。需要本地留存时放入 `secrets/` 或 `backups/`,并确认 `.gitignore` 已排除。
|
||||
|
||||
## 当前远程配置状态
|
||||
|
||||
- nginx 管理公网域名 `pai.cyan2000.uk`,证书由 Certbot 管理。
|
||||
- nginx 配置路径:`/etc/nginx/sites-available/sub2api.conf`
|
||||
- nginx 启用路径:`/etc/nginx/sites-enabled/sub2api.conf -> /etc/nginx/sites-available/sub2api.conf`
|
||||
- Sub2API 只绑定宿主机 `127.0.0.1:8080`,由 nginx 反代公网 HTTPS。
|
||||
- Docker Compose 使用本地目录持久化,不使用 Docker 命名卷。
|
||||
- 运行目录包含:
|
||||
- `/opt/sub2api/data`
|
||||
- `/opt/sub2api/postgres_data`
|
||||
- `/opt/sub2api/redis_data`
|
||||
- `/opt/sub2api/mihomo`
|
||||
- 当前容器:
|
||||
- `sub2api`:`weishaw/sub2api:latest`,healthy,`127.0.0.1:8080->8080`
|
||||
- `sub2api-postgres`:`postgres:18-alpine`,healthy
|
||||
- `sub2api-redis`:`redis:8-alpine`,healthy
|
||||
- `sub2api-mihomo`:`metacubex/mihomo:latest`
|
||||
- 最新运行数据规模快照:
|
||||
- `data`:约 70M
|
||||
- `postgres_data`:约 201M
|
||||
- `redis_data`:约 16M
|
||||
- `mihomo`:约 44K
|
||||
|
||||
## 部署架构
|
||||
|
||||
```text
|
||||
Browser :443 HTTPS -> nginx -> 127.0.0.1:8080 -> sub2api container
|
||||
-> postgres container
|
||||
-> redis container
|
||||
-> mihomo container (account proxy)
|
||||
```
|
||||
|
||||
公网只应暴露 `80/443/22`。`8080` 只监听 `127.0.0.1`,PostgreSQL、Redis、Mihomo 不映射到公网。
|
||||
|
||||
## 迁移关系
|
||||
|
||||
这台服务器是已完成迁移的数据来源,并保留为短期回滚来源。目标服务器记录见:
|
||||
|
||||
```text
|
||||
ops/remote/us-racknerd-0526-sub2api/
|
||||
```
|
||||
|
||||
迁移时已优先迁移:
|
||||
|
||||
- PostgreSQL 数据:`/opt/sub2api/postgres_data` 或通过 `pg_dump` 导出。
|
||||
- Redis 数据:`/opt/sub2api/redis_data`。
|
||||
- Sub2API 数据目录:`/opt/sub2api/data`。
|
||||
- Mihomo 配置:`/opt/sub2api/mihomo`。
|
||||
- 运行配置:`/opt/sub2api/docker-compose.yml`、`/opt/sub2api/.env`。
|
||||
- nginx 配置:`/etc/nginx/sites-available/sub2api.conf`,目标域名需改为 `proxy.api.lilifamily.com`。
|
||||
|
||||
迁移包路径:
|
||||
|
||||
```text
|
||||
/opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/
|
||||
```
|
||||
|
||||
## 运行检查
|
||||
|
||||
```bash
|
||||
cd /opt/sub2api
|
||||
docker compose ps
|
||||
curl -sS http://127.0.0.1:8080/health
|
||||
curl --noproxy '*' -sS https://pai.cyan2000.uk/health
|
||||
systemctl status nginx --no-pager
|
||||
```
|
||||
|
||||
期望结果:
|
||||
|
||||
- `sub2api`、`sub2api-postgres`、`sub2api-redis` 为 healthy。
|
||||
- 本机 `/health` 返回 `{"status":"ok"}`。
|
||||
- 公网入口 `/health` 返回 `{"status":"ok"}`。
|
||||
|
||||
## 操作记录
|
||||
|
||||
### 2026-05-26 迁移前盘点
|
||||
|
||||
- 通过 Tabby profile `个人-US-racknerd-0425-1c1g` 只读核对。
|
||||
- 确认 `/opt/sub2api` 存在且 Docker Compose 服务健康。
|
||||
- 确认 nginx 站点配置由 Certbot 管理 `pai.cyan2000.uk`。
|
||||
- 已将可提交的 Compose/nginx 配置副本写入 `current/`。
|
||||
|
||||
### 2026-05-26 迁移备份与回滚状态
|
||||
|
||||
- 旧机生成迁移包:`/opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/`。
|
||||
- 迁移包包含 PostgreSQL custom dump、运行配置和非日志运行数据归档、旧 nginx 配置、SHA256 校验文件。
|
||||
- 旧机本地 `sha256sum -c SHA256SUMS` 校验通过。
|
||||
- 迁移完成后只读复核:`sub2api`、`sub2api-postgres`、`sub2api-redis` 仍为 healthy,`sub2api-mihomo` 仍为 running。
|
||||
- 旧机本机 `/health` 返回 `{"status":"ok"}`。
|
||||
- 未执行停机或数据删除操作;如新机回滚失败,可继续使用旧域名 `https://pai.cyan2000.uk`。
|
||||
1
ops/remote/us-racknerd-0425-sub2api/backups/.gitkeep
Normal file
1
ops/remote/us-racknerd-0425-sub2api/backups/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
4fa5f72c3bc3c68b95bd5846cee5114516435c1dcf1e7623c377a1911e8b6cfe /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/nginx-sub2api.conf
|
||||
c3890902498a69f5bb4f2d2632a852bbba075744043bf88cc08b4116ee8c9599 /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/sub2api-config-data-mihomo-redis.tar.gz
|
||||
808f4790296bb13525c6cdc4ae3e372e083cd3f220c9bf6e41988d4886474b61 /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/sub2api.dump
|
||||
152
ops/remote/us-racknerd-0425-sub2api/current/docker-compose.yml
Normal file
152
ops/remote/us-racknerd-0425-sub2api/current/docker-compose.yml
Normal file
@ -0,0 +1,152 @@
|
||||
x-logging: &default-logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: ${LOG_MAX_SIZE:-20m}
|
||||
max-file: "${LOG_MAX_FILE:-5}"
|
||||
|
||||
services:
|
||||
sub2api:
|
||||
image: ${SUB2API_IMAGE:-weishaw/sub2api:latest}
|
||||
container_name: sub2api
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 100000
|
||||
hard: 100000
|
||||
ports:
|
||||
- "${BIND_HOST:-127.0.0.1}:${SERVER_PORT:-8080}:8080"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
environment:
|
||||
- AUTO_SETUP=true
|
||||
- SERVER_HOST=0.0.0.0
|
||||
- SERVER_PORT=8080
|
||||
- SERVER_MODE=${SERVER_MODE:-release}
|
||||
- RUN_MODE=${RUN_MODE:-standard}
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
|
||||
- DATABASE_HOST=postgres
|
||||
- DATABASE_PORT=5432
|
||||
- DATABASE_USER=${POSTGRES_USER:-sub2api}
|
||||
- DATABASE_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||||
- DATABASE_DBNAME=${POSTGRES_DB:-sub2api}
|
||||
- DATABASE_SSLMODE=disable
|
||||
- DATABASE_MAX_OPEN_CONNS=${DATABASE_MAX_OPEN_CONNS:-20}
|
||||
- DATABASE_MAX_IDLE_CONNS=${DATABASE_MAX_IDLE_CONNS:-5}
|
||||
- DATABASE_CONN_MAX_LIFETIME_MINUTES=30
|
||||
- DATABASE_CONN_MAX_IDLE_TIME_MINUTES=5
|
||||
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||
- REDIS_DB=${REDIS_DB:-0}
|
||||
- REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-128}
|
||||
- REDIS_MIN_IDLE_CONNS=${REDIS_MIN_IDLE_CONNS:-4}
|
||||
- REDIS_ENABLE_TLS=false
|
||||
|
||||
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
|
||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:?ADMIN_PASSWORD is required}
|
||||
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required}
|
||||
- JWT_EXPIRE_HOUR=24
|
||||
- TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:?TOTP_ENCRYPTION_KEY is required}
|
||||
|
||||
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-}
|
||||
- SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false}
|
||||
- SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-false}
|
||||
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mihomo:
|
||||
condition: service_started
|
||||
networks:
|
||||
- sub2api-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
postgres:
|
||||
image: ${POSTGRES_IMAGE:-postgres:18-alpine}
|
||||
container_name: sub2api-postgres
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
shm_size: 64m
|
||||
volumes:
|
||||
- ./postgres_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER:-sub2api}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-sub2api}
|
||||
- PGDATA=/var/lib/postgresql/data
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- max_connections=30
|
||||
- -c
|
||||
- shared_buffers=64MB
|
||||
- -c
|
||||
- effective_cache_size=256MB
|
||||
- -c
|
||||
- work_mem=2MB
|
||||
- -c
|
||||
- maintenance_work_mem=32MB
|
||||
networks:
|
||||
- sub2api-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
redis:
|
||||
image: ${REDIS_IMAGE:-redis:8-alpine}
|
||||
container_name: sub2api-redis
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
volumes:
|
||||
- ./redis_data:/data
|
||||
command: >
|
||||
sh -c 'redis-server
|
||||
--save 60 1
|
||||
--appendonly yes
|
||||
--appendfsync everysec
|
||||
$${REDIS_PASSWORD:+--requirepass "$$REDIS_PASSWORD"}
|
||||
$${REDIS_MAXMEMORY:+--maxmemory "$$REDIS_MAXMEMORY" --maxmemory-policy allkeys-lru}'
|
||||
environment:
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||
- REDIS_MAXMEMORY=${REDIS_MAXMEMORY:-128mb}
|
||||
- REDISCLI_AUTH=${REDIS_PASSWORD:-}
|
||||
networks:
|
||||
- sub2api-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
|
||||
mihomo:
|
||||
image: ${MIHOMO_IMAGE:-metacubex/mihomo:latest}
|
||||
container_name: sub2api-mihomo
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
volumes:
|
||||
- ./mihomo:/root/.config/mihomo
|
||||
environment:
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
networks:
|
||||
- sub2api-network
|
||||
|
||||
networks:
|
||||
sub2api-network:
|
||||
name: sub2api-network
|
||||
driver: bridge
|
||||
@ -0,0 +1,43 @@
|
||||
server {
|
||||
server_name pai.cyan2000.uk;
|
||||
|
||||
server_tokens off;
|
||||
underscores_in_headers on;
|
||||
client_max_body_size 50m;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
listen [::]:443 ssl ipv6only=on; # managed by Certbot
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/pai.cyan2000.uk/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/pai.cyan2000.uk/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
}
|
||||
|
||||
server {
|
||||
if ($host = pai.cyan2000.uk) {
|
||||
return 301 https://$host$request_uri;
|
||||
} # managed by Certbot
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name pai.cyan2000.uk;
|
||||
return 404; # managed by Certbot
|
||||
}
|
||||
8
ops/remote/us-racknerd-0425-sub2api/secrets/README.md
Normal file
8
ops/remote/us-racknerd-0425-sub2api/secrets/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Secrets Directory
|
||||
|
||||
此目录用于本机保存 `个人-US-racknerd-0425-1c1g` 上 Sub2API 运行所需密钥备份。
|
||||
|
||||
- 远程 `.env` 路径:`/opt/sub2api/.env`
|
||||
- 远程 Mihomo 配置路径:`/opt/sub2api/mihomo/config.yaml`
|
||||
- 这些文件包含数据库密码、JWT 密钥、TOTP 密钥、订阅地址或代理凭据,默认被上级 `.gitignore` 排除。
|
||||
- 不要提交真实密钥,不要把密钥写入 README、issue 或 PR。
|
||||
13
ops/remote/us-racknerd-0526-sub2api/.gitignore
vendored
Normal file
13
ops/remote/us-racknerd-0526-sub2api/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Remote runtime secrets and ad-hoc backups.
|
||||
secrets/*
|
||||
!secrets/README.md
|
||||
backups/*
|
||||
!backups/.gitkeep
|
||||
!backups/*.SHA256SUMS
|
||||
|
||||
# Never commit copied environment files or proxy subscriptions.
|
||||
*.env
|
||||
.env
|
||||
.env.*
|
||||
mihomo-config.yaml
|
||||
airport.yaml
|
||||
168
ops/remote/us-racknerd-0526-sub2api/README.md
Normal file
168
ops/remote/us-racknerd-0526-sub2api/README.md
Normal file
@ -0,0 +1,168 @@
|
||||
# US RackNerd 0526 Sub2API Remote Inventory
|
||||
|
||||
最后核对时间:2026-05-26 19:37 Asia/Shanghai
|
||||
|
||||
## 远程位置
|
||||
|
||||
- Tabby profile:`个人-US-racknerd-0526-3c4g`
|
||||
- Hostname:`racknerd-0a1c758`
|
||||
- 公网 IP:`216.45.59.243`
|
||||
- origin 入口:`https://origin.proxy.api.lilifamily.com`
|
||||
- 公开直连主站入口:`https://catproxy.lilifamily.com`
|
||||
- 部署目录:`/opt/sub2api`
|
||||
- Compose project:`sub2api`
|
||||
- 系统:Debian GNU/Linux 13 (trixie)
|
||||
|
||||
## 当前状态
|
||||
|
||||
- 已部署并验证。
|
||||
- Docker:`Docker version 29.5.2, build 79eb04c`
|
||||
- Docker Compose:`Docker Compose version v5.1.4`
|
||||
- nginx:`nginx/1.26.3`
|
||||
- DNS:`origin.proxy.api.lilifamily.com -> 216.45.59.243`,`catproxy.lilifamily.com -> 216.45.59.243`
|
||||
- `proxy.api.lilifamily.com` 已切到 JPpro `151.242.164.72`,3c4g 上旧 `proxy.api` nginx 站点已禁用。
|
||||
- Sub2API 只绑定宿主机 `127.0.0.1:8080`,公网只能经 nginx `catproxy` 或受限 `origin` 转发。
|
||||
- PostgreSQL、Redis、Mihomo 不映射公网端口。
|
||||
- 从旧机迁移的业务计数已核对:`users=1`、`api_keys=3`、`accounts=133`。
|
||||
|
||||
## 本地留痕
|
||||
|
||||
- 当前远程 Compose 副本:`current/docker-compose.yml`
|
||||
- 旧主域 nginx 配置副本:`current/nginx-sub2api.conf`,当前远程已禁用,仅作历史/回滚参考
|
||||
- 当前 origin 配置副本:`current/nginx-origin-sub2api.conf`
|
||||
- 当前 catproxy 配置副本:`current/nginx-catproxy-sub2api.conf`
|
||||
- 当前默认拒绝配置副本:`current/nginx-default-deny.conf`
|
||||
- 当前 token map 配置副本:`current/proxy-ng-token-map.conf`,真实 token 已脱敏
|
||||
- 防火墙与 origin 边界记录:`current/firewall-notes.md`
|
||||
- 迁移包 SHA256 记录:`backups/migrate-to-racknerd-0526-20260525-205103.SHA256SUMS`
|
||||
- 密钥目录说明:`secrets/README.md`
|
||||
|
||||
真实 `.env`、Mihomo 配置、订阅文件、proxy-ng token、证书私钥和临时备份不得提交。需要本地留存时放入 `secrets/` 或 `backups/`,并确认 `.gitignore` 已排除。
|
||||
|
||||
## 部署架构
|
||||
|
||||
```text
|
||||
Client
|
||||
↓ HTTPS :443 proxy.api.lilifamily.com
|
||||
JPpro nginx proxy-ng
|
||||
↓ HTTPS + X-Proxy-Ng-Token
|
||||
3c4g nginx origin.proxy.api.lilifamily.com
|
||||
↓ 127.0.0.1:8080
|
||||
sub2api container
|
||||
↓
|
||||
postgres / redis / mihomo containers
|
||||
```
|
||||
|
||||
公开直连主站路径:
|
||||
|
||||
```text
|
||||
Client
|
||||
↓ HTTPS :443 catproxy.lilifamily.com
|
||||
3c4g nginx
|
||||
↓ 127.0.0.1:8080
|
||||
Sub2API
|
||||
```
|
||||
|
||||
部署安全边界:
|
||||
|
||||
- `origin.proxy.api.lilifamily.com` 只允许 JPpro IP `151.242.164.72` 访问,并校验 `X-Proxy-Ng-Token`。
|
||||
- `catproxy.lilifamily.com` 不做 IP 白名单限制,是 3c4g 上可直接访问的主站域名和回滚入口。
|
||||
- `proxy.api.lilifamily.com` 不再解析到 3c4g,也不再由 3c4g nginx 站点提供服务。
|
||||
- Sub2API 容器端口只绑定 `127.0.0.1:8080`。
|
||||
|
||||
## 当前配置要点
|
||||
|
||||
- origin 域名:`origin.proxy.api.lilifamily.com`
|
||||
- catproxy 域名:`catproxy.lilifamily.com`
|
||||
- TLS:Certbot 管理 nginx 证书。
|
||||
- origin 证书路径:`/etc/letsencrypt/live/origin.proxy.api.lilifamily.com/fullchain.pem`
|
||||
- catproxy 证书路径:`/etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem`
|
||||
- token 配置:`/etc/nginx/proxy-ng/token.conf`,权限 `600 root root`
|
||||
- token map 配置:`/etc/nginx/conf.d/proxy-ng-token-map.conf`,本地副本已脱敏
|
||||
- 远程 `.env`:`PUBLIC_DOMAIN=proxy.api.lilifamily.com`
|
||||
- URL allowlist 当前关闭:`SECURITY_URL_ALLOWLIST_ENABLED=false`
|
||||
|
||||
## DNS state after proxy-ng hardening
|
||||
|
||||
| Name | A records | Role |
|
||||
|---|---|---|
|
||||
| `proxy.api.lilifamily.com` | `151.242.164.72` | Public entry |
|
||||
| `jp.proxy.api.lilifamily.com` | `151.242.164.72` | JPpro proxy-ng node |
|
||||
| `catproxy.lilifamily.com` | `216.45.59.243` | Public direct main-site and rollback entry |
|
||||
| `origin.proxy.api.lilifamily.com` | `216.45.59.243` | Proxy-ng origin only |
|
||||
|
||||
## 运行检查
|
||||
|
||||
```bash
|
||||
cd /opt/sub2api
|
||||
docker compose ps
|
||||
curl -sS http://127.0.0.1:8080/health
|
||||
nginx -t
|
||||
systemctl is-active nginx
|
||||
curl --noproxy '*' -i https://origin.proxy.api.lilifamily.com/health
|
||||
curl --noproxy '*' -i https://catproxy.lilifamily.com/health
|
||||
```
|
||||
|
||||
期望结果:
|
||||
|
||||
- `sub2api`、`sub2api-postgres`、`sub2api-redis` 为 healthy。
|
||||
- 本机 `/health` 返回 `{"status":"ok"}`。
|
||||
- 非 JPpro 来源访问 `origin.proxy.api.lilifamily.com/health` 返回 `403`。
|
||||
- JPpro 带 `X-Proxy-Ng-Token` 访问 origin 返回 `200`。
|
||||
- 公网访问 `catproxy.lilifamily.com/health` 返回 `200`。
|
||||
- 直连 `216.45.59.243:8080` 不对公网开放。
|
||||
|
||||
## 操作记录
|
||||
|
||||
### 2026-05-26 迁移目标盘点
|
||||
|
||||
- 通过 Tabby profile `个人-US-racknerd-0526-3c4g` 只读核对。
|
||||
- 确认公网 IP:`216.45.59.243`。
|
||||
- 确认系统:Debian GNU/Linux 13 (trixie)。
|
||||
- 确认尚未部署 `/opt/sub2api`,尚未安装 Docker。
|
||||
|
||||
### 2026-05-26 部署与迁移完成
|
||||
|
||||
- 安装 Docker CE、Docker Compose plugin、nginx、certbot、`python3-certbot-nginx`、`rsync`。
|
||||
- 从旧机迁移包恢复 `/opt/sub2api`,迁移包 SHA256 在新机按 basename 重新校验通过。
|
||||
- 使用 `pg_restore -U sub2api -d sub2api --clean --if-exists` 导入 PostgreSQL dump。
|
||||
- 业务数据计数核对通过:`users=1`、`api_keys=3`、`accounts=133`。
|
||||
- 启动 Compose 服务,`sub2api`、`sub2api-postgres`、`sub2api-redis` 均为 healthy,`sub2api-mihomo` 为 running。
|
||||
- 配置 nginx 站点 `/etc/nginx/sites-available/sub2api.conf`,启用 `/etc/nginx/sites-enabled/sub2api.conf`。
|
||||
- Certbot 已为 `proxy.api.lilifamily.com` 签发并部署 HTTPS 证书。
|
||||
- 公网验证通过:`https://proxy.api.lilifamily.com/health` 返回 `{"status":"ok"}`。
|
||||
- API 路由验证通过:未授权 `POST https://proxy.api.lilifamily.com/v1/responses` 返回 `401 API_KEY_REQUIRED`。
|
||||
- 旧机未停止,仍作为短期回滚来源。
|
||||
|
||||
### 2026-05-26 proxy-ng origin hardening backup
|
||||
|
||||
- Backup path: `/root/sub2api-hardening-backup-20260526-033010`
|
||||
- Includes nginx config, docker-compose.yml, `.env`, and PostgreSQL dump.
|
||||
- Secret token exists only on remote root-readable nginx config and is not stored in Git.
|
||||
|
||||
### 2026-05-26 origin/catproxy hardening
|
||||
|
||||
- 为 `origin.proxy.api.lilifamily.com` 和 `catproxy.lilifamily.com` 签发 Let's Encrypt 证书,到期时间均为 `2026-08-24`。
|
||||
- 新增 `/etc/nginx/sites-available/origin-sub2api.conf`,只允许 `151.242.164.72` 并校验 `X-Proxy-Ng-Token`。
|
||||
- 新增 `/etc/nginx/sites-available/catproxy-sub2api.conf`;初始版本限制管理来源,后续已按当前决策改为公开直连主站入口。
|
||||
- 新增 `/etc/nginx/sites-available/00-default-deny.conf`,未知 Host 默认返回 `444`。
|
||||
- 为 nginx 增加 `map_hash_bucket_size 128`,修复长 hostname map 导致的 `nginx -t` 失败。
|
||||
- 旧 `proxy.api.lilifamily.com` 站点已从 `sites-enabled` 禁用,备份路径:`/root/sub2api-disable-public-proxy-backup-20260526-040529`。
|
||||
- 验证结果:非白名单访问 origin 返回 `403`,JPpro 带 token 访问 origin 返回 `200`,公网主入口 `proxy.api.lilifamily.com` 返回 JPpro 节点响应头。
|
||||
- 配置 logrotate:`/etc/logrotate.d/nginx-proxy-ng`,dry-run 无语法错误。
|
||||
|
||||
### 2026-05-26 catproxy 访问边界调整
|
||||
|
||||
- 按当前决策,`catproxy.lilifamily.com` 不再限制访问来源,作为 3c4g 公开直连主站域名和回滚入口。
|
||||
- 远程备份:`/etc/nginx/sites-available/catproxy-sub2api.conf.bak-20260526043650`。
|
||||
- 从 `/etc/nginx/sites-available/catproxy-sub2api.conf` 删除 `allow 106.37.220.142` 和 `deny all`。
|
||||
- 保留 `origin.proxy.api.lilifamily.com` 的来源 IP 白名单和 `X-Proxy-Ng-Token` 校验。
|
||||
- 验证结果:`nginx -t` 成功,`systemctl reload nginx` 成功,`catproxy.lilifamily.com/health` 返回 `200`,非白名单访问 `origin.proxy.api.lilifamily.com/health` 返回 `403`。
|
||||
|
||||
### 2026-05-26 主站限速核对
|
||||
|
||||
- `catproxy.lilifamily.com` 当前无 nginx `limit_req` / `limit_conn`,主站直连不按 IP 限速。
|
||||
- `origin.proxy.api.lilifamily.com` 当前只做 JPpro IP 白名单和 `X-Proxy-Ng-Token` 校验,不做 nginx 速率限制。
|
||||
- Sub2API 业务请求限速维度是用户、用户+分组、API Key 窗口额度;不是按 IP 限速。
|
||||
- 生产库非敏感快照:当前用户全局 `rpm_limit=0`;部分分组配置 `20/30/40/100 RPM`;API Key `briqt` 配置 `5h=300`、`1d=500`,API Key `free` 配置 `1d=50`。
|
||||
- 本机测速与限速维度详见 `docs/memorys/2026-05-26-主站测速与限速核对.md`。
|
||||
1
ops/remote/us-racknerd-0526-sub2api/backups/.gitkeep
Normal file
1
ops/remote/us-racknerd-0526-sub2api/backups/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
4fa5f72c3bc3c68b95bd5846cee5114516435c1dcf1e7623c377a1911e8b6cfe /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/nginx-sub2api.conf
|
||||
c3890902498a69f5bb4f2d2632a852bbba075744043bf88cc08b4116ee8c9599 /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/sub2api-config-data-mihomo-redis.tar.gz
|
||||
808f4790296bb13525c6cdc4ae3e372e083cd3f220c9bf6e41988d4886474b61 /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/sub2api.dump
|
||||
152
ops/remote/us-racknerd-0526-sub2api/current/docker-compose.yml
Normal file
152
ops/remote/us-racknerd-0526-sub2api/current/docker-compose.yml
Normal file
@ -0,0 +1,152 @@
|
||||
x-logging: &default-logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: ${LOG_MAX_SIZE:-20m}
|
||||
max-file: "${LOG_MAX_FILE:-5}"
|
||||
|
||||
services:
|
||||
sub2api:
|
||||
image: ${SUB2API_IMAGE:-weishaw/sub2api:latest}
|
||||
container_name: sub2api
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 100000
|
||||
hard: 100000
|
||||
ports:
|
||||
- "${BIND_HOST:-127.0.0.1}:${SERVER_PORT:-8080}:8080"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
environment:
|
||||
- AUTO_SETUP=true
|
||||
- SERVER_HOST=0.0.0.0
|
||||
- SERVER_PORT=8080
|
||||
- SERVER_MODE=${SERVER_MODE:-release}
|
||||
- RUN_MODE=${RUN_MODE:-standard}
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
|
||||
- DATABASE_HOST=postgres
|
||||
- DATABASE_PORT=5432
|
||||
- DATABASE_USER=${POSTGRES_USER:-sub2api}
|
||||
- DATABASE_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||||
- DATABASE_DBNAME=${POSTGRES_DB:-sub2api}
|
||||
- DATABASE_SSLMODE=disable
|
||||
- DATABASE_MAX_OPEN_CONNS=${DATABASE_MAX_OPEN_CONNS:-20}
|
||||
- DATABASE_MAX_IDLE_CONNS=${DATABASE_MAX_IDLE_CONNS:-5}
|
||||
- DATABASE_CONN_MAX_LIFETIME_MINUTES=30
|
||||
- DATABASE_CONN_MAX_IDLE_TIME_MINUTES=5
|
||||
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||
- REDIS_DB=${REDIS_DB:-0}
|
||||
- REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-128}
|
||||
- REDIS_MIN_IDLE_CONNS=${REDIS_MIN_IDLE_CONNS:-4}
|
||||
- REDIS_ENABLE_TLS=false
|
||||
|
||||
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
|
||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:?ADMIN_PASSWORD is required}
|
||||
- JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required}
|
||||
- JWT_EXPIRE_HOUR=24
|
||||
- TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:?TOTP_ENCRYPTION_KEY is required}
|
||||
|
||||
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-}
|
||||
- SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false}
|
||||
- SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-false}
|
||||
- SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mihomo:
|
||||
condition: service_started
|
||||
networks:
|
||||
- sub2api-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
postgres:
|
||||
image: ${POSTGRES_IMAGE:-postgres:18-alpine}
|
||||
container_name: sub2api-postgres
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
shm_size: 64m
|
||||
volumes:
|
||||
- ./postgres_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER:-sub2api}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-sub2api}
|
||||
- PGDATA=/var/lib/postgresql/data
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- max_connections=30
|
||||
- -c
|
||||
- shared_buffers=64MB
|
||||
- -c
|
||||
- effective_cache_size=256MB
|
||||
- -c
|
||||
- work_mem=2MB
|
||||
- -c
|
||||
- maintenance_work_mem=32MB
|
||||
networks:
|
||||
- sub2api-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
redis:
|
||||
image: ${REDIS_IMAGE:-redis:8-alpine}
|
||||
container_name: sub2api-redis
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
volumes:
|
||||
- ./redis_data:/data
|
||||
command: >
|
||||
sh -c 'redis-server
|
||||
--save 60 1
|
||||
--appendonly yes
|
||||
--appendfsync everysec
|
||||
$${REDIS_PASSWORD:+--requirepass "$$REDIS_PASSWORD"}
|
||||
$${REDIS_MAXMEMORY:+--maxmemory "$$REDIS_MAXMEMORY" --maxmemory-policy allkeys-lru}'
|
||||
environment:
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||
- REDIS_MAXMEMORY=${REDIS_MAXMEMORY:-128mb}
|
||||
- REDISCLI_AUTH=${REDIS_PASSWORD:-}
|
||||
networks:
|
||||
- sub2api-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
|
||||
mihomo:
|
||||
image: ${MIHOMO_IMAGE:-metacubex/mihomo:latest}
|
||||
container_name: sub2api-mihomo
|
||||
restart: unless-stopped
|
||||
logging: *default-logging
|
||||
volumes:
|
||||
- ./mihomo:/root/.config/mihomo
|
||||
environment:
|
||||
- TZ=${TZ:-Asia/Shanghai}
|
||||
networks:
|
||||
- sub2api-network
|
||||
|
||||
networks:
|
||||
sub2api-network:
|
||||
name: sub2api-network
|
||||
driver: bridge
|
||||
@ -0,0 +1,14 @@
|
||||
# 3c4g firewall and origin boundary notes
|
||||
|
||||
日期:2026-05-26
|
||||
|
||||
- `origin.proxy.api.lilifamily.com`: nginx allowlist only permits JPpro `151.242.164.72`.
|
||||
- `origin.proxy.api.lilifamily.com`: `X-Proxy-Ng-Token` is required and redacted from Git.
|
||||
- `catproxy.lilifamily.com`: public direct main-site entry on 3c4g; no nginx IP allowlist is applied.
|
||||
- `proxy.api.lilifamily.com`: DNS points to JPpro `151.242.164.72`; 3c4g old `sub2api.conf` site is disabled from `sites-enabled`.
|
||||
- `127.0.0.1:8080`: Sub2API local upstream only; Docker publishes it on loopback, not public interfaces.
|
||||
- Default unknown Host behavior: nginx `00-default-deny.conf` returns `444` for unknown HTTP/HTTPS hosts.
|
||||
- logrotate: `/etc/logrotate.d/nginx-proxy-ng` dry-run succeeded.
|
||||
- Validation: local/public origin without token returned `403`; JPpro with token returned `200`; `catproxy.lilifamily.com/health` returned `200`; direct 3c4g `proxy.api` SNI no longer serves the application.
|
||||
|
||||
Known limitation: origin enforcement is nginx-layer allow/deny, not a system firewall rule. If future origin is moved to a dedicated port, also add nftables/cloud firewall rules for proxy-ng node IPs only.
|
||||
@ -0,0 +1,28 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name catproxy.lilifamily.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/catproxy.lilifamily.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
server_tokens off;
|
||||
underscores_in_headers on;
|
||||
client_max_body_size 100m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host proxy.api.lilifamily.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Host proxy.api.lilifamily.com;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
default_type text/plain;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 444;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name origin.proxy.api.lilifamily.com catproxy.lilifamily.com;
|
||||
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
default_type text/plain;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 444;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server;
|
||||
server_name _;
|
||||
ssl_certificate /etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/catproxy.lilifamily.com/privkey.pem;
|
||||
return 444;
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name origin.proxy.api.lilifamily.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
server_tokens off;
|
||||
underscores_in_headers on;
|
||||
client_max_body_size 100m;
|
||||
|
||||
location / {
|
||||
allow 151.242.164.72;
|
||||
deny all;
|
||||
|
||||
if ($proxy_ng_token_ok = 0) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $sub2api_host;
|
||||
proxy_set_header X-Real-IP $http_x_real_ip;
|
||||
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Host $sub2api_host;
|
||||
proxy_set_header X-Proxy-Ng-Token "";
|
||||
proxy_set_header X-Proxy-Ng-Node $http_x_proxy_ng_node;
|
||||
proxy_redirect https://origin.proxy.api.lilifamily.com/ https://$sub2api_host/;
|
||||
proxy_redirect https://catproxy.lilifamily.com/ https://$sub2api_host/;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
# HISTORICAL / DISABLED CONFIG.
|
||||
# 2026-05-26 起,3c4g 不再作为 `proxy.api.lilifamily.com` 公开入口。
|
||||
# 远程 `/etc/nginx/sites-enabled/sub2api.conf` 已删除;此文件仅保留作迁移历史和紧急回滚参考。
|
||||
|
||||
server {
|
||||
server_name proxy.api.lilifamily.com;
|
||||
|
||||
server_tokens off;
|
||||
# Sub2API/Codex clients use headers with underscores, so nginx must keep them.
|
||||
underscores_in_headers on;
|
||||
client_max_body_size 100m;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
listen [::]:443 ssl ipv6only=on; # managed by Certbot
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/proxy.api.lilifamily.com/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
}
|
||||
server {
|
||||
if ($host = proxy.api.lilifamily.com) {
|
||||
return 301 https://$host$request_uri;
|
||||
} # managed by Certbot
|
||||
|
||||
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name proxy.api.lilifamily.com;
|
||||
return 404; # managed by Certbot
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
map $http_x_proxy_ng_token $proxy_ng_token_ok {
|
||||
default 0;
|
||||
"REDACTED_SECRET" 1;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_host $sub2api_host {
|
||||
default "proxy.api.lilifamily.com";
|
||||
"proxy.api.lilifamily.com" "proxy.api.lilifamily.com";
|
||||
"jp.proxy.api.lilifamily.com" "jp.proxy.api.lilifamily.com";
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name proxy.api.lilifamily.com;
|
||||
|
||||
server_tokens off;
|
||||
# Sub2API/Codex clients use headers with underscores, so nginx must keep them.
|
||||
underscores_in_headers on;
|
||||
client_max_body_size 100m;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
}
|
||||
8
ops/remote/us-racknerd-0526-sub2api/secrets/README.md
Normal file
8
ops/remote/us-racknerd-0526-sub2api/secrets/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Secrets Directory
|
||||
|
||||
此目录用于本机保存 `个人-US-racknerd-0526-3c4g` 上 Sub2API 运行所需密钥备份。
|
||||
|
||||
- 目标远程 `.env` 路径:`/opt/sub2api/.env`
|
||||
- 目标远程 Mihomo 配置路径:`/opt/sub2api/mihomo/config.yaml`
|
||||
- 这些文件包含数据库密码、JWT 密钥、TOTP 密钥、订阅地址或代理凭据,默认被上级 `.gitignore` 排除。
|
||||
- 不要提交真实密钥,不要把密钥写入 README、issue 或 PR。
|
||||
207
proxy-ng/README.md
Normal file
207
proxy-ng/README.md
Normal file
@ -0,0 +1,207 @@
|
||||
# proxy-ng 多节点中转方案
|
||||
|
||||
日期:2026-05-25
|
||||
|
||||
## 结论
|
||||
|
||||
方案可行:一个域名可以解析到多个 proxy-ng 节点,每个节点运行 nginx 反向代理,再把请求转发到同一个 sub2api 后端。sub2api 前面需要有一个后端入口网关,只允许这些 proxy-ng 节点访问,并校验共享密钥,避免后端公网端口被扫到后被直接利用。
|
||||
|
||||
最小推荐拓扑:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U[用户] --> D[同一域名<br/>多 A/AAAA 记录]
|
||||
D --> N1[proxy-ng 节点 A<br/>nginx]
|
||||
D --> N2[proxy-ng 节点 B<br/>nginx]
|
||||
D --> N3[proxy-ng 节点 C<br/>nginx]
|
||||
N1 --> B[后端入口网关<br/>校验节点 IP + 共享密钥]
|
||||
N2 --> B
|
||||
N3 --> B
|
||||
B --> S[sub2api :8080]
|
||||
S --> P[(PostgreSQL)]
|
||||
S --> R[(Redis)]
|
||||
```
|
||||
|
||||
## 关键设计
|
||||
|
||||
### 1. 一个域名配置多个 IP
|
||||
|
||||
可行,但有边界:
|
||||
|
||||
- 普通 DNS 多 A 记录只能做粗粒度轮询,不能保证按节点健康状态实时摘除故障节点。
|
||||
- 如果需要健康检查、故障摘除、权重、地区路由,必须使用 Cloudflare Load Balancing、DNS 服务商健康检查,或自建调度。
|
||||
- 只配置多个 A 记录时,某个 proxy-ng 节点宕机后,仍可能有用户解析到坏 IP,直到 DNS 缓存过期。
|
||||
|
||||
推荐阶段:
|
||||
|
||||
| 阶段 | DNS 方案 | 适用场景 |
|
||||
|---|---|---|
|
||||
| P0 | 多个 A 记录,TTL 60-300 秒 | 节点少、可接受短暂失败 |
|
||||
| P1 | Cloudflare Load Balancing 或同类健康检查 | 要求自动故障摘除 |
|
||||
| P2 | 按地区/运营商调度 | 有明确区域访问质量目标 |
|
||||
|
||||
### 2. proxy-ng 节点职责
|
||||
|
||||
proxy-ng 节点只做入口和转发,不保存业务状态:
|
||||
|
||||
- TLS 终止或透传到后端入口网关。
|
||||
- 限流、连接数控制、请求体大小限制。
|
||||
- 透传真实客户端 IP 链路。
|
||||
- 给后端入口网关注入节点共享密钥。
|
||||
- 健康检查自身 nginx 和后端可达性。
|
||||
|
||||
proxy-ng 节点不应该持有 sub2api 管理员账号、数据库密码、Redis 密码、业务 API Key,也不提供账号出口代理能力。
|
||||
|
||||
### 3. 后端入口网关职责
|
||||
|
||||
后端入口网关可以是后端服务器上的 Caddy 或 nginx,放在 sub2api 前面:
|
||||
|
||||
- 只允许 proxy-ng 节点公网 IP 访问后端入口端口。
|
||||
- 校验 `X-Proxy-Ng-Token` 共享密钥。
|
||||
- 校验通过后删除密钥头,再转发给 sub2api。
|
||||
- 统一写入 `X-Forwarded-For` / `X-Real-IP`。
|
||||
- sub2api `server.trusted_proxies` 只信任后端入口网关或最后一跳代理。
|
||||
|
||||
密钥不是替代防火墙的安全边界。正确做法是“来源 IP 白名单 + 密钥校验 + TLS 回源”同时存在。
|
||||
|
||||
### 4. sub2api 内置节点管理
|
||||
|
||||
如果要求“在 sub2api 应用内管理 proxy-ng 节点和密钥”,也可行,但这是代码改造,不是单纯运维配置:
|
||||
|
||||
- 新增 inbound proxy node 配置或数据模型,至少包含节点名、节点公网 IP/CIDR、密钥哈希、启停状态、备注。
|
||||
- 在 Gin 全局中间件里校验来源 IP 和 `X-Proxy-Ng-Token`,校验失败直接 403。
|
||||
- 支持双 token 轮换,避免所有节点同时停机换密钥。
|
||||
- 管理端需要提供节点增删改查和密钥重置入口。
|
||||
|
||||
第一版不建议直接改 sub2api 业务代码。先用后端入口网关完成 IP 白名单和密钥校验,复杂度更低,回滚也更干净。
|
||||
|
||||
### 5. 账号出口代理池独立
|
||||
|
||||
sub2api 的账号代理不属于 proxy-ng 节点职责。账号出口代理池应独立建设和管理:
|
||||
|
||||
- proxy-ng 只优化用户到 sub2api 的入口线路。
|
||||
- sub2api 账号访问上游模型/API 时,继续使用现有代理管理里的 `proxy_id`。
|
||||
- 这些账号代理可以来自独立代理池、第三方代理、专用出口节点或其他合规网络资源。
|
||||
- proxy-ng 节点不暴露 `1080`、`7890`、`mixed`、`socks` 等出口代理端口。
|
||||
- 不在 proxy-ng 节点上部署 sing-box、Clash、Xray 等出口代理服务。
|
||||
|
||||
这个边界可以避免 proxy-ng 节点变成开放代理风险点,也避免入口中转带宽和账号出口流量互相抢资源。
|
||||
|
||||
## 会出问题的步骤
|
||||
|
||||
### 高风险
|
||||
|
||||
1. **只做多 A 记录但没有健康检查**
|
||||
|
||||
节点宕机时 DNS 仍可能返回坏 IP。用户表现为随机失败,不是全部失败。
|
||||
|
||||
处理方式:P0 接受这个限制;P1 引入 Cloudflare Load Balancing 或 DNS 健康检查。
|
||||
|
||||
2. **后端公网端口只靠隐藏 IP**
|
||||
|
||||
后端 IP 可能被扫到。只要后端入口没有 IP 白名单和密钥校验,攻击者可以绕过 proxy-ng 直接打 sub2api。
|
||||
|
||||
处理方式:后端防火墙只放行 proxy-ng 节点 IP;入口网关校验 `X-Proxy-Ng-Token`;sub2api 容器端口不直接暴露公网。
|
||||
|
||||
3. **真实客户端 IP 链路配置错误**
|
||||
|
||||
当前 sub2api 限流器使用 Gin `c.ClientIP()`,它依赖 `server.trusted_proxies`。配置错会导致所有用户被当成同一个节点 IP,登录/注册/API Key ACL 都可能异常。
|
||||
|
||||
处理方式:只信任最后一跳后端入口网关,端到端验证登录限流和 API Key IP ACL。
|
||||
|
||||
4. **共享密钥轮换没有计划**
|
||||
|
||||
如果所有 proxy-ng 节点共用一个密钥,泄露后必须同步更新所有节点和后端入口网关。
|
||||
|
||||
处理方式:后端入口支持至少两个有效 token,轮换时先加新 token,再滚动节点,最后删旧 token。
|
||||
|
||||
### 中风险
|
||||
|
||||
1. **回源证书和 TLS 模式混乱**
|
||||
|
||||
proxy-ng 到后端如果走公网,HTTP 明文会暴露请求内容和 API Key。
|
||||
|
||||
处理方式:回源使用 HTTPS。自签证书可以先落地,但生产建议固定 CA 或证书指纹,避免 `proxy_ssl_verify off` 长期存在。
|
||||
|
||||
2. **SSE / 流式请求被 nginx 缓冲**
|
||||
|
||||
sub2api 代理模型 API 时存在流式响应。nginx 默认缓冲可能让流式变成攒包。
|
||||
|
||||
处理方式:代理路径关闭 `proxy_buffering`,并验证 SSE 逐块到达。
|
||||
|
||||
3. **节点流量配额耗尽**
|
||||
|
||||
多节点会把每个请求复制成“用户到节点 + 节点到后端”两段公网流量。
|
||||
|
||||
处理方式:上线前按日请求量、平均响应大小、流式比例估算月流量。
|
||||
|
||||
4. **多个节点证书签发和 DNS 切换顺序**
|
||||
|
||||
多 A 记录生效前,节点需要先能签发或加载证书。否则部分用户会命中 TLS 错误节点。
|
||||
|
||||
处理方式:先部署节点,逐个本地 `--resolve` 验证,再加入 DNS。
|
||||
|
||||
5. **把入口中转节点误用为账号代理节点**
|
||||
|
||||
如果把 proxy-ng 节点同时拿来做账号出口代理,入口线路优化、账号代理池、上游访问风险会混在一起,后续很难排障和扩容。
|
||||
|
||||
处理方式:proxy-ng 节点只开放 80/443 和必要回源链路;账号代理池另建,不纳入 `proxy-ng/`。
|
||||
|
||||
## 我可以完全完成的部分
|
||||
|
||||
在本仓库内可以完整交付:
|
||||
|
||||
- `proxy-ng/` 目录结构、架构说明和部署模板。
|
||||
- proxy-ng nginx 配置模板。
|
||||
- proxy-ng `.env` 模板。
|
||||
- proxy-ng 节点安装脚本骨架。
|
||||
- 后端入口网关 nginx/Caddy 配置模板。
|
||||
- sub2api 配置变更说明:`server.trusted_proxies`、API Key ACL 可信转发开关。
|
||||
- 账号代理池和 proxy-ng 的边界说明。
|
||||
- 本地静态检查:nginx 配置语法只能在目标机器上最终验证,但模板结构可以先审阅。
|
||||
|
||||
如果授权连接目标服务器,还可以完成:
|
||||
|
||||
- 在每台 proxy-ng VPS 安装 nginx、写入配置、启动服务。
|
||||
- 在后端服务器配置入口网关、密钥校验、防火墙白名单。
|
||||
- 端到端验证 `/health`、真实 IP、登录限流、SSE。
|
||||
- 输出实际节点清单和回滚命令。
|
||||
|
||||
## 必须人工配置或确认的部分
|
||||
|
||||
- 域名 DNS 托管平台账号操作,尤其是添加多 A 记录、切 Cloudflare、开健康检查或 Load Balancing。
|
||||
- proxy-ng 节点机器购买、系统初始化、SSH 权限授权。
|
||||
- 后端服务器防火墙/云安全组放行规则确认。
|
||||
- 生产密钥生成和保存位置确认。仓库只放模板,不提交真实 token。
|
||||
- 是否只允许中国大陆访问,以及是否包含香港、澳门、台湾。
|
||||
- 是否接受普通多 A 记录的故障窗口,还是一开始就购买/启用健康检查调度。
|
||||
- 入口中转流量规模和节点带宽预算。
|
||||
- 账号代理池使用哪个外部方案。该问题不由 proxy-ng 节点承担。
|
||||
|
||||
## 官方依据
|
||||
|
||||
- Cloudflare DNS 支持同名多个 A/AAAA 记录,但普通 DNS 记录不是健康检查调度:<https://developers.cloudflare.com/dns/manage-dns-records/reference/dns-record-types/>
|
||||
- Cloudflare 同名多个 A/AAAA 记录可用于简单负载均衡,但 CNAME 与 A/AAAA 不能同名混用:<https://developers.cloudflare.com/dns/manage-dns-records/troubleshooting/records-with-same-name/>
|
||||
- Cloudflare Load Balancing 的 DNS 记录支持 A/AAAA/CNAME,并按池健康状态和策略分发:<https://developers.cloudflare.com/load-balancing/load-balancers/dns-records/>
|
||||
- Cloudflare Health Checks / Load Balancing 可以探测源站健康,用于故障判断和摘除:<https://developers.cloudflare.com/health-checks/>
|
||||
|
||||
## 推荐落地顺序
|
||||
|
||||
1. 先部署一个 proxy-ng 节点,后端入口网关开启 IP 白名单和密钥校验。
|
||||
2. 验证单节点链路:`/health`、真实 IP、登录限流、SSE、后端直连拒绝。
|
||||
3. 增加第二个 proxy-ng 节点,但先不加入正式 DNS,用 `--resolve` 指定节点 IP 验证入口。
|
||||
4. 把多个节点加入 DNS,多 A 记录先设低 TTL。
|
||||
5. 如果出现随机失败或节点经常波动,再升级为带健康检查的 DNS/负载均衡。
|
||||
|
||||
## 目录说明
|
||||
|
||||
```text
|
||||
proxy-ng/
|
||||
├── README.md
|
||||
├── env.example
|
||||
├── nginx/
|
||||
│ ├── proxy-node.conf.template
|
||||
│ └── backend-gateway.conf.template
|
||||
└── scripts/
|
||||
└── install-proxy-node.sh
|
||||
```
|
||||
19
proxy-ng/env.example
Normal file
19
proxy-ng/env.example
Normal file
@ -0,0 +1,19 @@
|
||||
# proxy-ng 节点配置模板。复制到目标机器后填写真实值,不要把真实密钥提交到仓库。
|
||||
|
||||
PROXY_NG_DOMAIN=proxy.api.lilifamily.com
|
||||
PROXY_NG_NODE_NAME=jppro-akile-0504-1c1g
|
||||
|
||||
# 后端入口网关地址,建议使用 HTTPS。
|
||||
SUB2API_BACKEND_ORIGIN=https://origin.proxy.api.lilifamily.com
|
||||
SUB2API_BACKEND_ORIGIN_HOST=origin.proxy.api.lilifamily.com
|
||||
|
||||
# proxy-ng 节点发给后端入口网关的共享密钥只应保存在远程 root-only 文件。
|
||||
# 当前约定路径:/etc/nginx/proxy-ng/token.conf,权限 600 root root。
|
||||
PROXY_NG_TOKEN_FILE=/etc/nginx/proxy-ng/token.conf
|
||||
|
||||
# 可选:中国大陆访问控制。需要 Cloudflare 或其他入口注入可信国家头时才启用。
|
||||
ALLOW_COUNTRY_CODES=CN
|
||||
|
||||
# TLS 证书路径。可以由 certbot 写入,也可以由运维系统分发。
|
||||
TLS_CERT_PATH=/etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem
|
||||
TLS_KEY_PATH=/etc/letsencrypt/live/proxy.api.lilifamily.com/privkey.pem
|
||||
53
proxy-ng/nginx/backend-gateway.conf.template
Normal file
53
proxy-ng/nginx/backend-gateway.conf.template
Normal file
@ -0,0 +1,53 @@
|
||||
# 后端入口网关 nginx 模板,部署在 sub2api 所在服务器。
|
||||
# 适合放到 Debian/Ubuntu 默认 nginx 的 /etc/nginx/conf.d/ 下;
|
||||
# 该目录通常被包含在 http {} 作用域内,因此可以使用 map 指令。
|
||||
# 目标:后端公网入口即使被扫到,也必须同时满足来源 IP 白名单和共享密钥。
|
||||
# 真实 token 不写入模板产物;后端通过 root-only map 条目文件 include。
|
||||
# 文件内容示例: "real-token" 1;
|
||||
|
||||
map $http_x_proxy_ng_token $proxy_ng_token_valid {
|
||||
default 0;
|
||||
include {{PROXY_NG_TOKEN_MAP_FILE}};
|
||||
}
|
||||
|
||||
map $http_x_real_ip $proxy_ng_real_ip {
|
||||
default $http_x_real_ip;
|
||||
"" $remote_addr;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8443 ssl http2;
|
||||
server_name {{BACKEND_GATEWAY_SERVER_NAME}};
|
||||
|
||||
ssl_certificate {{BACKEND_TLS_CERT_PATH}};
|
||||
ssl_certificate_key {{BACKEND_TLS_KEY_PATH}};
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
# 目标机器上应同时配置系统防火墙或云安全组,只允许 proxy-ng 节点 IP 访问 8443。
|
||||
allow {{PROXY_NG_NODE_IP_1}};
|
||||
allow {{PROXY_NG_NODE_IP_2}};
|
||||
deny all;
|
||||
|
||||
if ($proxy_ng_token_valid = 0) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $proxy_ng_real_ip;
|
||||
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
|
||||
# 密钥只用于入口网关校验,不能继续传给 sub2api。
|
||||
proxy_set_header X-Proxy-Ng-Token "";
|
||||
proxy_set_header X-Proxy-Ng-Node $http_x_proxy_ng_node;
|
||||
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
117
proxy-ng/nginx/proxy-node.conf.template
Normal file
117
proxy-ng/nginx/proxy-node.conf.template
Normal file
@ -0,0 +1,117 @@
|
||||
# proxy-ng 节点 nginx 模板。
|
||||
# 部署时替换 {{DOMAIN}}、{{BACKEND_ORIGIN}}、{{BACKEND_ORIGIN_HOST}}、{{NODE_NAME}}、{{TLS_CERT_PATH}}、{{TLS_KEY_PATH}}。
|
||||
# 真实 token 不写入模板产物;节点通过 root-only 文件 include,例如 /etc/nginx/proxy-ng/token.conf。
|
||||
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 30s;
|
||||
keepalive_requests 100;
|
||||
|
||||
client_header_timeout 10s;
|
||||
client_body_timeout 30s;
|
||||
client_max_body_size 256m;
|
||||
|
||||
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
|
||||
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=30r/s;
|
||||
limit_req_zone $binary_remote_addr zone=req_auth:10m rate=10r/m;
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
"" close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{DOMAIN}};
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name {{DOMAIN}};
|
||||
|
||||
ssl_certificate {{TLS_CERT_PATH}};
|
||||
ssl_certificate_key {{TLS_KEY_PATH}};
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_session_cache shared:SSL:20m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
include {{PROXY_NG_TOKEN_FILE}};
|
||||
|
||||
limit_conn conn_per_ip 20;
|
||||
limit_req zone=req_per_ip burst=30 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
add_header X-Proxy-Ng-Node "{{NODE_NAME}}" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
|
||||
location = /health {
|
||||
proxy_pass {{BACKEND_ORIGIN}}/health;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name {{BACKEND_ORIGIN_HOST}};
|
||||
proxy_set_header Host {{BACKEND_ORIGIN_HOST}};
|
||||
proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token;
|
||||
proxy_set_header X-Proxy-Ng-Node "{{NODE_NAME}}";
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_buffering off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/auth/(login|login/2fa|register|send-verify-code|forgot-password|reset-password) {
|
||||
limit_conn conn_per_ip 5;
|
||||
limit_req zone=req_auth burst=3 nodelay;
|
||||
limit_req_status 429;
|
||||
|
||||
proxy_pass {{BACKEND_ORIGIN}};
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name {{BACKEND_ORIGIN_HOST}};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host {{BACKEND_ORIGIN_HOST}};
|
||||
proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token;
|
||||
proxy_set_header X-Proxy-Ng-Node "{{NODE_NAME}}";
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass {{BACKEND_ORIGIN}};
|
||||
proxy_ssl_server_name on;
|
||||
proxy_ssl_name {{BACKEND_ORIGIN_HOST}};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Host {{BACKEND_ORIGIN_HOST}};
|
||||
proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token;
|
||||
proxy_set_header X-Proxy-Ng-Node "{{NODE_NAME}}";
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
proxy-ng/scripts/install-proxy-node.sh
Executable file
72
proxy-ng/scripts/install-proxy-node.sh
Executable file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 在 proxy-ng 节点上执行的安装骨架。
|
||||
# 该脚本只处理 nginx 基础安装和配置落位;DNS、证书签发、真实密钥分发仍由人工或运维系统完成。
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "must run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ENV_FILE="${1:-/etc/proxy-ng/proxy-ng.env}"
|
||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||
echo "env file not found: ${ENV_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "${ENV_FILE}"
|
||||
|
||||
required_vars=(
|
||||
PROXY_NG_DOMAIN
|
||||
PROXY_NG_NODE_NAME
|
||||
SUB2API_BACKEND_ORIGIN
|
||||
SUB2API_BACKEND_ORIGIN_HOST
|
||||
PROXY_NG_TOKEN_FILE
|
||||
TLS_CERT_PATH
|
||||
TLS_KEY_PATH
|
||||
)
|
||||
|
||||
for name in "${required_vars[@]}"; do
|
||||
if [[ -z "${!name:-}" ]]; then
|
||||
echo "missing required env: ${name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
apt-get update
|
||||
apt-get install -y nginx openssl ca-certificates curl
|
||||
|
||||
install -d -m 0755 /etc/proxy-ng
|
||||
install -d -m 0700 /etc/nginx/proxy-ng
|
||||
|
||||
if [[ ! -f "${PROXY_NG_TOKEN_FILE}" ]]; then
|
||||
echo "token file not found: ${PROXY_NG_TOKEN_FILE}" >&2
|
||||
echo "create it as root-only, for example: set \\$proxy_ng_expected_token \"<token>\";" >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod 600 "${PROXY_NG_TOKEN_FILE}"
|
||||
chown root:root "${PROXY_NG_TOKEN_FILE}"
|
||||
|
||||
TEMPLATE_PATH="${PROXY_NG_TEMPLATE_PATH:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/nginx/proxy-node.conf.template}"
|
||||
if [[ ! -f "${TEMPLATE_PATH}" ]]; then
|
||||
echo "template not found: ${TEMPLATE_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed \
|
||||
-e "s|{{DOMAIN}}|${PROXY_NG_DOMAIN}|g" \
|
||||
-e "s|{{NODE_NAME}}|${PROXY_NG_NODE_NAME}|g" \
|
||||
-e "s|{{BACKEND_ORIGIN}}|${SUB2API_BACKEND_ORIGIN}|g" \
|
||||
-e "s|{{BACKEND_ORIGIN_HOST}}|${SUB2API_BACKEND_ORIGIN_HOST}|g" \
|
||||
-e "s|{{PROXY_NG_TOKEN_FILE}}|${PROXY_NG_TOKEN_FILE}|g" \
|
||||
-e "s|{{TLS_CERT_PATH}}|${TLS_CERT_PATH}|g" \
|
||||
-e "s|{{TLS_KEY_PATH}}|${TLS_KEY_PATH}|g" \
|
||||
"${TEMPLATE_PATH}" > /etc/nginx/nginx.conf
|
||||
|
||||
nginx -t
|
||||
systemctl enable --now nginx
|
||||
systemctl reload nginx
|
||||
|
||||
echo "proxy-ng node installed: ${PROXY_NG_NODE_NAME} (${PROXY_NG_DOMAIN})"
|
||||
Loading…
x
Reference in New Issue
Block a user