# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## 这个仓库是什么 Pangolin 是面向中国大陆用户的终端 VPN(科学上网)项目。本仓库**不是应用代码**,而是: - `deploy/` —— 数据/分发面的容器化部署(Docker + Gitea Actions CI/CD) - `docs/` —— 设计与运维文档(`技术方案.md`、`回滚手册.md`) - `plan/` —— 分阶段实施计划(phase-0 → phase-3 + 安全/获客横切) 客户端(Flutter)与后端面板尚未进入本仓库;当前实质内容是 `deploy/`。 ## 部署目标与不可破坏的约束(最重要) 部署目标是**一台已在生产运行的 EC2**(`18.136.60.128`,ap-southeast-1,user `ec2-user`),上面还跑着**与本项目无关、不能搞坏**的服务:`marzban`(线上 REALITY 代理 + 面板)、`jiu`(jiu_mysql + jiu.service :8080 + 静态)、`blog`(blog/umami/postgres)、`billing.service` :18080。 - **线上代理 = Marzban 的 REALITY(443 → 127.0.0.1:10443),绝不能中断。** 运维者本人通过 Shadowrocket 连的就是它。 - 切换/重建 `pangolin-edge`(占用 443)会有 **~1-2 秒** TCP 交接,代理客户端会自动重连——选空闲时段做。 - **注意自噬隧道**:运维者本机常挂 VPN,且该 VPN 可能正穿这台的 443。重建 edge 时 443 闪断会**把你自己的 SSH 也掐了**(SSH 经代理隧道)。重建后等几秒重连即可,不是故障。 - 从运维者 Mac 做"外部"验证不可信(fake-IP DNS / 经 Cloudflare)。**权威验证一律在 EC2 内部做**(`ssh ec2` 后 curl `127.0.0.1` / openssl `127.0.0.1`)。 ## 目标主机的关键事实(代码里看不出来) - 部署目录:**`~/pangolin`(即 `/home/ec2-user/pangolin`)**,不是 `/opt`。 - `ec2-user` **免 sudo 跑 docker**;但**通用 `sudo` 需要密码**(Claude 跑不了)。需要 root 的步骤(如 `systemctl stop nginx`)必须让用户执行;`crontab` 在用户设置里被 deny。 - Compose 是**独立 `docker-compose`**(无 `docker compose` v2 插件),且**不支持 `compose build`**。因此: - 命令一律用 `docker-compose -p pangolin ...` - edge 镜像用 **`docker build -t pangolin-edge:local ./edge`** 单独构建,compose 只 `up`(不要 `--build`)。 - `edge`、`singbox` 用 **`network_mode: host`**(才能绑 80/443/udp443 并回连一堆只听 `127.0.0.1` 的后端)。 - 证书在 `/etc/letsencrypt`(Let's Encrypt:blog/jiu/vpn.51yanmei.com),**rw 挂进 edge**。 ## 架构(部署后) ``` EC2 :443/tcp + :80 → pangolin-edge (自定义镜像: nginx + certbot) stream ssl_preread 按 SNI 分流(edge/stream.conf): www.cloudflare.com/default → 10443 Marzban REALITY(线上,勿断) vpn/blog/jiu.51yanmei.com → 8443/8444/8445(edge 内 http vhost) http vhost(edge/conf.d/*)→ marzban面板8899 / blog3000+umami3001 / jiu8080 + 静态 EC2 :443/udp → pangolin-singbox (Hysteria2) ``` - `pangolin-edge`:分发层。**自定义镜像自带 certbot,容器内每 12h 自动 DNS-01 续期 + 本容器内 `nginx -s reload`**(见 `deploy/edge/pangolin-entrypoint.sh`)——无宿主 cron、无 docker socket。 - `pangolin-singbox`:Hysteria2 加速线(UDP 443)。 - `pangolin-xray`:REALITY,**本台不启用**(compose `profile: newnode`,留给新节点;本台 REALITY 仍由 Marzban 提供)。 - `certbot` 服务:仅 `profile: certbot`,用于**手动**首次签发/重配;日常续期由 edge 自身完成。 `deploy/edge/conf.d/*.conf` 与 `stream.conf` 是**现网宿主 nginx 配置的逐字拷贝**——改动需与现网后端地址保持一致。镜像版本固定(nginx 1.27 / sing-box v1.13.12 / xray 26.3.27)。 ## 常用命令(在 `deploy/` 下) ```bash # 幂等部署(免 root):起 singbox;宿主 nginx 已停则构建+起 edge,否则只校验 edge 配置 ./scripts/deploy.sh # 一次性切换:宿主 nginx → edge 容器(需 root,用户执行;失败自动回滚) sudo bash scripts/cutover.sh # 回滚:停 edge,恢复宿主 nginx(需 root,用户执行) sudo bash scripts/rollback.sh # 仅构建 edge 镜像 / 重建 edge(会有 ~1-2s 443 闪断) docker build -t pangolin-edge:local ./edge docker-compose -p pangolin up -d edge # 离线校验 edge 配置(不绑端口,挂真实证书) docker run --rm -v "$PWD/edge/nginx.conf:/etc/nginx/nginx.conf:ro" \ -v "$PWD/edge/stream.conf:/etc/nginx/stream.conf:ro" -v "$PWD/edge/conf.d:/etc/nginx/conf.d:ro" \ -v /etc/letsencrypt:/etc/letsencrypt:ro -v /opt/jiu:/opt/jiu:ro -v /var/www/pay:/var/www/pay:ro \ nginx:1.27-alpine nginx -t # 验证证书自续期(staging,不动正式证书) docker exec pangolin-edge certbot renew --dry-run # 取客户端导入串(Hy2,含口令,不进日志) cat secrets/clients.txt # 脚本改动后:本地 shellcheck -S warning deploy/scripts/*.sh ``` ## CI/CD `push deploy/**` → NAS 上的 Gitea(`ssh://git@192.168.3.200:2222/wangjia/pangolin.git`)→ Gitea Actions runner → `ssh ec2` → `rsync deploy/ → ~/pangolin/` → `deploy.sh`(免 root,幂等)。 - 需在 Gitea 配 Secrets:`EC2_SSH_KEY` / `EC2_HOST` / `EC2_USER` / `CF_API_TOKEN`,并把 `.gitea/workflows/deploy.yml` 的 `runs-on` 改成实际 runner label。 - 手动等价流程:`rsync -az --exclude secrets deploy/ ec2:pangolin/ && ssh ec2 'cd ~/pangolin && ./scripts/deploy.sh'`。 ## 密钥与渲染产物 `deploy/secrets/`、`*/config.json`、`cloudflare.ini` 均 **gitignore**。`gen-secrets.sh` 在 EC2 首跑生成 Hy2 口令/自签证书;`CF_API_TOKEN`(本机 `~/.env` 已加载,CI 由 Secret 注入)渲染 `cloudflare.ini`。模板用 `__PLACEHOLDER__`,脚本 `sed` 渲染。