diff --git a/doc/01-frontend.html b/doc/01-frontend.html new file mode 100644 index 0000000..b7570ac --- /dev/null +++ b/doc/01-frontend.html @@ -0,0 +1,152 @@ + + + + + +01 前端架构 · 穿山甲架构设计 + + + + + + +
+ +
+
Chapter 01 · Frontend
+

前端架构

+

原则:性能、稳定性、用户体验优先;不同平台允许不同技术栈,但视觉与交互必须 100% 还原 design/ 设计系统——以 ui_kits/ React 原型为像素验收标准,以 colors_and_type.css 为令牌唯一真相源。

+
+ +
+

§1分端技术栈选型

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
技术栈选型理由(性能 / 稳定 / UX)像素基准
移动 App
iOS / Android
Flutter + sing-box libbox(gomobile 桥接)
iOS NetworkExtension / Android VpnService
需要系统级隧道能力,Web 栈不可行;Flutter 自绘渲染保证两端像素一致,design/flutter/ 起步包(theme + widgets + main)拿来即跑。ui_kits/mobile/
iPad 客户端Flutter——与移动 App 同一工程,宽度 ≥900 自适应切换为侧栏分栏布局不是第五套代码:复用移动端全部原子组件与内核层,仅布局开关;横屏 1180×820 基准,左侧栏导航(触控尺寸,行高 ≥48px),连接页双栏(左大连接键 / 右信息列),节点双列网格。ui_kits/tablet/
桌面客户端
Windows / macOS
Flutter desktop + sing-box 子进程(TUN 模式)与移动端共享 ~90% UI 代码(连接键、节点列表、统计、账户全复用),920×600 固定窗 + 侧栏布局照搬原型;避免 Electron 的内存与启动开销。ui_kits/desktop/
官网Astro(SSG)→ Cloudflare Pages纯静态 = 极致加载性能 + 整站可秒级复制到任意备用域名(灾备关键,见 05 章);Astro 直接吃 JSX,ui_kits/website 组件近乎逐字迁移。ui_kits/website/
Web 用户中心Next.js(React,静态导出 + 客户端取数)ui_kits/usercenter 本身是 React——组件直接复用是保真上限;静态导出后同样可多镜像部署,动态数据全走 API。ui_kits/usercenter/
+
为什么不强行统一一个栈 + 客户端离不开系统级隧道与开机自启,必须是原生壳;Web 端的两个原型本身就是 React,重写成 Flutter Web 反而损失保真与首屏性能。五个产品面、四套实现(iPad 归入移动端工程的自适应断点),通过同一套设计令牌(CSS 变量 ↔ pangolin_theme.dart 一一对应)保证视觉同源——栈不同,像素相同。
+
+ +
+

§2客户端分层架构(移动 + 桌面共享)

+
+
+
UI 层(共享 ~90%)flutter/widgets/:连接键三态 · 智能选择推荐卡 · 免费额度卡 · 节点列表 · 统计 · 账户;明暗双主题 + 中英单显;宽度 ≥900 自适应切侧栏分栏布局(iPad / 桌面同构)
+
状态层Riverpod:连接状态机(off / connecting / on)· 会话 · 节点目录(带 version)· 用量额度
+
服务层API client(域名池 + 重试退避 + 响应签名校验)· 节点目录缓存 · 订阅凭证存储(Keychain / Keystore)
+
内核层sing-box libbox(移动:gomobile AAR/XCFramework;桌面:子进程 + TUN)· URLTest 智能选线 · Kill-switch
+
平台壳iOS NetworkExtension · Android VpnService(前台服务)· macOS/Win TUN 设备 + 开机自启
+
+
UI 与内核之间仅通过状态层通信;内核崩溃不拖垮 UI,UI 重启不掉隧道
+
+ +
+ +
+

§3100% 还原策略

+

3.1 令牌同源

+
+ + + + + +
令牌载体同步方式
官网 / 用户中心colors_and_type.css 直接链入原样引用,不复制不改写
移动 / 桌面flutter/pangolin_theme.dart与 CSS 一一对应的 Dart 镜像;改令牌必须两处同改(design/CLAUDE.md §6)
+

3.2 像素基准与组件对照

+

每个界面动手前先打开对应 React 原型比对;以下关键组件逐一对照验收:

+ +

3.3 提交前验收清单(每个界面,引用 design/CLAUDE.md §9)

+ +
+ +
+

§4客户端断网弹性

+

设计目标:API 全灭、域名全被污染时,客户端仍能连上节点。详见 06 章断网应对矩阵,此处为客户端侧实现。

+
+ + + + + + + + + +
机制实现覆盖的故障
节点目录缓存每次成功拉取 /v1/nodes 后落盘(带 version 与时间戳);启动时先用缓存渲染并尝试连接,后台再刷新API 暂时不可达
API 端点池客户端内置:主域名 + 备用域名 N 个 + IP 直连兜底;按序故障转移,成功的端点置顶记忆单个域名被墙 / 被污染
DoH 解析域名解析优先走 DoH(多个提供方),绕开本地污染DNS 污染
签名端点更新定期从多镜像静态文件(Cloudflare Pages / GitHub 等)拉取 Ed25519 签名的端点列表,验签后合并进端点池内置端点全部失效
紧急逃生配置安装包内置 1–2 个应急节点参数(低速、仅够拉新目录),所有在线途径失效时启用极端封锁
应急公告客户端内公告位从签名静态 JSON 读取(多镜像),可引导用户更新或切换渠道需要人工广播时
+
注意所有兜底参数(IP、应急节点、签名公钥)属于敏感资产:随版本轮换、按渠道分包(不同分发渠道内置不同 IP 子集),泄露一个渠道不烧全部。
+
+ +
+ + + + + + + + + diff --git a/doc/02-backend.html b/doc/02-backend.html new file mode 100644 index 0000000..49b1040 --- /dev/null +++ b/doc/02-backend.html @@ -0,0 +1,174 @@ + + + + + +02 后端设计 · 穿山甲架构设计 + + + + + + +
+ +
+
Chapter 02 · Backend
+

后端设计

+

Go 模块化单体,满足前端全部功能即可、不做过度设计。API 契约完整继承 design/server/ARCHITECTURE.md §3 蓝本,唯一语义修订:数据面从 WireGuard 换为 sing-box(REALITY / Hysteria2),connect 改为下发用户凭证 + 节点连接参数。

+
+ +
+

§1服务形态与模块划分

+

单体起步、按模块分包(实现顺序即蓝本 §7):

+
+
+
HTTP APIchi · /v1
+
+
auth验证码/注册/JWT
+
codes激活码/兑换/webhook
+
devices设备/订阅校验
+
nodes目录/connect/调度
+
usage用量/额度
+
admin管理端(独立监听)
+
+
+
gRPC server(mTLS)节点 agent:注册 / 心跳 / 凭证下发与回收 / 用量上报
+
scheduler探针汇聚 · 判封 · 节点生命周期驱动(见 04 章)
+
+
MySQL主数据
+
Redis验证码/限流/实时负载
+
+
一个二进制三个监听:公网 API(CDN 后)· agent gRPC(mTLS 双向认证)· 管理端(内网/白名单)
+
+ +
+ +
+

§2API 契约 v1(继承蓝本 §3,正式版用 OpenAPI 定义)

+
# 认证(无需 JWT)
+POST /v1/auth/code        {email}                  # 发 6 位验证码(Redis 10min,限 1/min)
+POST /v1/auth/register    {email, code, password}  # 建号 + 7 天试用 + JWT
+POST /v1/auth/login       {email, password}        # JWT(access 15min + refresh 30d)
+POST /v1/auth/refresh     {refresh_token}
+
+# 账户
+GET    /v1/me                                      # 账户 + 订阅 + 用量摘要
+GET    /v1/me/devices
+DELETE /v1/me/devices/:id                          # 移除设备(同步回收节点侧凭证)
+
+# 商业闭环(App 内无支付,只有兑换)
+POST /v1/redeem           {code}                   # 兑换激活码(幂等 + 审计)
+POST /v1/ads/unlock       {device_id, ad_token}    # 免费版激励视频解锁当日时长(验 SDK 回执)
+GET  /v1/plans                                     # 套餐目录(数字与 design/CLAUDE.md §7 一致)
+
+# 节点(数据面入口)
+GET  /v1/nodes            ?if_version=N            # 节点目录(按套餐过滤,304 支持)
+POST /v1/nodes/:id/connect    {device_id}          # 下发连接凭证(见 §3,语义已适配 sing-box)
+POST /v1/nodes/:id/disconnect {device_id}
+
+GET  /v1/usage            ?days=7                  # 用量曲线(统计页)
+GET  /v1/notices                                   # 公告(亦发布为多镜像签名静态 JSON)
+ +
+ +
+

§3connect 语义适配(相对蓝本 v0.1 的唯一修订)

+
修订理由蓝本 v0.1 的数据面是 WireGuard(connect = 下发 WG peer)。WG 协议特征明显,在目标网络环境会被快速识别阻断;plan/ phase-0~3 已选定 REALITY 主线 + Hysteria2 备线。API 形状不变,仅返回体语义调整。
+

3.1 连接流程

+
+
+
客户端POST /nodes/:id/connect
+
+
API 校验订阅有效 · 设备数 ≤ 上限 · 免费版校验 ad_unlocked_at + 剩余分钟
+
+
gRPC → agent确保该用户 UUID 已在节点 sing-box 用户表
+
+
返回连接参数server, port, uuid, flow, reality pbk/sid/sni · hy2 备用
+
+
免费版凭证带 TTL = 当日剩余分钟,到时 agent 自动从用户表移除;付费版 TTL 24h 在线自动续期
+
+

3.2 凭证模型

+ +
+ +
+

§4关键业务流程

+

4.1 注册与试用(对齐客户端 UI:邮箱 → 验证码 → 设密码)

+ +

4.2 激活码生命周期

+
+
+
发卡店售出webhook(HMAC 签名)
+
人工渠道TG/LINE/邮箱 · 管理端批量生成 batch
+
+
codes 入库status=unused · 库存 hash
+
+
POST /redeem事务:redeemed + 订阅顺延(叠加非覆盖)
+
+
audit_log必记
+
+
+ +

4.3 免费版广告解锁

+ +
+ +
+

§5后端安全要点(详见 06 章总纲)

+ +
+ +
+ + + + + + + + + diff --git a/doc/03-database.html b/doc/03-database.html new file mode 100644 index 0000000..4be94c3 --- /dev/null +++ b/doc/03-database.html @@ -0,0 +1,239 @@ + + + + + +03 数据库 · 穿山甲架构设计 + + + + + + +
+ +
+
Chapter 03 · Database
+

数据库设计

+

MySQL 8.x(InnoDB · utf8mb4 · 时区 UTC)作为主数据存储,Redis 承担验证码、限流、节点实时状态与会话。schema 自蓝本 §2 全量移植并做 MySQL 类型适配,另为弹性拓扑(04 章)扩展三张表。

+
+ +
+

§1移植约定(Postgres 蓝本 → MySQL 8)

+
+ + + + + + + + +
蓝本用法MySQL 8 适配说明
jsonbJSON审计 meta、节点标签等
tags[](数组)JSON(数组)查询用 JSON_CONTAINS,量大再拆关联表
timestamptzDATETIME(6),应用层一律 UTC连接串强制 time_zone='+00:00'
枚举字符串ENUM(...)状态机字段固定且少,ENUM 自带约束
主键BIGINT UNSIGNED AUTO_INCREMENT 内部主键 + 对外 uuid CHAR(36) 业务标识避免顺序 ID 外泄可枚举;UUID 不做主键防 InnoDB 页分裂
+
+ +
+

§2核心表(DDL 摘要)

+
-- 账户与设备
+CREATE TABLE users (
+  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+  uuid        CHAR(36) NOT NULL UNIQUE,            -- 对外标识
+  email       VARCHAR(255) NOT NULL UNIQUE,
+  pw_hash     VARCHAR(255) NOT NULL,               -- argon2id
+  dp_uuid     CHAR(36) NOT NULL,                   -- 数据面凭证 UUID(可轮换,节点侧只见它)
+  status      ENUM('active','banned') NOT NULL DEFAULT 'active',
+  created_at  DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE devices (
+  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+  uuid        CHAR(36) NOT NULL UNIQUE,
+  user_id     BIGINT UNSIGNED NOT NULL,
+  name        VARCHAR(64) NOT NULL,
+  platform    ENUM('ios','android','windows','macos') NOT NULL,
+  last_seen   DATETIME(6) NULL,
+  created_at  DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+  FOREIGN KEY (user_id) REFERENCES users(id),
+  INDEX idx_user (user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 套餐与订阅(数字口径 = design/CLAUDE.md §7)
+CREATE TABLE plans (
+  id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+  code          ENUM('free','pro','team') NOT NULL UNIQUE,
+  max_devices   INT NOT NULL,                      -- free 1 / pro 5 / team 10
+  daily_minutes INT NULL,                          -- free 10;NULL = 不限
+  ad_gate       BOOLEAN NOT NULL DEFAULT FALSE     -- free 每日看广告解锁
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE subscriptions (
+  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+  user_id     BIGINT UNSIGNED NOT NULL,
+  plan_id     BIGINT UNSIGNED NOT NULL,
+  expires_at  DATETIME(6) NOT NULL,
+  source      ENUM('trial','code') NOT NULL,       -- 注册自动 7 天试用 source='trial'
+  created_at  DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+  FOREIGN KEY (user_id) REFERENCES users(id),
+  FOREIGN KEY (plan_id) REFERENCES plans(id),
+  INDEX idx_user_exp (user_id, expires_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 激活码(App 内无支付,资金流外部化)
+CREATE TABLE code_batches (
+  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+  channel     ENUM('store','tg','line','manual') NOT NULL,
+  created_by  VARCHAR(64) NOT NULL,
+  note        VARCHAR(255) NULL,
+  created_at  DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE codes (
+  id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+  code_hash     CHAR(64) NOT NULL UNIQUE,          -- SHA-256;明文只出现一次
+  plan_id       BIGINT UNSIGNED NOT NULL,
+  duration_days INT NOT NULL,
+  batch_id      BIGINT UNSIGNED NOT NULL,
+  status        ENUM('unused','redeemed','void') NOT NULL DEFAULT 'unused',
+  redeemed_by   BIGINT UNSIGNED NULL,
+  redeemed_at   DATETIME(6) NULL,
+  FOREIGN KEY (plan_id) REFERENCES plans(id),
+  FOREIGN KEY (batch_id) REFERENCES code_batches(id),
+  INDEX idx_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 用量(最小数据原则:仅字节/分钟,无目的地)
+CREATE TABLE usage_daily (
+  user_id        BIGINT UNSIGNED NOT NULL,
+  date           DATE NOT NULL,
+  bytes_up       BIGINT UNSIGNED NOT NULL DEFAULT 0,
+  bytes_down     BIGINT UNSIGNED NOT NULL DEFAULT 0,
+  minutes_used   INT NOT NULL DEFAULT 0,
+  ad_unlocked_at DATETIME(6) NULL,                 -- 免费版当日激励视频解锁时刻
+  PRIMARY KEY (user_id, date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 审计(兑换/封禁/节点操作必记)
+CREATE TABLE audit_log (
+  id      BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+  actor   VARCHAR(64) NOT NULL,
+  action  VARCHAR(64) NOT NULL,
+  target  VARCHAR(128) NOT NULL,
+  meta    JSON NULL,
+  at      DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+  INDEX idx_at (at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ +
+

§3节点与弹性拓扑表(支撑 04 章)

+
-- 节点:蓝本 nodes 表 + 拓扑/生命周期扩展字段
+CREATE TABLE nodes (
+  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+  uuid        CHAR(36) NOT NULL UNIQUE,
+  region      VARCHAR(8) NOT NULL,                 -- HK / JP / SG / US…(UI 显示 2 字母码块)
+  name_zh     VARCHAR(64) NOT NULL,
+  name_en     VARCHAR(64) NOT NULL,
+  role        ENUM('entry','relay','exit') NOT NULL DEFAULT 'entry',
+  tier        ENUM('free','pro') NOT NULL,         -- 消耗品池 / 精品池
+  endpoint    VARCHAR(255) NOT NULL,               -- ip:port(REALITY)
+  hy2_port    INT NULL,                            -- Hysteria2 端口跳跃区间起点
+  reality_pbk VARCHAR(64) NOT NULL,
+  reality_sni VARCHAR(128) NOT NULL,               -- 伪装目标 SNI
+  provider_id BIGINT UNSIGNED NOT NULL,
+  tags        JSON NULL,
+  status      ENUM('provisioning','probing','up','draining','down','destroyed')
+              NOT NULL DEFAULT 'provisioning',     -- 生命周期状态机(04 章 §3)
+  weight      INT NOT NULL DEFAULT 100,
+  created_at  DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+  FOREIGN KEY (provider_id) REFERENCES providers(id),
+  INDEX idx_status_tier (status, tier)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- VPS 厂商池(多厂商、账号隔离,见 04 章 §5)
+CREATE TABLE providers (
+  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+  name        VARCHAR(64) NOT NULL,
+  api_kind    VARCHAR(32) NOT NULL,                -- 开通自动化所用 API 适配器
+  regions     JSON NOT NULL,
+  pool        ENUM('consumable','premium') NOT NULL,
+  enabled     BOOLEAN NOT NULL DEFAULT TRUE,
+  note        VARCHAR(255) NULL                    -- 凭证不入库,存独立 secrets 管理
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 节点事件:状态迁移与判封依据(探针明细在 Redis,结论落库)
+CREATE TABLE node_events (
+  id        BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+  node_id   BIGINT UNSIGNED NOT NULL,
+  event     ENUM('provisioned','probe_pass','probe_fail','marked_up','draining',
+                 'blocked_suspect','blocked_confirmed','replaced','destroyed') NOT NULL,
+  detail    JSON NULL,                             -- 探针命中率、判封依据等
+  at        DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+  FOREIGN KEY (node_id) REFERENCES nodes(id),
+  INDEX idx_node_at (node_id, at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- 目录版本:nodes 任何变更 bump,客户端 if_version 灰度
+CREATE TABLE directory_version (
+  id      TINYINT PRIMARY KEY DEFAULT 1,
+  version BIGINT UNSIGNED NOT NULL,
+  CHECK (id = 1)
+) ENGINE=InnoDB;
+
节点表里没有的东西(有意为之)节点服务器上不落任何用户身份;本表也不存节点登录凭证(SSH 密钥、厂商 API key 走独立 secrets 管理,与运营身份隔离)。
+
+ +
+

§4Redis 键设计

+
+ + + + + + + + + + +
键模式类型 / TTL用途
auth:code:{email}STRING · 10min邮箱验证码(同邮箱 1/min 限发)
rl:{scope}:{key}ZSET · 滑动窗口接口限流(IP / 用户 / 邮箱多维度)
jwt:refresh:{jti}STRING · 30drefresh token 白名单(登出/封禁即删)
node:load:{node_uuid}HASH · 90sagent 每 30s 上报:在线数/带宽/CPU——目录排序与客户端"信号条"数据源
probe:{node_uuid}:{vantage}STRING · 30min境内外探针最近结果(判封原始数据)
dir:versionSTRING目录版本缓存(DB 为权威)
redeem:fail:{user_id}STRING · 1h兑换失败计数(5 次锁 1 小时)
+

Redis 全部可重建(无权威数据),故不参与备份;重启后由 agent 心跳与 DB 回填。

+
+ +
+

§5备份与恢复

+
+ + + + + + + +
策略目标
全量每日 xtrabackup(或托管 DB 自动快照)RPO ≤ 15min
增量binlog 持续归档(15min 滚动上传)
加密与异地备份用 age 公钥加密后传对象存储(与生产、与运营身份均隔离的账号);保留 30 天单点被端不丢数据
恢复演练每月在隔离环境做一次全量 + binlog 回放演练,记录耗时RTO ≤ 4h(控制面整体重建含 DB,见 05 章)
+
备份内容敏感性用户库(邮箱)是全系统最敏感资产:备份加密私钥离线保存、不进任何在线主机;对象存储账号用独立身份注册,与 VPS / 域名 / 收款不产生关联(身份隔离红线,见 06 章)。
+
+ +
+ + + + + + + + + diff --git a/doc/04-infra.html b/doc/04-infra.html new file mode 100644 index 0000000..46bd2c7 --- /dev/null +++ b/doc/04-infra.html @@ -0,0 +1,187 @@ + + + + + +04 机器与网络拓扑 · 穿山甲架构设计 + + + + + + +
+ +
+
Chapter 04 · Infrastructure & Topology
+

机器与网络拓扑

+

目标:弹性可变的网络拓扑——所有节点可一键更换或自动更换,IP 可随时轮换;任何单点被识别、被阻断都不影响整体服务。设计核心是「节点是牲口不是宠物」:节点无状态、可丢弃、分钟级重建。

+
+ +
+

§1三层弹性拓扑

+
+
+
用户REALITY / Hysteria2
+
+
入口层 Entry消耗品 IP 池 · 多厂商 · 高频轮换
+
+
中转层 Relay(可选)内部隧道转发
+
+
落地层 Exit精品 IP · 稳定出口 · 用户不可见
+
+
+
免费用户:直连消耗品池入口(单跳直出)。付费用户:入口 → 落地两跳为主,中转层在入口与落地间链路质量差时按需启用。
+
+
被识别阻断的永远是入口 IP(暴露面);落地 IP 只对内部隧道可见,几乎不会被烧
+
+
+ + + + + + + +
角色暴露面IP 等级更换频率跑什么
entry 入口直面用户与审查(唯一公网暴露)消耗品池:冷门小厂、便宜、可隐私购买被封即换;常态每 2–4 周主动轮换sing-box(REALITY + Hy2 端口跳跃)+ agent
relay 中转仅入口可见低价大带宽低频内部隧道转发 + agent
exit 落地仅中转/入口可见精品池:稳定厂商、原生 IP、解锁好极低频出口 + agent
probe 探针境内多 ISP 拨测点 + 境外对照点探测脚本,结果回报控制面
+ +
+ +
+

§2节点无状态化(可丢弃的前提)

+ +
+ +
+

§3节点生命周期状态机

+
+
+
provisioning厂商 API 开机中
+
+
probing探活:境内外可达性验证
+
+
up在目录 · 接受连接
+
+
draining不接新连接 · 排空存量
+
+
down已出目录
+
+
destroyed销毁 · IP 释放
+
+
up → down(跳过 draining):判封确认时立即下架。probing 失败 → 直接 destroyed(坏 IP 不入池)。每次迁移写 node_events 并 bump 目录 version。
+
+ +
+ +
+

§4一键更换与自动更换

+

4.1 一键更换(管理端按钮 / CLI 一条命令)

+
+
+
replace(node)管理端触发
+
+
开新机厂商 API(同区域 · 厂商池内可换家)
+
+
cloud-init装 agent · 自注册
+
+
probing境内外探活通过
+
+
新机入池version bump
+
+
旧机 draining→ destroy · IP 释放
+
+
先建后拆(make-before-break):任何更换动作期间容量不下降
+
+ +

4.2 自动更换:判封 → 下架 → 补新

+

判封信号(三路互证,避免误判)

+
+ + + + + + +
信号源采集说明
境内探针电信 / 联通 / 移动 ≥3 个拨测点,每 5 分钟:TCP 连通 + TLS 握手 + 真实协议握手核心信号;探针点用境内云函数/拨测服务,与运营身份隔离
境外对照境外探针同款探测区分「被墙」与「机器挂了」:境内挂 + 境外通 = 疑似被封;两边都挂 = 故障
流量侧agent 上报在线数/带宽,环比骤降(如 15 分钟内 −80%)辅助信号,提早怀疑
+

判定与处置规则(scheduler 驱动,阈值可配)

+ +
主动降低被封概率(比换得快更重要) +
    +
  • REALITY 伪装 SNI 定期评估更换(目标站不可被墙、延迟特征合理);密钥/short_id 随节点轮换。
  • +
  • Hysteria2 端口跳跃(区间随机),抗 UDP QoS 与端口特征。
  • +
  • 新 IP 上线前先「养」:探活通过后小流量灰度 24h 再放满 weight,避免新 IP 行为突变被标记。
  • +
  • 免费池与付费池物理隔离(不同厂商账号),免费池被批量烧不殃及付费池。
  • +
+
+ +
+

§5调度、容量与厂商池

+

5.1 节点目录排序(服务端)与智能选择(客户端)

+ +

5.2 厂商池策略

+
+ + + + + +
厂商画像要求
消耗品池(entry/free)冷门小厂 ≥3 家,按量/小时计费,IP 便宜量大有 API 可自动开退机;支持加密货币付款、隐私注册;避开被重点封锁的大厂 IP 段
精品池(exit/pro entry)稳定厂商 1–2 家,原生 IP、带宽质量好同样隐私注册;区域:HK / JP / SG / US 起步
+ +

5.3 监控告警

+ +
+ +
+ + + + + + + + + diff --git a/doc/05-web-security.html b/doc/05-web-security.html new file mode 100644 index 0000000..3ff0368 --- /dev/null +++ b/doc/05-web-security.html @@ -0,0 +1,160 @@ + + + + + +05 Web 安全与灾备 · 穿山甲架构设计 + + + + + + +
+ +
+
Chapter 05 · Web Security & DR
+

Web 安全与灾备

+

Web 面资产:官网(获客入口)、用户中心、API、公告/端点分发。威胁两类:常规 Web 攻击,以及本项目特有的域名被墙、源站被定位、服务节点被封。原则:一切可被秒级重建,一切入口皆有备份。

+
+ +
+

§1资产与暴露面

+
+ + + + + + + +
资产形态暴露面被打击后果
官网Astro 纯静态主域名 + 镜像域名获客入口断(存量用户无感)
用户中心Next.js 静态导出同上网页端兑换/订阅导入断(客户端内同功能仍可用)
APIGo 服务(CDN 后)API 域名池登录/兑换/目录刷新断(客户端缓存兜底)
公告与端点分发签名静态 JSON多镜像(CF Pages / GitHub / 对象存储)失去应急广播能力——所以镜像最多、优先级最高
+
+ +
+

§2常规 Web 安全基线

+ +
+ +
+

§3域名体系与解析兜底

+
+ + + + + + + +
域名组用途策略
主域名官网 + 用户中心品牌入口;被墙概率最高,做好牺牲准备
API 域名池(N≥3)客户端 API与主域不同注册商、不同 WHOIS(隐私保护)、互不关联;客户端内置全部
订阅/分发域名端点更新、公告 JSON独立于以上两组——「取订阅」行为最容易被关联封锁,单独隔离
冷备域名池未启用常备 ≥5 个已注册未使用的域名,启用即配 CDN,随签名端点更新推给客户端
+ +
+ +
+

§4备份与可重建性(一切皆可秒建)

+
+ + + + + + + + +
资产备份形态重建路径RTO
官网 / 用户中心git 仓库即备份(纯静态)CI 一键部署到任意新域名/新平台(CF Pages、对象存储+CDN)< 30min
API 服务容器镜像 + IaC(Terraform/Ansible 全量定义)新机拉镜像 + 配置注入,接回 DB< 2h
MySQL每日全量 + binlog 增量,age 加密异地(03 章 §5)新实例还原 + 回放≤ 4h(RPO ≤ 15min)
节点无需备份(无状态,04 章)cloud-init 自动重建3–5min/台
密钥类JWT 私钥 / 内部 CA / 签名私钥 / 备份解密钥:离线密管,两地保存
+
演练制度每月一次「假装全没了」演练:从干净环境凭 git + IaC + 备份把官网、API、DB 全链路拉起来并跑通注册→兑换→连接,记录实际 RTO。没演练过的备份等于没有备份。
+
+ +
+

§5被封 / 被打击应对 Runbook

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
场景检测应对动作目标恢复
API 域名被墙域名探针:境内 DNS 污染/TCP 阻断,境外正常① 客户端自动故障转移到池内下一域名(无人工动作)② 启用一个冷备域名补池 ③ 签名端点更新下发新池用户无感;池恢复 < 1d
主域名(官网)被墙同上① 镜像域名顶上(官网常备 ≥2 个活跃镜像)② TG 频道 + 客户端公告位广播新地址 ③ 评估是否换主品牌域名获客入口 < 1d
入口节点大面积被封04 章判封体系 + 熔断触发① 自动:存量好节点承载 + 客户端目录刷新 ② 人工:判断封锁模式(IP 段?协议特征?SNI?)③ 对症换厂商/换区域/换伪装参数后批量重建核心连接 < 4h
源站 IP 暴露 / 被攻击源站直连流量告警、异常负载① 立即换源站 IP(IaC 重建)② 复查泄露途径(DNS 历史、出站请求、证书透明度日志)③ 轮换回源鉴权< 2h
CDN 账号风险(封号/要求实名)账号通知 / 服务异常① 备好第二 CDN 账号与配置导出,DNS 切换 ② 极端情况:API 走多域名直连源站集群(牺牲部分防护换可用性)< 1d
发卡店/收款通道失效webhook 静默 / 渠道通知① 切换备用发卡店(激活码体系与渠道解耦,codes.channel 隔离)② 客户端「兑换/购买」页指向新渠道(远程配置,无需发版)< 1d
DB 损坏 / 控制面全灭监控全红走 §4 重建路径;期间客户端靠缓存目录 + 已下发凭证维持已有用户连接(数据面独立存活)≤ 4h,存量连接不断
+

应急广播体系(所有场景的公共依赖)

+ +
+ +
+ + + + + + + + + diff --git a/doc/06-security.html b/doc/06-security.html new file mode 100644 index 0000000..1e0583e --- /dev/null +++ b/doc/06-security.html @@ -0,0 +1,187 @@ + + + + + +06 安全总纲 · 穿山甲架构设计 + + + + + + +
+ +
+
Chapter 06 · Security
+

安全总纲

+

安全是本项目的第一优先级,且分三个互不混淆的层面:运营者安全(身份隔离)、系统安全(攻击面与数据保护)、可用性安全(被封/断网后的存活)。任何功能与安全冲突时,安全赢。

+
+ +
+

§1威胁模型总表

+
+ + + + + + + + + + +
资产威胁对策详见
运营主体身份服务器/域名/收款被关联追踪全链路境外身份、加密货币付款、WHOIS 隐私、专用管理环境;各资产间零身份交叉本章 §2
用户库(邮箱)控制面被入侵、备份泄露管理端内网+2FA、最小攻击面、备份 age 加密 + 私钥离线、audit_log02/03/05 章
用户行为隐私连接行为被记录/关联无日志口径(仅字节/分钟)、节点零用户库(只见 dp_uuid)、DoH、Kill-switch本章 §4
数据面节点IP 被识别封锁、机器被抄REALITY 无指纹 + Hy2 端口跳跃、三层拓扑、判封自动换、节点无状态零数据04 章
Web 入口域名被墙、源站被定位、DDoSCDN 前置、域名池 + 冷备、源站隐藏、全资产可秒建05 章
资金流收款账户冻结、被关联App 内零支付;USDT(TRC20) + 发卡店,通道可热切换,绝不绑实名账户02 章 §4
客户端安装包被篡改、端点被伪造安装包签名 + 多渠道校验值;端点/公告更新 Ed25519 验签;敏感参数按渠道分包01 章 §4
+
+ +
+

§2身份隔离红线(继承 plan/security,任何阶段不得触碰)

+
红线清单 +
    +
  • 不得用国内真实身份注册任何环节(服务器、域名、CDN、对象存储、收款、应用商店)。
  • +
  • 管理后台不得暴露公网(仅内网/白名单 + 强口令 + 2FA)。
  • +
  • 节点不得保留连接日志。
  • +
  • 收款不得绑定个人实名账户。
  • +
  • 服务器、域名、支付、邮箱、手机号之间不得产生可被关联的身份线索;管理操作只在专用环境进行。
  • +
+

架构上的落实:providers 凭证、域名注册商、CDN 账号、备份存储账号全部独立身份并在文档中登记隔离关系;CI/CD 与管理操作走专用通道。

+
+ +
+

§3系统安全基线(汇总各章)

+
+
+

控制面

+
    +
  • 管理端独立监听:内网 + IP 白名单 + 2FA
  • +
  • SSH 仅密钥、禁 root 口令、最小开放端口
  • +
  • 密码 argon2id;JWT RS256 密钥轮换;TLS 1.3
  • +
  • 验证码/兑换/登录全限流 + 失败锁定 + 防一次性邮箱
  • +
  • 敏感操作全量 audit_log
  • +
+
+
+

数据面

+
    +
  • 节点零用户库:只持有 dp_uuid 表,被抄一台不泄露任何账号
  • +
  • agent ↔ 控制面双向 mTLS,证书绑节点、回收即吊销
  • +
  • 节点间隧道密钥按节点对发放
  • +
  • 节点无状态、无日志、无持久数据(04 章 §2)
  • +
+
+
+

密码学口径

+
    +
  • 口令哈希 argon2id;激活码库内只存 SHA-256
  • +
  • 端点/公告分发 Ed25519 签名,公钥随安装包内置
  • +
  • 备份 age 公钥加密,私钥离线两地保存
  • +
  • 内部 CA 签发 agent 证书,根私钥离线
  • +
+
+
+

客户端

+
    +
  • 凭证存 Keychain / Keystore,不落明文
  • +
  • Kill-switch 默认开;DoH 防 DNS 泄露;阻 WebRTC 泄露
  • +
  • 安装包多渠道发布 + 校验值公示
  • +
  • 敏感内置参数按分发渠道分包轮换
  • +
+
+
+
+ +
+

§4无日志口径(写进隐私政策并据实执行)

+
+ + + + + + + +
数据是否保留说明
浏览内容 / 目的地址 / DNS 查询不记录节点与控制面均无此数据,技术上不可补记
连接日志(谁何时连了哪个节点)不记录节点 no-log;控制面仅 connect 校验通过与否的限流计数(短 TTL)
用量字节数 / 分钟数(按日聚合)保留计费与免费额度所需的最小元数据(usage_daily)
账号(邮箱)、设备名、最后活跃保留登录与设备数限制所需;不收集真实身份
+

对外文案遵守设计铁律 13 的脱敏口径:「我们不记录你的浏览数据 · 端到端加密」。

+
+ +
+

§5断网应对策略矩阵(端 × 故障)

+

「断网」指各类不可达事件。目标排序:① 已连接用户不掉线 → ② 能重连 → ③ 能获取新入口 → ④ 能联系到我们。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
故障 \ 端移动 / 桌面客户端Web 用户中心官网
API 不可达(单域名)端点池自动故障转移(域名池→IP 直连→DoH 重解析),用户无感同源 API 池故障转移;只读功能照常纯静态,不依赖 API
API 全灭(控制面宕机)缓存目录 + 已下发凭证继续连接(数据面独立);登录/兑换暂停,连接不受影响暂不可用,公告位提示照常;展示状态公告
节点大面积被封URLTest 自动切存量好节点;目录刷新拿补充节点;极端时启用内置应急节点订阅导入页提示刷新订阅不受影响
域名被墙 / DNS 污染DoH 绕污染 → 失败走 IP 直连 → 签名端点更新拿新域名切镜像域名(公告/TG 广播)切镜像域名(≥2 活跃镜像常备)
用户本地断网(非封锁)连接键回 off 态 + 明确文案;Kill-switch 防泄露;网络恢复自动重连浏览器原生提示
一切在线途径失效内置应急节点拉新目录;客户端内「联系我们」展示 TG/LINE/邮箱(离线可见)TG 频道为最终广播渠道;安装包在 GitHub/网盘多镜像可获取
+
设计自检上表每个格子都不依赖「正在故障的那个组件」自身恢复——客户端兜底链(缓存 → 端点池 → DoH → 签名更新 → 应急节点 → TG)层层递降,每层的信任锚(内置公钥、应急参数)都在安装包内离线可用。
+
+ +
+

§6安全例行制度

+ +
+ +
+ + + + + + + + + diff --git a/doc/assets/doc.js b/doc/assets/doc.js new file mode 100644 index 0000000..d18acd5 --- /dev/null +++ b/doc/assets/doc.js @@ -0,0 +1,17 @@ +lucide.createIcons(); +(function () { + var btn = document.getElementById('themeBtn'); + if (localStorage.getItem('pg-doc-theme') === 'dark') { + document.documentElement.setAttribute('data-theme', 'dark'); + } + btn.addEventListener('click', function () { + var root = document.documentElement; + if (root.getAttribute('data-theme') === 'dark') { + root.removeAttribute('data-theme'); + localStorage.setItem('pg-doc-theme', 'light'); + } else { + root.setAttribute('data-theme', 'dark'); + localStorage.setItem('pg-doc-theme', 'dark'); + } + }); +})(); diff --git a/doc/assets/logo-mark.svg b/doc/assets/logo-mark.svg new file mode 100644 index 0000000..b3e364b --- /dev/null +++ b/doc/assets/logo-mark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/doc/assets/style.css b/doc/assets/style.css new file mode 100644 index 0000000..29b90d4 --- /dev/null +++ b/doc/assets/style.css @@ -0,0 +1,408 @@ +/* ============================================================= + 穿山甲 · Pangolin — 架构设计文档样式 + 令牌部分逐字拷贝自 design/colors_and_type.css(唯一真相源); + 改令牌请先改源文件再同步此处。文档版式附在令牌之后。 + ============================================================= */ + +@import url('https://fonts.googleapis.com/css2?family=Sora:wght@500;600;700&family=Manrope:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap'); + +:root { + /* Clay / Copper — the pangolin-armor primary (warm earth) */ + --clay-50: #FAF3ED; + --clay-100: #F2E2D4; + --clay-200: #E6C7AC; + --clay-300: #D9A982; + --clay-400: #CC8B5C; + --clay-500: #B96A3D; /* ← brand primary */ + --clay-600: #9E5630; + --clay-700: #7E4426; + --clay-800: #5E331D; + --clay-900: #3D2213; + + /* Sand / Taupe — warm neutral ramp */ + --sand-50: #FAF8F4; + --sand-100: #F2EEE7; + --sand-200: #E6DFD3; + --sand-300: #D2C8B8; + --sand-400: #B0A491; + --sand-500: #8C8270; + --sand-600: #6B6253; + --sand-700: #4E4940; + --sand-800: #2E2A24; + --sand-900: #1F1C18; + --sand-950: #14110E; + + /* Semantic hues (earth-tuned) */ + --green-400: #7FB07A; + --green-500: #5B8C5A; + --green-600: #467046; + --amber-400: #E2B05A; + --amber-500: #D69A3C; + --amber-600: #B47E29; + --red-400: #D4715A; + --red-500: #C0533B; + --red-600: #9E4230; + + /* Semantic tokens — light */ + --bg: var(--sand-50); + --bg-subtle: var(--sand-100); + --surface: #FFFFFF; + --surface-2: var(--sand-50); + --overlay: rgba(31, 28, 24, 0.45); + + --fg1: var(--sand-900); + --fg2: var(--sand-600); + --fg3: var(--sand-500); + --fg-on-accent: #FFFFFF; + + --accent: var(--clay-500); + --accent-hover: var(--clay-600); + --accent-press: var(--clay-700); + --accent-subtle: var(--clay-50); + --accent-border: var(--clay-200); + + --border: var(--sand-200); + --border-strong: var(--sand-300); + --ring: rgba(185, 106, 61, 0.35); + + --success: var(--green-500); + --success-subtle: #E9F0E6; + --warning: var(--amber-500); + --warning-subtle: #F8EED6; + --danger: var(--red-500); + --danger-subtle: #F6E1DA; + + /* Typography */ + --font-display: 'Sora', 'Noto Sans SC', system-ui, sans-serif; + --font-sans: 'Manrope', 'Noto Sans SC', system-ui, sans-serif; + --font-cjk: 'Noto Sans SC', 'Manrope', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', monospace; + + --text-display-xl: 3rem; + --text-display: 2.25rem; + --text-h1: 1.875rem; + --text-h2: 1.5rem; + --text-h3: 1.25rem; + --text-body-lg: 1.125rem; + --text-body: 1rem; + --text-sm: 0.875rem; + --text-caption: 0.75rem; + + --leading-tight: 1.15; + --leading-snug: 1.3; + --leading-normal:1.5; + --leading-relaxed:1.65; + + --tracking-tight: -0.02em; + --tracking-snug: -0.01em; + --tracking-wide: 0.04em; + --tracking-caps: 0.08em; + + /* Spacing (4px base) */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + --space-16: 4rem; + + /* Radii */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 20px; + --radius-2xl: 28px; + --radius-full: 999px; + + /* Shadows (warm-tinted, soft) */ + --shadow-sm: 0 1px 2px rgba(45, 30, 20, 0.06); + --shadow-md: 0 4px 14px rgba(45, 30, 20, 0.08); + --shadow-lg: 0 12px 32px rgba(45, 30, 20, 0.12); + --shadow-xl: 0 24px 60px rgba(45, 30, 20, 0.16); + --shadow-focus: 0 0 0 4px var(--ring); + + /* Motion */ + --ease-out: cubic-bezier(0.22, 1, 0.36, 1); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --dur-fast: 140ms; + --dur-base: 220ms; + --dur-slow: 360ms; +} + +[data-theme="dark"] { + --bg: var(--sand-950); + --bg-subtle: var(--sand-900); + --surface: #221E19; + --surface-2: #2A251F; + --overlay: rgba(0, 0, 0, 0.6); + + --fg1: #F4EFE8; + --fg2: #B6AC9C; + --fg3: #897F6F; + --fg-on-accent: #1F1C18; + + --accent: var(--clay-400); + --accent-hover: var(--clay-300); + --accent-press: var(--clay-500); + --accent-subtle: rgba(204, 139, 92, 0.14); + --accent-border: rgba(204, 139, 92, 0.30); + + --border: rgba(242, 238, 231, 0.10); + --border-strong: rgba(242, 238, 231, 0.18); + --ring: rgba(204, 139, 92, 0.45); + + --success: var(--green-400); + --success-subtle: rgba(127, 176, 122, 0.16); + --warning: var(--amber-400); + --warning-subtle: rgba(226, 176, 90, 0.16); + --danger: var(--red-400); + --danger-subtle: rgba(212, 113, 90, 0.16); + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.45); + --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5); + --shadow-xl: 0 24px 60px rgba(0, 0, 0, 0.55); +} + +/* ============================================================= + 文档版式(doc-only) + ============================================================= */ + +* { box-sizing: border-box; margin: 0; padding: 0; } +html { scroll-behavior: smooth; } +body { + background: var(--bg); + color: var(--fg1); + font-family: var(--font-sans); + font-size: var(--text-body); + line-height: var(--leading-normal); + transition: background-color var(--dur-base) var(--ease-in-out), color var(--dur-base) var(--ease-in-out); +} +a { color: var(--accent); text-decoration: none; transition: color var(--dur-fast) var(--ease-in-out); } +a:hover { color: var(--accent-hover); } +strong { font-weight: 600; } + +/* ---- 顶栏 ---- */ +.site-header { + position: sticky; top: 0; z-index: 50; + backdrop-filter: blur(12px); + background: color-mix(in srgb, var(--bg) 82%, transparent); + border-bottom: 1px solid var(--border); +} +.site-header .inner { + max-width: 1120px; margin: 0 auto; + display: flex; align-items: center; gap: var(--space-4); + padding: var(--space-3) var(--space-6); +} +.brand { display: flex; align-items: center; gap: var(--space-2); color: var(--fg1); } +.brand img { width: 26px; height: 26px; display: block; } +.brand .zh { font-family: var(--font-display); font-weight: 600; font-size: var(--text-body); letter-spacing: var(--tracking-snug); } +.brand .en { font-size: 0.625rem; font-weight: 600; letter-spacing: var(--tracking-caps); color: var(--fg3); text-transform: uppercase; } +.site-nav { margin-left: auto; display: flex; gap: 2px; flex-wrap: wrap; } +.site-nav a { + padding: 5px 11px; border-radius: var(--radius-full); + font-size: var(--text-sm); color: var(--fg2); white-space: nowrap; + transition: background-color var(--dur-fast) var(--ease-in-out), color var(--dur-fast) var(--ease-in-out); +} +.site-nav a:hover { background: var(--accent-subtle); color: var(--accent); } +.site-nav a.active { background: var(--accent-subtle); color: var(--accent); font-weight: 600; } +.theme-btn { + margin-left: var(--space-2); + width: 32px; height: 32px; border-radius: var(--radius-full); + border: 1px solid var(--border); background: var(--surface); color: var(--fg2); + cursor: pointer; display: grid; place-items: center; + transition: border-color var(--dur-fast) var(--ease-in-out); +} +.theme-btn:hover { border-color: var(--accent-border); color: var(--accent); } +.theme-btn svg { width: 15px; height: 15px; } + +/* ---- 正文容器 ---- */ +.doc { max-width: 1120px; margin: 0 auto; padding: var(--space-12) var(--space-6) var(--space-16); } + +.doc-hero { max-width: 760px; margin-bottom: var(--space-10); } +.overline { + font-size: var(--text-caption); font-weight: 600; + letter-spacing: var(--tracking-caps); text-transform: uppercase; + color: var(--accent); margin-bottom: var(--space-3); +} +h1 { + font-family: var(--font-display); font-size: var(--text-display); + font-weight: 700; line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); + margin-bottom: var(--space-4); +} +.doc-hero .lede { color: var(--fg2); font-size: var(--text-body-lg); line-height: var(--leading-relaxed); } +.doc-meta { display: flex; gap: var(--space-4); margin-top: var(--space-5); font-size: var(--text-caption); color: var(--fg3); font-family: var(--font-mono); } + +section { margin-top: var(--space-12); } +h2 { + font-family: var(--font-display); font-size: var(--text-h2); font-weight: 600; + letter-spacing: var(--tracking-snug); line-height: var(--leading-snug); + margin-bottom: var(--space-5); + padding-top: var(--space-2); +} +h2 .num { color: var(--accent); font-family: var(--font-mono); font-size: var(--text-body); font-weight: 500; margin-right: var(--space-2); } +h3 { font-size: var(--text-h3); font-weight: 600; margin: var(--space-8) 0 var(--space-3); } +h4 { font-size: var(--text-body); font-weight: 600; margin: var(--space-6) 0 var(--space-2); } +p { margin-bottom: var(--space-3); color: var(--fg1); max-width: 76ch; } +p.muted, .muted { color: var(--fg2); } +ul, ol { margin: 0 0 var(--space-4) 1.25em; max-width: 76ch; } +li { margin-bottom: var(--space-1); } +li::marker { color: var(--accent); } + +/* ---- 卡片与栅格 ---- */ +.card { + background: var(--surface); border: 1px solid var(--border); + border-radius: var(--radius-xl); padding: var(--space-6); + box-shadow: var(--shadow-sm); +} +.grid2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-4); } +.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); } +@media (max-width: 860px) { .grid2, .grid3 { grid-template-columns: 1fr; } } + +/* 章节导航卡(index 用) */ +.chapter-card { + display: block; background: var(--surface); border: 1px solid var(--border); + border-radius: var(--radius-xl); padding: var(--space-6); + box-shadow: var(--shadow-sm); color: var(--fg1); + transition: box-shadow var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out); +} +.chapter-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); border-color: var(--accent-border); color: var(--fg1); } +.chapter-card .ic { + width: 38px; height: 38px; border-radius: var(--radius-md); + background: var(--accent-subtle); color: var(--accent); + display: grid; place-items: center; margin-bottom: var(--space-3); +} +.chapter-card .ic svg { width: 19px; height: 19px; } +.chapter-card .no { font-family: var(--font-mono); font-size: var(--text-caption); color: var(--fg3); } +.chapter-card b { display: block; font-family: var(--font-display); font-size: var(--text-body-lg); font-weight: 600; margin: 2px 0 var(--space-2); } +.chapter-card p { font-size: var(--text-sm); color: var(--fg2); margin: 0; } + +/* ---- 表格 ---- */ +.tbl-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); box-shadow: var(--shadow-sm); margin-bottom: var(--space-4); } +table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); } +th { + text-align: left; background: var(--bg-subtle); color: var(--fg2); + font-size: var(--text-caption); font-weight: 600; letter-spacing: var(--tracking-wide); + padding: 10px 14px; border-bottom: 1px solid var(--border); white-space: nowrap; +} +td { padding: 11px 14px; border-bottom: 1px solid var(--border); vertical-align: top; } +tr:last-child td { border-bottom: none; } +td code, li code, p code { + font-family: var(--font-mono); font-size: 0.85em; + background: var(--bg-subtle); border: 1px solid var(--border); + padding: 1px 6px; border-radius: var(--radius-sm); white-space: nowrap; +} + +/* ---- 代码块 ---- */ +pre { + background: var(--sand-900); color: #F4EFE8; + padding: var(--space-5); border-radius: var(--radius-lg); + overflow-x: auto; font-size: var(--text-sm); line-height: 1.65; + margin-bottom: var(--space-4); box-shadow: var(--shadow-sm); +} +pre code { font-family: var(--font-mono); background: none; border: none; padding: 0; } +pre .c { color: #8C8270; } /* comment */ +pre .k { color: #CC8B5C; } /* keyword (clay-400) */ +pre .s { color: #7FB07A; } /* string (green-400) */ +[data-theme="dark"] pre { background: var(--surface-2); border: 1px solid var(--border); } + +/* ---- 状态胶囊(色点 + 文字,铁律 7) ---- */ +.pill { + display: inline-flex; align-items: center; gap: 6px; + padding: 2px 10px; border-radius: var(--radius-full); + font-size: var(--text-caption); font-weight: 600; white-space: nowrap; +} +.pill .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; } +.pill.ok { background: var(--success-subtle); color: var(--success); } +.pill.ok .dot { background: var(--success); } +.pill.warn { background: var(--warning-subtle); color: var(--warning); } +.pill.warn .dot { background: var(--warning); } +.pill.bad { background: var(--danger-subtle); color: var(--danger); } +.pill.bad .dot { background: var(--danger); } +.pill.brand { background: var(--accent-subtle); color: var(--accent); } +.pill.brand .dot { background: var(--accent); } +.pill.dim { background: var(--bg-subtle); color: var(--fg2); } +.pill.dim .dot { background: var(--fg3); } + +/* ---- 提示块 ---- */ +.callout { + border-left: 3px solid var(--accent); background: var(--accent-subtle); + border-radius: var(--radius-md); padding: var(--space-4) var(--space-5); + margin: var(--space-4) 0; font-size: var(--text-sm); max-width: 86ch; +} +.callout b:first-child { display: block; margin-bottom: 4px; } +.callout.warn { border-left-color: var(--warning); background: var(--warning-subtle); } +.callout.danger { border-left-color: var(--danger); background: var(--danger-subtle); } +.callout.ok { border-left-color: var(--success); background: var(--success-subtle); } +.callout p { margin-bottom: var(--space-2); } +.callout p:last-child, .callout ul:last-child { margin-bottom: 0; } + +/* ---- 流程图(CSS 盒 + 箭头) ---- */ +.diagram { + background: var(--bg-subtle); border: 1px solid var(--border); + border-radius: var(--radius-xl); padding: var(--space-6); + overflow-x: auto; margin-bottom: var(--space-4); +} +.diagram .cap { + font-size: var(--text-caption); color: var(--fg3); text-align: center; + margin-top: var(--space-4); font-family: var(--font-mono); +} +.flow { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; } +.flow + .flow { margin-top: var(--space-4); } +.fbox { + background: var(--surface); border: 1.5px solid var(--border-strong); + border-radius: var(--radius-lg); padding: var(--space-3) var(--space-4); + min-width: 110px; text-align: center; flex: none; + box-shadow: var(--shadow-sm); +} +.fbox b { display: block; font-size: var(--text-sm); font-weight: 600; } +.fbox span { display: block; font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; } +.fbox.accent { border-color: var(--accent-border); background: var(--accent-subtle); } +.fbox.accent b { color: var(--accent); } +.fbox.ok { border-color: var(--success); background: var(--success-subtle); } +.fbox.ok b { color: var(--success); } +.fbox.warn { border-color: var(--warning); background: var(--warning-subtle); } +.fbox.warn b { color: var(--warning); } +.fbox.bad { border-color: var(--danger); background: var(--danger-subtle); } +.fbox.bad b { color: var(--danger); } +.fbox.ghost { border-style: dashed; background: transparent; } +.farr { color: var(--fg3); font-family: var(--font-mono); font-size: var(--text-sm); flex: none; padding: 0 2px; } +.fnote { font-size: var(--text-caption); color: var(--fg3); flex-basis: 100%; } + +/* 分层堆叠图 */ +.stack { display: flex; flex-direction: column; gap: var(--space-2); } +.stack .layer { + background: var(--surface); border: 1px solid var(--border); + border-radius: var(--radius-lg); padding: var(--space-3) var(--space-5); + display: flex; align-items: baseline; gap: var(--space-4); flex-wrap: wrap; +} +.stack .layer .t { font-weight: 600; font-size: var(--text-sm); min-width: 11em; } +.stack .layer .d { font-size: var(--text-caption); color: var(--fg2); } +.stack .layer.accent { background: var(--accent-subtle); border-color: var(--accent-border); } +.stack .layer.accent .t { color: var(--accent); } + +/* ---- 翻页 ---- */ +.pager { + max-width: 1120px; margin: 0 auto; padding: 0 var(--space-6) var(--space-16); + display: flex; justify-content: space-between; gap: var(--space-4); +} +.pager a { + flex: 1; max-width: 380px; display: block; + background: var(--surface); border: 1px solid var(--border); + border-radius: var(--radius-lg); padding: var(--space-4) var(--space-5); + color: var(--fg1); box-shadow: var(--shadow-sm); + transition: border-color var(--dur-fast) var(--ease-in-out), box-shadow var(--dur-base) var(--ease-out); +} +.pager a:hover { border-color: var(--accent-border); box-shadow: var(--shadow-md); } +.pager a span { display: block; font-size: var(--text-caption); color: var(--fg3); margin-bottom: 2px; } +.pager a b { font-size: var(--text-sm); font-weight: 600; } +.pager a.next { text-align: right; margin-left: auto; } + +footer.colophon { + text-align: center; padding: var(--space-8) var(--space-6) var(--space-12); + font-size: var(--text-caption); color: var(--fg3); border-top: 1px solid var(--border); +} diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 0000000..9b6eaed --- /dev/null +++ b/doc/index.html @@ -0,0 +1,153 @@ + + + + + +总览 · 穿山甲架构设计 + + + + + + +
+ +
+
Architecture Design · v1.0
+

穿山甲 整体架构设计

+

极简、轻量、亲和的跨平台网络加速应用。本设计覆盖前端五端(移动 / iPad / 桌面 / 官网 / 用户中心)、Go 控制面、MySQL 数据层、弹性节点拓扑、Web 安全与灾备、以及贯穿全局的安全与断网应对策略。UI 实现以 design/ui_kits/ React 原型为像素基准,100% 还原既有设计系统。

+
2026-06-11依据 design/ + plan/ + server/ARCHITECTURE.md v0.1
+
+ +
+

§1关键架构决策

+
+ + + + + + + + + +
领域决策理由 / 相对蓝本的变化
数据面协议sing-box:VLESS + Vision + REALITY 主线,Hysteria2 备线(端口跳跃)替换蓝本 v0.1 的 WireGuard——WG 协议特征明显,在目标网络环境必被识别阻断;plan/ phase-0~3 已实测选定 REALITY。API 契约形状保留,仅 connect 语义适配(见 02 章)。
数据库MySQL 8.x(InnoDB / utf8mb4)+ Redis按本次要求替换蓝本的 Postgres;schema 全量移植并做类型适配(见 03 章)。
移动 / iPad / 桌面客户端Flutter(共享 ~90% UI 代码)+ sing-box libbox 内核;iPad = 移动端工程宽度 ≥900 自适应切侧栏分栏(像素基准 ui_kits/tablet/design/flutter/ 起步包即为此准备;原生 VPN 壳(NetworkExtension / VpnService / TUN)性能与稳定性最优。
官网Astro SSG → Cloudflare Pages 多镜像纯静态、秒级整站复制到新域名,抗封锁;ui_kits/website JSX 近乎逐字迁移。
Web 用户中心Next.js(React)ui_kits/usercenter 本身是 React——组件直接复用,还原保真度最高。
节点拓扑三层弹性拓扑(入口 / 中转 / 落地)+ 节点生命周期状态机 + 一键/自动换节点换 IP补全 plan/ 中"IP 轮换自动化"的具体设计:判封探针、多厂商 VPS 池、目录版本灰度(见 04 章)。
+
+ +
+

§2系统全景

+
+
+
移动 AppFlutter · iOS/Android/iPad
+
桌面客户端Flutter · Win/macOS
+
用户中心Next.js
+
官网Astro · 静态
+
+
+
│ HTTPS(域名池 + CDN 前置 + IP 直连兜底)
+
+
+
CDN / 域名池Cloudflare · 源站隐藏
+
+
API 控制面Go 单体 · 模块分包
+
+
MySQL 8主数据 · 加密异地备份
+
Redis验证码/限流/节点状态
+
+
+
发卡店 Webhook激活码入库(签名)
+
→ 控制面 ←
+
管理端内网 · 白名单 + 2FA
+
探针系统多 ISP 拨测 · 判封
+
+
+
│ mTLS gRPC(节点注册 / 心跳 / 凭证下发 / 回收)
+
+
+
入口节点池消耗品 IP · 可秒换
+
+
中转层(可选)隧道转发
+
+
落地节点池精品 IP · 稳定出口
+
用户流量(REALITY / Hysteria2)只走数据面,绝不经过 API;节点零用户库。
+
+
控制面 / 数据面彻底分离 · 节点目录带版本灰度 · 任一节点被封不影响全局
+
+

四条铁律(继承蓝本 §0):① 控制面/数据面分离;② App 内无支付,资金流全部外部化为激活码;③ 节点可秒级灰度,客户端缓存最后一份可用目录兜底;④ 最小数据原则——不记录浏览内容与目的地,仅保留计费所需的字节数/分钟数。

+
+ +
+

§3章节导航

+ +
+ +
+

§4设计依据与文档关系

+
+ + + + + + + +
来源角色本设计如何使用
design/CLAUDE.md + colors_and_type.css设计铁律与令牌真相源13 条铁律作为前端验收清单;套餐口径(§7)为所有界面/接口数字的单一来源。
design/ui_kits/*像素验收标准各端实现逐屏对照 React 原型(含新增 tablet/ iPad kit),差异即缺陷。
design/server/ARCHITECTURE.md(v0.1)后端蓝本API 契约、数据模型、激活码流程全部继承;数据面协议与 DB 选型按本设计修订。
plan/phase-0~3 + plan/security-运营安全.md实施路线与威胁模型协议选型(REALITY/Hy2)、IP 分级(消耗品/精品池)、身份隔离红线全部纳入。
+
套餐口径(引用 design/CLAUDE.md §7,改数字先改那里) + 注册享 7 天免费试用(不限时长与节点)→ 之后免费版 1 个基础节点 + 每日 10 分钟 + 每日使用前看激励视频解锁;PRO ¥25/月(年付 ¥20/月),80+ 线路,5 台设备;团队版 ¥99/月 10 席位。收款走外部渠道(发卡店 / USDT TRC20 / Telegram / LINE / 邮箱),App 与官网内无支付表单。
+
+ +
+ + + + + + + + +