Initial LisgloSIPS V2 implementation

This commit is contained in:
hectorzhao
2026-06-22 10:56:38 +08:00
commit 5fa1bd35e9
303 changed files with 35644 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
# CDR Minimal Billing Runbook
> 任务:S23 - 最小计费与余额扣减
> 完成时间:2026-06-21 19:55 +08:00
## 1. 目标
S23 在 S22 的 Redis Stream 消费基础上完成最小计费闭环:
- 成功通话 CDR 写入 `raw_cdrs``rated_cdrs`
- 按周期秒数和周期费率计算费用。
- 扣减客户余额,并写入不可变客户余额流水。
- 同一 `event_id` 或同一 `raw_cdr_id` 不重复扣费。
当前系统尚未建立客户侧费率表,因此 S23 使用落地网关的 `billingCycleSec``cycleRate` 同时作为供应商成本和临时客户费用,`grossProfit``0.000000`。正式客户费率和利润计算留给后续计费版本。
## 2. 计费规则
`apps/worker-cdr/src/billing.ts` 提供周期计费函数:
```text
cycles = ceil(duration_sec / billing_cycle_sec)
bill_sec = cycles * billing_cycle_sec
amount = cycles * cycle_rate
```
约束:
- `duration_sec` 必须为非负整数。
- `billing_cycle_sec` 必须为 1 到 60 的整数。
- 金额统一保留 6 位小数。
- `sip_code``2xx``duration_sec > 0` 时才进入计费。
失败 CDR、零时长 CDR、缺少客户或落地网关 ID 的 CDR 只写 `raw_cdrs``ratingStatus` 标记为 `SKIPPED`,不扣余额。
## 3. 写库与幂等
`apps/worker-cdr/src/rating.ts``CdrRatingService` 在单个数据库事务内处理:
1.`raw_cdrs.event_id` 查询是否已处理。
2. 新建 `raw_cdrs`
3. 锁定客户余额行:`SELECT ... FOR UPDATE`
4. 读取落地网关周期费率。
5. 新建 `rated_cdrs`
6. 新建一条负数 `customer_recharges` 作为扣费流水。
7. 更新 `customers.balance`
8.`raw_cdrs.ratingStatus` 标记为 `RATED`
扣费流水约定:
| 字段 | 值 |
| --- | --- |
| `amount` | 负数费用 |
| `idempotencyKey` | `cdr:<event_id>` |
| `remark` | `CDR_CHARGE:<raw_cdr_id>` |
| `createdBy` | `worker-cdr` |
数据库唯一约束共同防止重复扣费:
- `raw_cdrs.event_id`
- `rated_cdrs.raw_cdr_id`
- `customer_recharges.idempotencyKey`
## 4. Worker 行为
`apps/worker-cdr/src/main.ts` 现在连接 Redis 和 MySQL
1.`stream:cdr_payload``billing-workers` Consumer Group 读取 CDR。
2. 解析成功后调用 `CdrRatingService.rate(event)`
3. 处理成功或重复消息后 ACK Redis 消息。
4. 处理异常时沿用 S22 死信机制写入 `stream:cdr_deadletter`
必需环境变量:
```text
REDIS_URL
DATABASE_URL
```
可选环境变量:
```text
CDR_CONSUMER_NAME
CDR_BLOCK_MS
CDR_BATCH_SIZE
CDR_PENDING_IDLE_MS
LISGLOSIPS_LOG_LEVEL
```
## 5. 验证结果
- `corepack pnpm@10.33.0 exec vitest run apps/worker-cdr/src/rating.spec.ts` 通过,3 条测试覆盖周期计费、成功 CDR 扣费幂等、失败 CDR 不扣费。
- `corepack pnpm@10.33.0 --filter @lisglosips/worker-cdr build` 通过。
- `corepack pnpm@10.33.0 typecheck` 通过。
- `corepack pnpm@10.33.0 lint` 通过。
- `corepack pnpm@10.33.0 build` 通过。
本次未连接 B 执行服务部署或真实库写入验证,因为 B sudo 凭据仍未通过校验。S23 仅完成本地代码闭环和构建验收。
## 6. 回滚
代码回滚:
- 恢复 `apps/worker-cdr/src/main.ts` 到 S22 只记录 CDR 的版本。
- 删除或恢复 `apps/worker-cdr/src/billing.ts``apps/worker-cdr/src/rating.ts``apps/worker-cdr/src/rating.spec.ts`
- 恢复 `apps/worker-cdr/package.json``apps/worker-cdr/tsconfig.json`
- 重新执行 `corepack pnpm@10.33.0 build`
数据回滚:
- S23 未执行真实数据库写入验证,无需清理远端数据。
- 若后续环境已运行 worker 并产生扣费数据,回滚前必须先停止 worker,记录待回滚的 `event_id``raw_cdr_id``rated_cdr_id` 和客户流水;不得直接删除余额流水,应通过受控反向流水或备份恢复方案处理。
+108
View File
@@ -0,0 +1,108 @@
# CDR Redis Stream Runbook
> 任务:S22 - CDR Redis Stream
> 完成时间:2026-06-21 19:34 +08:00
## 1. 目标
S22 建立原始 CDR 的 Redis Stream 契约和可靠消费基础,不做计费、不扣余额、不写正式 rated CDR。计费与余额扣减留给 S23。
## 2. Redis Key
| Key | 用途 |
| --- | --- |
| `stream:cdr_payload` | OpenSIPS 写入的原始 CDR Stream |
| `stream:cdr_deadletter` | CDR Worker 校验或处理失败后的死信 Stream |
| `billing-workers` | `stream:cdr_payload` 的 Consumer Group |
| `lock:cdr:{event_id}` | Worker 处理幂等占位,避免同一 `event_id` 重复处理 |
## 3. 事件字段
S22 事件 schema version 为 `1`。必填字段:
```text
schema_version
event_id
idempotency_key
call_id
node_id
opensips_instance
ingress_a_ip
rtpengine_node
source_ip
sip_code
hangup_reason
created_at
```
多 A 预留字段已经纳入契约:
- `node_id`A 节点 ID,当前单 A 为 `a1`
- `opensips_instance`OpenSIPS 实例名,当前为 `opensips-a1`
- `ingress_a_ip`:接收 SIP 的 A 节点地址,当前为 `100.90.90.90`
- `rtpengine_node`:媒体节点,当前每台 A 绑定本机 RTPEngine,因此为 `a1`
S22 阶段 A 端尚未进入真实落地路由,因此失败 CDR 的供应商、录音、应答时间等字段使用 `none``0` 占位。S23/S24 后续在不破坏 schema 的前提下补充真实计费和录音字段。
## 4. Producer
Server A OpenSIPS 在 `route[S20_CDR_XADD]` 中执行:
```text
XADD stream:cdr_payload * schema_version 1 ... created_at <unix>
```
当前 A 端字段已从 S20 冒烟格式升级为 S22 正式契约。A 端配置备份:
```text
/var/backups/lisglosips-s22/20260621T113157Z/opensips.cfg
```
## 5. Consumer
`packages/redis/src/cdr-stream.ts` 提供:
- `ensureCdrConsumerGroup`
- `publishCdrEvent`
- `parseCdrStreamEvent`
- `processCdrBatch`
- `processPendingCdrBatch`
`apps/worker-cdr` 已从骨架改为 Redis Stream Worker
1. 读取 `REDIS_URL`
2. 确认 `billing-workers` Consumer Group。
3. 先用 `XAUTOCLAIM` 重领 Pending,再用 `XREADGROUP` 读取新消息。
4.`lock:cdr:{event_id}` 做幂等占位。
5. 处理成功或重复消息后 `XACK`
6. 校验失败或处理异常时写入 `stream:cdr_deadletter``XACK` 原消息。
当前 handler 只记录并接受原始 CDR,不执行计费和余额扣减。
## 6. 验证结果
- 本地 `packages/redis/src/cdr-stream.spec.ts` 6 条测试通过,覆盖发布、解析、Consumer Group 幂等创建、`XREADGROUP`/`XACK`、重复 `event_id`、死信和 `XAUTOCLAIM` Pending 重领。
- `corepack pnpm@10.33.0 lint` 通过。
- `corepack pnpm@10.33.0 typecheck` 通过。
- `corepack pnpm@10.33.0 build` 通过。
- A `opensips -C -f /etc/opensips/opensips.cfg` 通过,`opensips.service` active。
- Redis `XGROUP CREATE stream:cdr_payload billing-workers 0 MKSTREAM` 返回 `OK`
- Redis `XINFO GROUPS stream:cdr_payload` 显示 `billing-workers`pending 为 `0`
- T 发起 INVITE 后,A 返回 `SIP/2.0 503 Config Missing`Redis Stream 最新事件包含 `schema_version=1``idempotency_key``node_id=a1``opensips_instance=opensips-a1``ingress_a_ip=100.90.90.90``rtpengine_node=a1`
## 7. 回滚
代码回滚:
- 恢复 `packages/redis/src/index.ts``packages/redis/src/cdr-stream.ts``packages/redis/src/cdr-stream.spec.ts``apps/worker-cdr/src/main.ts`
- 重新执行 `corepack pnpm@10.33.0 build`
Server A 回滚:
```bash
sudo cp -a /var/backups/lisglosips-s22/20260621T113157Z/opensips.cfg /etc/opensips/opensips.cfg
sudo opensips -C -f /etc/opensips/opensips.cfg
sudo systemctl restart opensips
```
Redis Consumer Group 可保留,不影响 Producer;如必须移除,应先确认没有未处理 Pending,再使用受控 Redis 维护窗口删除 group。S22 不删除 Stream 数据。
+65
View File
@@ -0,0 +1,65 @@
# S26 Dashboard 聚合 Runbook
## 范围
S26 在 API 中新增 Dashboard 聚合接口:
- `GET /api/v2/dashboard/summary`
- `GET /api/v2/dashboard/trends?hours=24&bucketMinutes=60`
两个接口均要求 `dashboard.view` 权限。S26 未连接服务器、未重启服务、未执行数据库迁移,也未修改 Redis/OpenSIPS 配置。
## 数据来源
当前实现基于已有 V2 Schema:
- `raw_cdrs`:今日通话数、接通数、失败数、接通率、通话时长、失败响应码、趋势通话量。
- `rated_cdrs` + `raw_cdrs.started_at`:客户费用、供应商成本、毛利和趋势金额。
- `customers``customer_gateways``vendor_gateways`:启用实体数量。
- `recordings` + `quality_reviews`:待质检录音数。
- `raw_cdrs.vendor_gateway_id`:今日失败落地网关 Top 10。
实时在线通话和注册数在 S26 返回 `0` 且标记 `source: "not_configured"`。这部分应在后续接入 Redis/OpenSIPS 指标后替换,避免返回伪实时数据。
## 查询窗口
- Summary 的“今日”按 `Asia/Shanghai` 自然日计算,数据库时间仍按 UTC ISO 语义传入。
- Trends 默认最近 24 小时,支持 `hours=1..168`
- `bucketMinutes` 仅允许 `5``15``60`
- 所有 CDR 查询都带 `started_at` 时间窗口,避免首页扫描全量 CDR。
## 后续预聚合演进
设计文档要求 24 小时趋势最终使用 5 分钟或 1 小时预聚合。当前 S26 没有新增指标表,是为了避免把本任务扩大为数据库迁移和 worker 部署。
后续可新增 `worker-metrics` 或复用 CDR Worker 增量写入统计表,例如:
```text
dashboard_call_buckets(bucket_start, bucket_minutes, total_calls, answered_calls, failed_calls, duration_sec, customer_fee, vendor_cost, gross_profit)
dashboard_failure_codes(bucket_start, bucket_minutes, sip_code, count)
dashboard_gateway_health(vendor_gateway_id, status, reason, updated_at)
```
切换时保持 API 响应结构不变,只替换 `DashboardRepository` 的数据来源。
## 验证
本地验收命令:
```bash
corepack pnpm@10.33.0 exec vitest run apps/api/src/modules/dashboard/dashboard.service.spec.ts
corepack pnpm@10.33.0 typecheck
corepack pnpm@10.33.0 lint
corepack pnpm@10.33.0 build
```
## 回滚
代码级回滚:
1.`apps/api/src/modules/app.module.ts` 移除 `DashboardModule`
2. 删除 `apps/api/src/modules/dashboard/`
3. 删除本 Runbook。
4. 重新执行 `corepack pnpm@10.33.0 build`
S26 未执行数据库迁移、服务器写操作或真实数据写入,因此没有远端数据回滚点。
+66
View File
@@ -0,0 +1,66 @@
# LisgloSIPS V2 数据库 Schema 与迁移 Runbook
## 范围
本文对应 `S08 - 数据库 Schema 与迁移`。业务数据库使用 MySQLSchema 由 `prisma/schema.prisma` 管理,初始化迁移位于 `prisma/migrations/20260621090000_init_v2_schema/`
## 本次产物
- `prisma/schema.prisma`:V2 核心业务表、索引、关系、枚举和审计列。
- `prisma/migrations/20260621090000_init_v2_schema/migration.sql`:从空库初始化的 SQL 迁移。
- `prisma/seed.ts`:内置权限和角色种子;不创建默认密码用户。
- `packages/database/src/schema-contract.spec.ts`Schema 合同测试。
## 标准初始化
只在空库或明确的新环境执行:
```bash
export DATABASE_URL='mysql://<APP_USER>@127.0.0.1:3306/<DATABASE_NAME>'
pnpm prisma:generate
pnpm exec prisma migrate deploy
pnpm db:seed
```
`DATABASE_URL` 示例不包含密码。实际凭据必须来自受控 Secret 文件或部署环境变量,禁止写入仓库。
## 验证命令
```bash
pnpm prisma:validate
pnpm typecheck
pnpm test
pnpm build
```
数据库侧验收:
```bash
pnpm exec prisma migrate deploy
pnpm exec prisma migrate status
pnpm db:seed
pnpm db:seed
```
重复执行 `migrate deploy``db:seed` 应安全:迁移不会重复应用,seed 使用 upsert。
## 回滚与恢复
初始化迁移只允许用于空库。若在临时库验证失败:
```sql
DROP DATABASE <TEMP_DATABASE_NAME>;
DROP USER '<TEMP_USER>'@'127.0.0.1';
DROP USER '<TEMP_USER>'@'localhost';
```
生产或准生产环境禁止依赖破坏性 down migration 回滚。若迁移已进入共享环境,应优先发布向前兼容修复迁移;上线窗口不执行不可逆删列。
## 重要约束
- 金额字段统一 `DECIMAL(20,6)`
- 充值、CDR、审计日志不做物理删除。
- `raw_cdrs.event_id``raw_cdrs.call_id + ended_at` 保证 CDR 幂等。
- 充值表和 `idempotency_keys` 均有幂等唯一键。
- 所有管理类表包含 `created_at``updated_at``created_by``updated_by``version`
- 密码字段只保存 hash 或 SIP Digest HA1seed 不写入明文密码。
+172
View File
@@ -0,0 +1,172 @@
# LisgloSIPS 部署实施指南
本文是 LisgloSIPS V2 的独立部署交接文档。目标是让新的 Codex 会话在不依赖聊天记忆的情况下,按阶段完成本地开发部署、后端实现、联调和阿里云迁移。
设计依据:`SOFTSWITCH_PLATFORM_DESIGN_V2.md`。进度依据:`IMPLEMENTATION_STATUS.md`。任何会话开始时先读取这两个文件,并且一次只执行当前 Sxx 任务。
## 1. 环境与边界
当前阶段使用三台本地 KVM 开发服务器,通过 Tailscale 通信:
| 服务器 | Tailscale 地址 | 职责 |
| --- | --- | --- |
| A | `100.90.90.90` | OpenSIPS、RTPEngine、录音内存盘、HEP、Exporter |
| B | `100.90.90.91` | API/Worker、MySQL、Redis、Web、HOMER、监控和录音持久化 |
| T | `100.93.185.30` | 模拟客户 SIP 注册、IP 对接和测试呼叫 |
开发完成后再迁移阿里云。当前 IP、开发证书和防火墙源地址不能原样作为生产配置。
## 2. 凭据规则
- SSH 与 sudo 凭据只存放在 `.codex-private/`,禁止写入文档、脚本、Git、日志或命令回显。
- 服务账号凭据保存在 `.codex-private/SERVICE_CREDENTIALS.clixml` 和服务器 `/etc/lisglosips/secrets/`
- 生产使用云 Secret 服务或权限为 `0600/0640` 的受控文件。
- 部署文档只记录凭据位置、权限和轮换流程,不记录明文。
## 3. 总体部署顺序
```text
S00-S02 安全接入、资产与网络
S03-S06 Server B 基础设施
S07-S17 后端骨架与运营业务 API
S18-S21 Server A/T 通信与呼叫基线
S22-S26 CDR、计费、录音、质检与 Dashboard
S27 React 接入真实 API
S28-S29 端到端、性能、故障与安全测试
S30 阿里云迁移、灰度、回滚和验收
```
禁止提前跨阶段实施依赖尚未完成的任务。例如 S08 Schema 完成前,不部署真实业务 API;S22 CDR 事件契约完成前,不让 OpenSIPS 写入正式扣费链路。
## 4. 已完成部署基线
截至 S06
| 阶段 | 结果 | 文档 |
| --- | --- | --- |
| S00 | SSH Key、host key 修复、SSH 管理端口调整、凭据轮换;保留密钥登录 | `docs/SSH_ACCESS_RUNBOOK.md` |
| S01 | 三机资产盘点 | `docs/inventory-summary.md` |
| S02 | Tailscale 网络和最小端口矩阵 | `docs/infra-check.md` |
| S03 | B 内核、chrony、nftables、fail2ban、目录和 NFS automount | `docs/SERVER_B_BASELINE_RUNBOOK.md` |
| S04 | B MySQL、Redis、ACL、持久化、备份与恢复演练 | `docs/SERVER_B_DATA_SERVICES_RUNBOOK.md` |
| S05 | B Node.js、pnpm、Nginx、systemd、TLS 与发布目录 | `docs/SERVER_B_WEB_RUNTIME_RUNBOOK.md` |
| S06 | B HOMER、HEP 接收、Prometheus、Grafana、ExportersA Node Exporter | `docs/SERVER_B_HOMER_MONITORING_RUNBOOK.md` |
所有可提交的 Server B 配置副本位于 `infra/server-b/s03/``s04/``s05/``s06/`Server A 的 S06 采集配置位于 `infra/server-a/s06/`。服务器私钥、密码和数据库数据不进入这些目录。
## 5. Server B 从基线到 Web 入口
在新 Server B 重建时按以下顺序执行:
1. 完成 Ubuntu 24.04 LTS 补丁、chrony、nftables、fail2ban 和系统用户。
2. 建立 `/data``/opt/lisglosips/releases``/etc/lisglosips` 和备份目录。
3. 安装固定 MySQL/Redis,配置 bind mount、最小权限账号、AOF/RDB、binlog 和备份 timer。
4. 安装固定 Node.js LTS、Corepack/pnpm 和 Nginx。
5. 安装 `lisglosips@.service`,创建实例 env 文件。
6. 发布到新 release,校验后切换 `/opt/lisglosips/current`
7. 配置 Nginx 静态站点、API 代理、限流、安全头和内部录音 location。
8. 开发阶段签发私有 CA 证书;生产阶段安装正式域名证书。
9. 验证 443、应用本地端口、服务自启、重启恢复和非授权来源拒绝。
具体文件、命令和回滚方式见三个 Server B Runbook,不在本指南重复保存可能过时的命令副本。
## 6. 后端部署模型
S07 起后端使用 TypeScript、NestJS/Fastify、Prisma、ioredis 和 pnpm monorepo。建议发布产物:
```text
/opt/lisglosips/releases/<release-id>/
├── apps/api/dist/
├── apps/cdr-worker/dist/
├── apps/recording-worker/dist/
├── apps/config-publisher/dist/
├── public/
├── package.json
├── pnpm-lock.yaml
└── node_modules/
```
每个进程使用单独的 systemd 实例和环境文件:
```text
lisglosips@api.service
lisglosips@cdr-worker.service
lisglosips@recording-worker.service
lisglosips@config-publisher.service
```
构建应在 CI 或独立构建目录完成,不在生产 `current` 中执行。锁文件必须冻结,安装使用 `pnpm install --frozen-lockfile`。各实例只获得所需数据库、Redis、目录和网络权限。
## 7. 标准发布流程
1. 记录当前 release、数据库版本、Redis 配置版本和服务状态。
2. 备份 MySQL、Redis 与待修改配置,并校验备份哈希。
3. 将构建产物传到新的唯一 release 目录。
4. 核对产物哈希、Node/pnpm 版本和依赖锁文件。
5. 执行向前兼容数据库迁移;禁止在同一窗口直接删除旧列。
6. 切换 `current`,按依赖顺序重启 API/Worker。
7. 执行内部健康、HTTPS 健康、鉴权、数据库、Redis 与队列探针。
8. 发布 Redis 配置快照,确认 Server A 读取的新版本。
9. 使用 T 发起真实测试呼叫,检查 SIP、RTP、CDR、费用、录音和 HOMER。
10. 观察日志、队列积压、错误率和资源指标后再完成发布。
每次发布都要生成一条交接记录,包含 release ID、迁移版本、验证结果和回滚目标。
## 8. 标准回滚流程
- Web/API:将 `current` 指回上一 release 并重启服务。
- 数据库:优先发布修复迁移;不依赖不可逆 down migration。
- Redis 配置:切回上一 `cfg:active_version`,不删除 Stream。
- CDR Worker:停止消费但保留 Pending/Stream,修复后重放。
- OpenSIPS:恢复已校验配置,先语法检查再 reload。
- RTPEngine:恢复上一 systemd 参数并验证现有通话影响。
- 录音:停止源文件删除,保留 A/B 两端待人工核验。
如果变更涉及防火墙、SSH、数据库数据目录或证书,必须先确认独立控制台/恢复路径可用。
## 9. 后续实施清单
### S07-S08
- 创建后端 monorepo、健康检查、结构化日志、配置校验和测试框架。
- 建立 Prisma Schema、迁移、种子和隔离恢复测试。
### S09-S17
- 实现认证、RBAC、审计、客户/供应商、充值、网关、策略、费率和线路组。
- 金额统一 Decimal;充值和扣费必须事务、幂等且有不可变流水。
- Redis 配置发布使用 Outbox、版本号和回滚,不允许数据库与 Redis 双写漂移。
### S18-S26
- 在 A 部署 OpenSIPS、RTPEngine、录音 tmpfs、HEP 和 exporter。
- 在 T 建立 IP 认证和 SIP 注册两套测试场景。
- 完成 CDR Stream、计费、录音搬运、质检和 Dashboard 聚合。
### S27-S30
- React 从 Mock 切到生成的 API Client。
- 完成端到端、并发、故障、安全与恢复测试。
- 创建阿里云 VPC、安全组、数据盘、正式 DNS/TLS 和监控告警。
- 灰度低风险客户或线路,验证后再全量切换。
## 10. 每阶段完成条件
每个 Sxx 任务只有同时满足以下条件才可标记完成:
- 配置或代码已落地,并有仓库副本。
- 语法、单元/集成或服务健康检查通过。
- 网络暴露和最小权限得到验证。
- 发生重启的服务完成重启恢复验证。
- 备份可校验;涉及数据时完成恢复样例。
- 无明文凭据进入仓库或日志。
- `IMPLEMENTATION_STATUS.md` 已更新产物、验证、回滚、遗留问题和下一任务。
## 11. 阿里云迁移前确认
- 确认预计 CPS、并发、日话单、录音量和保留期。
- 为 A/B 配置同 VPC 低时延私网和独立数据盘。
- 安全组只开放 A 的 SIP/RTP、B 的 443 和受控 SSH 管理端口;SSH 密钥登录必须保留。
- 使用正式域名和可信 TLS 证书,配置续期与告警。
- 替换全部开发凭据和证书,不复制本地 CA 私钥。
- 重新执行防火墙、备份恢复、故障注入和端到端呼叫验收。
+110
View File
@@ -0,0 +1,110 @@
# Quality Backend Runbook
> 任务:S25 - 质检后端
> 完成时间:2026-06-21 21:05 +08:00
## 1. 目标
S25 完成质检后端最小闭环:
- 质检抽样规则管理。
- 基于规则的稳定抽样。
- 录音列表、录音详情和上下条导航。
- 保存质检结果。
- RBAC 权限和审计元数据。
本任务不做 Dashboard 聚合,不新增前端页面,不发布 B 当前 placeholder release。
## 2. API
质检规则:
```text
GET /api/v2/quality/rules
GET /api/v2/quality/rules/:id
POST /api/v2/quality/rules
PATCH /api/v2/quality/rules/:id
DELETE /api/v2/quality/rules/:id
POST /api/v2/quality/rules/:id/enable
POST /api/v2/quality/rules/:id/disable
```
录音质检:
```text
GET /api/v2/recordings
GET /api/v2/recordings/:id
PUT /api/v2/recordings/:id/review
```
S24 的播放接口继续保留:
```text
GET /api/v2/recordings/:id/play
```
## 3. 权限
| 能力 | 权限 |
| --- | --- |
| 查看质检规则、录音列表、录音详情 | `quality.view` |
| 新增、修改、启停、删除质检规则 | `quality.manage` |
| 保存质检结果 | `quality.manage` |
| 播放录音 | `recordings.play` |
质检规则变更和保存质检结果都带 `AuditAction` 元数据,由既有审计拦截器记录。
## 4. 稳定抽样
当前 schema 没有单独“待质检任务表”,S25 采用计算型稳定抽样:
```text
score = sha256(rule_id + ":" + recording_id) 的前 32 bit 映射到 [0, 100)
selected = score < ratio
```
规则匹配条件:
- `customerId` 为空或等于录音关联 raw CDR 的 `customerId`
- `lineGroupId` 为空或等于录音关联 raw CDR 的 `lineGroupId`
- 规则状态为 `ENABLED`
- 当前时间在 `effectiveAt``expiresAt` 范围内。
同一规则和同一录音的抽样结果稳定不变,列表重复查询不会抖动。
## 5. 数据写入
S25 不新增数据库迁移,复用 S08 已建表:
- `quality_sampling_rules`
- `quality_reviews`
- `recordings`
保存质检结果会新增一条 `quality_reviews` 记录,不覆盖历史结果;录音详情返回最新质检结果和历史列表。
## 6. 验证结果
- `corepack pnpm@10.33.0 exec vitest run apps/api/src/modules/quality/sampling.spec.ts apps/api/src/modules/recordings/recordings.service.spec.ts` 通过,2 个测试文件 6 条测试。
- `corepack pnpm@10.33.0 typecheck` 通过。
- `corepack pnpm@10.33.0 lint` 通过。
- `corepack pnpm@10.33.0 build` 通过。
## 7. 当前限制
- B 当前 `/opt/lisglosips/current` 仍是 S05 placeholder release,本次只完成本地代码与构建验证,未部署到 B 当前 API 服务。
- 录音真实数据仍依赖 S28 端到端路由、媒体、CDR 和录音闭环产生。
- Dashboard 中的待质检数量和趋势留给 S26 聚合任务。
## 8. 回滚
代码回滚:
- 删除或恢复 `apps/api/src/modules/quality/`
- 恢复 `apps/api/src/modules/recordings/` 到 S24 版本。
-`apps/api/src/modules/app.module.ts` 移除 `QualityModule`
- 重新执行 `corepack pnpm@10.33.0 build`
数据回滚:
- S25 未在远端执行数据库写入验证,无需清理远端数据。
- 若后续已运行 API 并产生质检记录,不应直接删除历史 `quality_reviews`;应通过审计可见的更正记录或受控数据库维护窗口处理。
+138
View File
@@ -0,0 +1,138 @@
# Recording Transfer and Playback Runbook
> 任务:S24 - 录音搬运与播放
> 完成时间:2026-06-21 20:20 +08:00
## 1. 目标
S24 完成录音闭环的最小能力:
- Server B 通过 Tailscale 私网从 Server A 拉取 `.ready` 录音文件。
- 拉取后校验文件大小和 SHA-256。
- 本地 `.part-*` 临时文件校验成功后原子改名。
- 校验和数据库标记成功后才允许删除 Server A 源 `.ready` 文件。
- API 提供受 RBAC 保护的播放入口,通过 Nginx `X-Accel-Redirect` 进入内部 `/_recordings/` location,支持 Range。
## 2. 代码产物
| 文件 | 说明 |
| --- | --- |
| `apps/worker-recording/src/transfer.ts` | 录音扫描、SSH 拉取、大小校验、SHA-256、原子落盘、源文件安全删除、`recordings` 入库 |
| `apps/worker-recording/src/main.ts` | Recording Worker 主循环 |
| `apps/worker-recording/src/transfer.spec.ts` | 搬运、哈希、删除保护和路径安全测试 |
| `apps/api/src/modules/recordings/*` | `GET /api/v2/recordings/:id/play` 播放入口 |
| `apps/api/src/modules/recordings/recordings.service.spec.ts` | Nginx 内部路径和路径穿越防护测试 |
| `infra/server-a/s19/scripts/lisglosips-recording-finalize` | A 端 finalizer 仓库副本,ready 目录改为 `0770`,便于专用拉取用户校验后删除源文件 |
## 3. Worker 环境变量
```text
DATABASE_URL
RECORDING_REMOTE_HOST=lisglosips-a
RECORDING_SSH_CONFIG=/etc/lisglosips/recording/ssh_config
RECORDING_REMOTE_READY_DIR=/dev/shm/voip_rec/ready
RECORDING_LOCAL_ROOT=/data/recordings
RECORDING_SCAN_INTERVAL_MS=10000
RECORDING_MAX_FILES_PER_SCAN=50
RECORDING_DELETE_SOURCE_AFTER_COPY=true
```
本地开发可使用 `.codex-private/ssh/config`;生产或 B 端 systemd 应使用 `/etc/lisglosips/recording/` 下的受控 key 和 known_hosts。
## 4. 服务器变更
### Server B
- 新增 `/etc/lisglosips/recording/a_pull_ed25519`,权限 `0640 root:lisglosips`
- 新增 `/etc/lisglosips/recording/known_hosts`
- 录音持久化目录仍为 `/data/recordings`,由 `lisglo-recorder:lisglosips` 管理。
- 备份目录:`/var/backups/lisglosips-s24/20260621T121100Z`
### Server A
- 新增专用用户 `lisglo-rec-pull`,加入 `rtpengine` 组。
- 安装 B 端录音拉取公钥到 `/var/lib/lisglo-rec-pull/.ssh/authorized_keys`
-`/dev/shm/voip_rec/ready` 调整为 `0770 rtpengine:rtpengine`
- 更新 `/usr/local/sbin/lisglosips-recording-finalize`,让 ready 根目录和 ready 子目录保持 `0770`
- 备份目录:`/var/backups/lisglosips-s24/20260621T121130Z`
未调整 SSH 管理端口,未禁止 SSH key 登录,未重启 OpenSIPS、RTPEngine、Nginx 或 API。
## 5. 播放路径
API
```text
GET /api/v2/recordings/:id/play
Permission: recordings.play
```
API 只查询状态为 `READY` 的录音,返回:
```text
X-Accel-Redirect: /_recordings/<storage-key>
Content-Type: audio/wav 或 audio/mpeg
Cache-Control: private, no-store
```
Nginx 已在 S05 配置:
```text
location /_recordings/ {
internal;
alias /data/recordings/;
add_header Accept-Ranges bytes always;
}
```
外部直接访问 `/_recordings/...` 返回 `404`,只能经 API 鉴权后内部转发。
## 6. 验证结果
本地:
- `corepack pnpm@10.33.0 exec vitest run apps/worker-recording/src/transfer.spec.ts apps/api/src/modules/recordings/recordings.service.spec.ts` 通过,5 条测试。
- `corepack pnpm@10.33.0 --filter @lisglosips/worker-recording build` 通过。
- `corepack pnpm@10.33.0 typecheck` 通过。
- `corepack pnpm@10.33.0 lint` 通过。
- `corepack pnpm@10.33.0 build` 通过。
服务器冒烟:
- A 生成测试文件 `/dev/shm/voip_rec/incoming/s24-smoke/s24-smoke.wav`
- A finalizer 生成 `/dev/shm/voip_rec/ready/s24-smoke/s24-smoke.wav.ready`
- B 使用 `lisglo-recorder` 和专用 SSH key 从 A 私网拉取。
- B 落盘 `/data/recordings/s24-smoke/s24-smoke.wav`,大小 `31`,属主 `lisglo-recorder:lisglosips`
- A/B SHA-256 一致:`c72dd3909a317ad55817435b7ed5a0db6947455ffc66e09a2dde787fe4a8c348`
- 校验成功后 A 源 `.ready` 文件已删除。
- 外部直接访问 `https://127.0.0.1/_recordings/s24-smoke/s24-smoke.wav` 返回 `404`,内部录音 location 未暴露。
## 7. 当前限制
B 当前 `/opt/lisglosips/current` 仍是 S05 placeholder release,未切换到完整 monorepo API/Worker release。因此 S24 本次完成代码、构建、服务器私网拉取链路和 Nginx 内部播放保护验证;正式 `lisglosips@recording-worker.service` 随完整应用 release 发布时启用。
## 8. 回滚
Server A
```bash
sudo cp -a /var/backups/lisglosips-s24/20260621T121130Z/lisglosips-recording-finalize /usr/local/sbin/lisglosips-recording-finalize
sudo chmod 0755 /usr/local/sbin/lisglosips-recording-finalize
sudo userdel -r lisglo-rec-pull
sudo chmod 0750 /dev/shm/voip_rec/ready
```
Server B
```bash
sudo rm -rf /etc/lisglosips/recording
sudo rm -rf /data/recordings/s24-smoke
```
代码回滚:
- 恢复 `apps/worker-recording/src/main.ts` 到骨架版本。
- 删除或恢复 `apps/worker-recording/src/transfer.ts``transfer.spec.ts`
- 删除 `apps/api/src/modules/recordings/` 并从 `AppModule` 移除 `RecordingsModule`
- 恢复 `apps/worker-recording/package.json``tsconfig.json``pnpm-lock.yaml`
- 重新执行 `corepack pnpm@10.33.0 build`
@@ -0,0 +1,58 @@
# S29 性能、故障与安全测试报告
## 测试范围
- 环境:本地 KVM 开发环境,A/B/T 通过 Tailscale 联调。
- 范围:小规模 CPS、并发、录音搬运、CDR/Recording Worker 恢复、Redis/MySQL 短故障、API 越权、重放与 SIP 非法来源/超 CPS 探针。
- 约束:不做长时间满载压测,不删除 S28 验收数据,不禁用 SSH 密钥登录,不提前执行 S30。
## 执行记录
### 1. 基线健康检查
- B`lisglosips@api``lisglosips@cdr-worker``lisglosips@recording-worker``mysql``redis-server``heplify-server``grafana-server``lisglosips-prometheus.service` 均 activeAPI `/api/v2/health/ready` 返回 `ok`Prometheus 实际项目服务 `lisglosips-prometheus.service` 返回 ready。
- A`opensips``rtpengine-daemon``rtpengine-recording-daemon``lisglosips-redis-auth-proxy``lisglosips-recording-finalize.timer` 均 active。
- T`lisglosips-s28-uas``opensips` 均 active。
### 2. CPS/并发/录音
- 5 路并发呼叫:T 同时发起 5 路 S28 呼叫,均收到 `100 Giving it a try``200 OK`、BYE `200 OK`,用时约 11.92 秒;B 端生成 5 条 raw CDR、5 条 rated CDR、5 条 READY 录音,录音大小均为 55758 bytes。
- 12 路短突发:T 同时发起 12 路短持话呼叫,均收到 `100 Giving it a try``200 OK`、BYE `200 OK`,用时约 2.13 秒;B 端核对 `raws=12``rated=12``recordingsReady=12``totalDuration=72`、录音大小 `55758-56078` bytes。
- 修复项:T 呼叫脚本原默认固定 `--local-port 50621`,并发时出现 `OSError: [Errno 98] Address already in use`;已改为默认 `--local-port 0` 并在 socket bind 后用实际端口生成 Via/Contact。复测 5 路和 12 路并发均通过。
### 3. Worker 与数据服务故障
- Recording Worker 故障:停止 B `lisglosips@recording-worker` 后发起呼叫,通话成功;恢复 worker 后自动搬运并入库录音,`recordings.status=READY`,大小 55758 bytes。
- CDR Worker 故障:停止 B `lisglosips@cdr-worker` 后发起呼叫,Redis consumer group 出现 `lag=1`MySQL 无 raw CDR;恢复 worker 后 `pending=0``lag=0`raw/rated CDR 正常落库。
- Redis 故障:停止 B `redis-server` 后,A 返回 `503 Redis Unavailable`,符合保护预期;恢复 Redis 后 A 热路径仍持续返回 503,重启 A `lisglosips-redis-auth-proxy` 无效,重启 A `opensips` 后恢复,复测呼叫成功。
- MySQL 故障:停止 B `mysql` 后发起呼叫,SIP 通话成功但 CDR 入库失败。修复后复测:MySQL 恢复并等待 pending idle 窗口后,CDR Worker 自动处理 pending Redis IDraw/rated CDR 正常入库,无新增 deadletter。
### 4. Web/API 安全
- 未认证访问 `/api/v2/users` 返回 `401 AUTH_REQUIRED`
- 质检角色测试用户 `usr_s29_quality_only` 访问 `/api/v2/users` 返回 `403 RBAC_FORBIDDEN`
- 未认证访问录音播放 `/api/v2/recordings/:id/play` 返回 `401 AUTH_REQUIRED`
- 伪造录音 ID `..etcpasswd` 通过认证访问播放接口返回 `404 RECORDING_NOT_READY`,未泄露文件内容。
- 充值重放:对 `cus_s28_t` 使用同一 `idempotencyKey=s29-replay-cus-s28-000001` 连续 POST 两次 `0.000001` 充值,两次返回同一充值 ID `rch_da0bba1c7b1d44428ab92d77a55e503d`;数据库核对 `customer_recharges.rows=1``idempotency_keys.rows=1`、余额只从 `99.748000` 增加到 `99.748001`
### 5. SIP 安全探针
- 从 B `100.90.90.91` 向 A `100.90.90.90:15060/udp` 发送非法来源 INVITECall-ID `s29-illegal-1782094573-3991@lisglosips-b`,客户端等待 3 秒无 SIP 响应;A OpenSIPS 最近日志未出现该 Call-ID。该结果说明非允许来源未进入当前业务路由;因无 OpenSIPS 层日志,保守记录为网络/主机边界静默丢弃证据。
## 缺陷与处理
| 编号 | 现象 | 处理 | 复测 |
| --- | --- | --- | --- |
| S29-D1 | T 呼叫脚本固定本地 UDP 端口,导致并发发起时端口冲突。 | 修改 `infra/server-t/s28/lisglosips-s28-sip.py`,默认 `--local-port 0`,bind 后写入实际端口到 Via/Contact,并部署到 T。 | 5 路并发和 12 路短突发均成功。 |
| S29-D2 | MySQL 短故障时 CDR Worker 将可恢复数据库连接错误 deadletter 并 ACK 原 Redis 消息,导致 CDR 不自动重试。 | `packages/redis` 增加 `CdrRetryableError`retryable 错误不 ACK、不 deadletter,并释放幂等锁;`worker-cdr` 将数据库连接类错误标记为 retryable。 | 停 MySQL 后发起呼叫,恢复 MySQL 并等待 pending idle 窗口后 raw/rated CDR 自动入库,无新增 deadletter。 |
| S29-D3 | 失败 CDR 的长 `event_id` 超过 `raw_cdrs.event_id varchar(64)`,触发 Prisma 长度错误并 deadletter。 | `worker-cdr` 入库前对超过 64 字符的 `event_id` 做稳定 SHA-256 后缀压缩,原始事件仍保留在 payload 中。 | 本地 Vitest 与 build 通过;B 部署后 MySQL 故障复测无新增长度类 deadletter。 |
| S29-D4 | Redis 恢复后 A 热路径仍返回 `503 Redis Unavailable`,仅重启 Redis proxy 无法恢复。 | 本次执行运行时恢复:重启 A `opensips`,恢复呼叫。根因需在后续 OpenSIPS Redis 连接池/故障重连策略中继续排查。 | 重启 A `opensips` 后 T->A 呼叫恢复成功。 |
| S29-D5 | S29 期间本地/T 到 B 的 Tailscale 管理链路出现短时高延迟/丢包,SSH 偶发超时。 | 未改网络配置;采用短命令、分步执行、服务健康复核。 | 后续 SSH 与 API 健康检查恢复;记录为开发环境网络风险。 |
## 结论
S29 在本地 KVM 开发环境完成。单 A/S28 最小闭环在 5 路并发和 12 路短突发下能完成 SIP、CDR、计费、录音入库;Recording Worker、CDR Worker、MySQL 短故障的可恢复路径已验证,其中 MySQL 可恢复缺陷已完成代码修复并部署到 B 当前 release。
API 鉴权、RBAC、录音播放未认证保护和充值幂等重放均通过。SIP 非法来源探针未进入业务路由,但缺少 OpenSIPS 层拒绝日志,建议后续补充明确的非法来源日志/指标。
遗留风险:Redis 恢复后 OpenSIPS 热路径仍需重启 `opensips` 才恢复,当前只有运行时恢复与复测记录,尚未完成根因级修复;多 A、完整生产路由、多 B 高可用不属于 S29 本次范围。
+86
View File
@@ -0,0 +1,86 @@
# S30 最终验收报告
## 1. 目标
完成 LisgloSIPS V2 本地 KVM 开发环境的备份恢复演练、上线/回滚脚本、灰度记录和最终验收,不执行真实阿里云生产切流。
## 2. 执行结果
| 项目 | 结果 |
| --- | --- |
| 当前 B release | `/opt/lisglosips/releases/s28-v2-20260621220924` |
| B preflight | 通过 |
| A OpenSIPS 配置语法 | 通过 |
| 备份恢复演练 | 通过 |
| 灰度呼叫 | 通过 |
| 回滚脚本语法检查 | 通过 |
新增产物:
- `docs/S30_RELEASE_BACKUP_RUNBOOK.md`
- `docs/S30_FINAL_ACCEPTANCE_REPORT.md`
- `infra/server-b/s30/lisglosips-release-preflight.sh`
- `infra/server-b/s30/lisglosips-release-rollback.sh`
- `infra/server-a/s30/lisglosips-opensips-config-restore.sh`
## 3. 备份恢复演练
执行时间:2026-06-22 10:28 +08:00。
备份:
- MySQL`/data/backups/mysql/20260622T022811Z`
- Redis`/data/backups/redis/20260622T022812Z`
校验:
- MySQL `all-databases.sql.gz``lisglosips.sql.gz``metadata.tsv``SHA256SUMS` 校验通过。
- Redis `dump.rdb``metadata.txt``SHA256SUMS` 校验通过。
恢复演练:
- MySQL 恢复到隔离临时库 `lisglosips_s30_restore`,核对计数:`raw_cdrs=33``recordings=27``customer_recharges=34``users=2`;演练后已删除临时库。
- Redis 从备份 RDB 启动隔离 Unix socket 临时实例,核对 `DBSIZE=51``stream:cdr_payload=54``cfg:active_version=s28-v1`;演练后已关闭临时实例并删除临时目录。
未覆盖运行中的 MySQL 或 Redis 数据目录。
## 4. 灰度记录
灰度对象:本地开发环境 T 测试端和 S28 专用测试客户/落地模拟链路。
灰度呼叫:
- Call-ID`s28-1782095548702-luppqjd4@lisglosips-t`
- SIPT 收到 `100 Giving it a try``200 OK`、BYE `200 OK`
- B raw CDR`raw_7a6356cc76974652aa30a9d7a350041b`
- 计费:`rated_e0eb8cbe1b90454cb3cf2d9a3bbc9290``billSec=6`,客户费用和供应商成本均为 `0.012000`
- 录音:`rec_5fdf599d679f493a83fc7a2d205a128a``READY`55758 bytes
结论:本地灰度链路在当前冻结 release 下通过。
## 5. 回滚验证
脚本语法检查:
- B`lisglosips-release-preflight.sh``lisglosips-release-rollback.sh` 通过 `bash -n`
- A`lisglosips-opensips-config-restore.sh` 通过 `bash -n`
运行验证:
- B `lisglosips-release-preflight.sh` 实际运行通过:current release、MySQL、Redis、Nginx、API、CDR Worker、Recording Worker、HEP、Prometheus、Grafana 均 activeAPI ready 正常,Nginx 配置测试成功,最近备份目录可见。
- A `/etc/opensips/opensips.cfg` 通过 `opensips -C` 语法检查。
未实际执行 release 回滚或 OpenSIPS 配置恢复,避免对当前已通过的 S28/S29/S30 环境造成不必要中断。
## 6. 遗留风险
- 本地开发环境不是阿里云生产环境。正式迁移前必须重新执行 VPC、安全组、数据盘、正式 DNS/TLS、备份恢复、故障注入和端到端呼叫验收。
- S29 遗留风险仍存在:B Redis 恢复后 A/OpenSIPS 热路径可能需要重启 `opensips` 才从 `Redis Unavailable` 恢复。S30 Runbook 已纳入处置步骤,但根因级修复仍需后续单独处理。
- 当前 A 是单 A/S28 最小真实路由,非完整多落地网关生产路由;真实 3A、多 B、高可用仍不属于本地 V2 闭环上线范围。
- 开发 CA、Tailscale IP、本地 KVM 根盘布局不能原样用于生产。
## 7. 结论
S30 在本地 KVM 开发环境完成。当前 V2 闭环具备可恢复备份、可执行上线/回滚 Runbook、回滚脚本产物、灰度呼叫记录和最终验收记录。
本地开发阶段可冻结在 `s28-v2-20260621220924` 作为迁移前基线。阿里云生产上线前不得跳过 S30 Runbook 中的重新备份恢复演练、正式网络/TLS 验收和灰度观察。
+142
View File
@@ -0,0 +1,142 @@
# S30 备份、Runbook、灰度与上线
## 1. 范围
当前 S30 面向本地 KVM 开发环境的 V2 闭环冻结与上线演练,不执行真实阿里云切流。阿里云迁移必须在新 VPC、正式 DNS/TLS、独立数据盘和安全组完成后,按本文重新演练。
## 2. 冻结清单
| 项 | 当前值 |
| --- | --- |
| B release | `/opt/lisglosips/releases/s28-v2-20260621220924` |
| B current | `/opt/lisglosips/current` 指向当前 release |
| A OpenSIPS | `/etc/opensips/opensips.cfg` |
| T 测试脚本 | `/opt/lisglosips-s28/lisglosips-s28-sip.py` |
| 数据库迁移 | 以 `prisma/migrations/` 和 B 当前库为准 |
| Redis 配置版本 | 以 `cfg:active_version` 为准 |
冻结后只允许以下变更进入上线窗口:
- 修复已确认阻塞缺陷;
- 更新正式域名/TLS/安全组等环境参数;
- 文档化灰度和回滚记录。
## 3. 上线前备份
在任何 release 切换、数据库迁移、OpenSIPS 配置替换、Nginx/TLS 调整前执行:
```bash
sudo systemctl start lisglosips-backup.service
sudo find /data/backups/mysql -mindepth 1 -maxdepth 1 -type d | sort | tail -1
sudo find /data/backups/redis -mindepth 1 -maxdepth 1 -type d | sort | tail -1
sudo sha256sum -c /data/backups/mysql/<stamp>/SHA256SUMS
sudo sha256sum -c /data/backups/redis/<stamp>/SHA256SUMS
```
另外备份:
```bash
sudo install -d -m 0700 /var/backups/lisglosips-s30/<stamp>
sudo cp -a /etc/lisglosips /etc/nginx /etc/systemd/system/lisglosips@.service /var/backups/lisglosips-s30/<stamp>/
sudo cp -a /etc/opensips/opensips.cfg /var/backups/lisglosips-s30/<stamp>/opensips.cfg
```
备份目录只允许 root 读取,不复制明文 secret 到仓库或聊天记录。
## 4. 恢复演练
MySQL 只恢复到隔离临时库:
```bash
sudo mysql -e 'DROP DATABASE IF EXISTS lisglosips_s30_restore; CREATE DATABASE lisglosips_s30_restore CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;'
gzip -dc /data/backups/mysql/<stamp>/lisglosips.sql.gz | sudo mysql lisglosips_s30_restore
sudo mysql -e 'SELECT COUNT(*) FROM lisglosips_s30_restore.raw_cdrs;'
sudo mysql -e 'DROP DATABASE lisglosips_s30_restore;'
```
Redis 只启动隔离临时实例:
```bash
sudo install -d -m 0700 /tmp/lisglosips-s30-redis-restore
sudo cp /data/backups/redis/<stamp>/dump.rdb /tmp/lisglosips-s30-redis-restore/dump.rdb
sudo redis-server --dir /tmp/lisglosips-s30-redis-restore --dbfilename dump.rdb --port 0 --unixsocket /tmp/lisglosips-s30-redis.sock --daemonize yes
sudo redis-cli -s /tmp/lisglosips-s30-redis.sock DBSIZE
sudo redis-cli -s /tmp/lisglosips-s30-redis.sock SHUTDOWN NOSAVE
```
禁止把备份直接覆盖运行中 `/var/lib/mysql``/var/lib/redis`
## 5. 发布顺序
1. 记录当前 release、数据库迁移版本、Redis `cfg:active_version`、A OpenSIPS 配置 SHA-256。
2. 执行 S30 备份与恢复演练。
3. 上传新 release 到 `/opt/lisglosips/releases/<release-id>`,校验 `package.json``pnpm-lock.yaml` 和构建产物。
4. 执行向前兼容数据库迁移,禁止同窗口不可逆删列。
5. 原子切换 `/opt/lisglosips/current`
6. 重启 `lisglosips@api``lisglosips@cdr-worker``lisglosips@recording-worker`
7. `nginx -t` 后 reload Nginx。
8. 执行 `infra/server-b/s30/lisglosips-release-preflight.sh`
9. 发布 Redis 配置快照并确认 A 使用新版本。
10. T 发起测试呼叫,按 Call-ID 核对 SIP、CDR、计费、录音、HOMER。
11. 开始灰度。
## 6. 灰度策略
本地环境使用 `cus_s28_t` 和 T 专用 UAS 作为灰度对象;生产环境选择一个低风险客户或一条低风险线路。
灰度观察项:
- SIP 2xx/4xx/5xx 分布;
- Redis Stream lag 和 pending
- CDR 入库与重复扣费;
- 余额变化和充值幂等;
- 录音 READY 比例、A tmpfs 使用率;
- API 5xx、P95/P99
- HOMER 是否可按 Call-ID 查询。
扩大条件:连续观察 24-72 小时无 P1/P2 缺陷、无 CDR/余额异常、录音搬运无持续积压。
## 7. 回滚
### B 应用回滚
```bash
sudo infra/server-b/s30/lisglosips-release-rollback.sh /opt/lisglosips/releases/<previous-release>
```
脚本会切换 `current`、重启 API/CDR/Recording Worker、校验 Nginx 并检查 API ready。
### Redis 配置回滚
切回上一个 `cfg:active_version`,不删除 Stream 或 deadletter。回滚后用 T 呼叫验证 A 已读取旧版本。
### OpenSIPS 回滚
```bash
sudo infra/server-a/s30/lisglosips-opensips-config-restore.sh /var/backups/<path>/opensips.cfg
```
脚本会先备份当前配置、执行 `opensips -C`,通过后替换并重启 OpenSIPS。重启会影响现有通话,生产必须放在维护窗口或确认可接受。
### 数据库回滚
优先发布向前修复迁移;只有在确认不可用且有完整备份时,才进入维护窗口做数据恢复。充值、CDR、审计、录音记录不直接物理删除。
### S29 Redis 恢复遗留风险
若 B Redis 故障恢复后 A 继续返回 `503 Redis Unavailable`
1. 确认 B `redis-server` activeA 到 B Redis 端口可达。
2. 重启 A `lisglosips-redis-auth-proxy`
3. 若仍失败,维护窗口内重启 A `opensips`
4. 记录 Call-ID、A 日志、Redis proxy 日志,作为后续根因修复输入。
## 8. 阿里云迁移前阻断项
- 正式 DNS/TLS 未完成;
- A/B 未位于同 VPC 低时延私网;
- B 未使用独立数据盘承载 MySQL、Redis、录音和监控;
- 安全组未限制 SSH 管理来源,或禁用了 SSH 密钥登录;
- 未完成新环境备份恢复演练;
- 未完成 T 或外部测试端到端呼叫;
- Redis 恢复后 OpenSIPS 自动恢复风险未纳入告警/Runbook。
+112
View File
@@ -0,0 +1,112 @@
# Server A OpenSIPS Baseline Runbook
> Task: S18 - Server A OpenSIPS baseline
> Environment: local KVM development servers over Tailscale
## Scope
S18 installs and pins OpenSIPS 3.6.x on Server A, binds SIP to the Tailscale address `100.90.90.90:15060/udp`, enables local-only MI HTTP, loads the Redis and HEP/tracer module baseline, and adds basic SIP anti-scan handling.
Full Redis routing, RTPEngine/media handling, CDR Stream emission, and production SIP routing are intentionally left for S19-S22.
## Installed Packages
Packages installed from `https://apt.opensips.org noble 3.6-releases`:
- `opensips` `3.6.7-1`
- `opensips-auth-modules` `3.6.7-1`
- `opensips-redis-module` `3.6.7-1`
- `opensips-http-modules` `3.6.7-1`
- `opensips-json-module` `3.6.7-1`
- `opensips-restclient-module` `3.6.7-1`
- `opensips-prometheus-module` `3.6.7-1`
- `opensips-tlsmgm-module` `3.6.7-1`
- `opensips-tls-module` `3.6.7-1`
- `opensips-tls-openssl-module` `3.6.7-1`
- `opensips-cli` `0.4.0~20260522~570a9a9-1`
All listed OpenSIPS packages are held with `apt-mark hold`.
## Server Files
- `/etc/opensips/opensips.cfg`
- `/etc/default/opensips`
- `/etc/apt/sources.list.d/opensips.list`
- `/etc/apt/sources.list.d/opensips-cli.list`
- `/etc/nftables.d/lisglosips-s18-opensips.nft`
Repository copies:
- `infra/server-a/s18/opensips/opensips.cfg`
- `infra/server-a/s18/opensips/opensips.default`
- `infra/server-a/s18/nftables/lisglosips-s18-opensips.nft`
## Runtime
- OpenSIPS listens on `100.90.90.90:15060/udp`.
- HEP transport placeholder listens on `127.0.0.1:9061/udp`.
- MI HTTP listens on `127.0.0.1:8888/tcp`.
- Node Exporter remains on `100.90.90.90:9100/tcp` from S06.
## SIP Behavior
- `OPTIONS` without a user part returns `200 Keepalive`.
- `REGISTER` returns `401 Authentication Required` with a Digest challenge.
- `INVITE` returns `503 Routing Not Ready` until S20/S21 routes are implemented.
- `pike` blocks excessive request density.
- `maxfwd` rejects loops with `483 Too Many Hops`.
## Firewall
The S18 nftables table only filters the new SIP and MI surfaces:
- allow `100.93.185.30 -> 100.90.90.90:15060/udp`;
- drop other UDP `15060` sources;
- drop non-local TCP access to `8888`.
The table uses `policy accept` and does not alter SSH. SSH key login remains enabled and unchanged.
## Validation
Commands used:
```bash
opensips -C -f /etc/opensips/opensips.cfg
systemctl is-active opensips
systemctl is-enabled opensips
ss -lntu | grep -E '15060|8888|9061|9100'
curl -X POST http://127.0.0.1:8888/mi -H 'Content-Type: application/json' --data-binary @mi-version.json
```
Observed results:
- OpenSIPS config check: OK.
- `opensips.service`: active and enabled.
- MI JSON-RPC `version`: returned `OpenSIPS (3.6.7 (x86_64/linux))`.
- From Server T, SIP `OPTIONS` returned `SIP/2.0 200 Keepalive`.
- From Server T, SIP `REGISTER` returned `SIP/2.0 401 Authentication Required` with `WWW-Authenticate`.
## Rollback
Backup directory:
```text
/var/backups/lisglosips-s18/20260621T051403Z
```
Basic rollback:
```bash
sudo systemctl stop opensips
sudo cp -a /var/backups/lisglosips-s18/20260621T051403Z/opensips-etc-before-deploy/. /etc/opensips/
sudo cp -a /var/backups/lisglosips-s18/20260621T051403Z/opensips-default-before-deploy /etc/default/opensips
sudo nft delete table inet lisglosips_s18_opensips
sudo systemctl daemon-reload
```
If removing packages is required, do it in a maintenance window after confirming no S19/S20 work depends on them:
```bash
sudo apt-mark unhold opensips opensips-auth-modules opensips-redis-module opensips-http-modules opensips-json-module opensips-restclient-module opensips-prometheus-module opensips-tlsmgm-module opensips-tls-module opensips-tls-openssl-module opensips-cli
sudo apt-get remove --purge opensips opensips-auth-modules opensips-redis-module opensips-http-modules opensips-json-module opensips-restclient-module opensips-prometheus-module opensips-tlsmgm-module opensips-tls-module opensips-tls-openssl-module opensips-cli
```
+143
View File
@@ -0,0 +1,143 @@
# Server A Redis 热路径、HEP 与指标 Runbook
适用范围:S20 在 Server A 上部署的 OpenSIPS Redis 热路径、HEP 发送、自定义指标和本地 Redis AUTH 代理。
## 部署结果
- OpenSIPS 继续监听 `100.90.90.90:15060/udp`HEP 本地发送 socket 为 `100.90.90.90:9061/udp`
- 本地 Redis AUTH 代理监听 `127.0.0.1:6380/tcp`,上游连接 Server B Redis `100.90.90.91:6379`,凭据通过 systemd encrypted credential 加载,不写入明文配置。
- OpenSIPS Redis URL 由预处理器注入为本地代理地址。
- Lua 热路径脚本读取版本化配置:
- `cfg:active_version`
- `cfg:v:{version}:auth:ip:{source_ip}`
- `cfg:v:{version}:customer_gateway:{id}`
- `cfg:v:{version}:customer:{id}`
- `cfg:v:{version}:customer_gateway:{id}:policies`
- INVITE 会按 Lua 返回结果降级:
- Redis 不可用:`503 Redis Unavailable`
- 配置缺失:`503 Config Missing`
- 策略缺失:`503 No Route Policy`
- 策略允许但 S21/S28 路由未完成:`503 Routing Not Ready`
- CDR 冒烟事件写入 `stream:cdr_payload`S20 验证事件已清理。
- Prometheus 指标:
- OpenSIPS MI/Prometheus`http://127.0.0.1:8888/metrics`
- Node Exporter textfile`/var/lib/prometheus/node-exporter/lisglosips_s20.prom`
- Server B 抓取 A`http://100.90.90.90:9100/metrics`
## 关键文件
- `/etc/opensips/opensips.cfg`
- `/etc/opensips/lisglosips_hotpath.lua`
- `/usr/local/sbin/lisglosips-redis-auth-proxy`
- `/usr/local/sbin/lisglosips-redis-load-hotpath`
- `/usr/local/sbin/lisglosips-opensips-preprocess`
- `/usr/local/sbin/lisglosips-s20-metrics`
- `/etc/systemd/system/lisglosips-redis-auth-proxy.service`
- `/etc/systemd/system/lisglosips-redis-hotpath-load.service`
- `/etc/systemd/system/opensips.service.d/20-lisglosips-s20.conf`
- `/etc/systemd/system/lisglosips-s20-metrics.service`
- `/etc/systemd/system/lisglosips-s20-metrics.timer`
- `/etc/default/lisglosips-node-exporter`
- `/etc/nftables.d/lisglosips-s20-observability.nft`
- `/etc/systemd/system/lisglosips-a-firewall.service`
仓库副本位于 `infra/server-a/s20/`
## 服务检查
```bash
sudo systemctl is-system-running
sudo systemctl is-active opensips lisglosips-redis-auth-proxy lisglosips-s20-metrics.timer lisglosips-node-exporter lisglosips-a-firewall
sudo systemctl is-enabled opensips lisglosips-redis-auth-proxy lisglosips-redis-hotpath-load lisglosips-s20-metrics.timer lisglosips-node-exporter lisglosips-a-firewall
sudo ss -lntup | grep -E ':15060|:9061|:8888|:6380|:9100'
```
`lisglosips-redis-hotpath-load.service` 是 oneshot,执行成功后显示 inactive 属于正常状态;检查 `systemctl show -p Result lisglosips-redis-hotpath-load.service` 应为 `success`
## 指标检查
```bash
curl -fsS http://127.0.0.1:8888/metrics | grep 'lisglosips_opensips_s20_'
cat /var/lib/prometheus/node-exporter/lisglosips_s20.prom
```
Server B 可抓取:
```bash
curl -fsS http://100.90.90.90:9100/metrics | grep -E 'lisglosips_(voip_rec|recording|redis_proxy|rtpengine)'
```
当前 S20 自定义指标包括:
- `lisglosips_opensips_s20_invite_total`
- `lisglosips_opensips_s20_hotpath_allow_total`
- `lisglosips_opensips_s20_hotpath_reject_total`
- `lisglosips_opensips_s20_redis_error_total`
- `lisglosips_opensips_s20_cdr_xadd_total`
- `lisglosips_opensips_s20_cdr_xadd_error_total`
- `lisglosips_redis_proxy_up`
- `lisglosips_voip_rec_tmpfs_bytes`
- `lisglosips_recording_files`
- `lisglosips_recording_oldest_ready_age_seconds`
- `lisglosips_rtpengine_sessions`
## HEP 检查
A 侧确认无发送错误:
```bash
sudo journalctl -u opensips --since '10 minutes ago' --no-pager | grep -E 'Cannot send HEP|Failed to duplicate|S20'
```
B 侧确认 heplify 收包统计:
```bash
journalctl -u heplify-server --since '10 minutes ago' --no-pager | tail -n 80
```
S20 验证中曾因 HEP socket 绑定 `127.0.0.1` 触发发送错误,OpenSIPS 仍按预期返回 SIP 响应;配置修正为 `100.90.90.90:9061` 后,A 侧抓包看到 HEP 发往 B `100.90.90.91:9060`B heplify 统计显示 HEP 收包且 Error 为 0。
## 降级验证记录
- Redis 配置缺失:T 发 INVITE 后 A 返回 `SIP/2.0 503 Config Missing`OpenSIPS 记录 `S20 hotpath reject reason=CONFIG_MISSING`CDR XADD 成功。
- Redis 代理不可用:短暂停止 `lisglosips-redis-auth-proxy` 后,T 发 INVITE 返回 `SIP/2.0 503 Redis Unavailable`OpenSIPS 记录 `S20 Redis hotpath unavailable``S20 CDR XADD failed`;随后启动代理并重新执行 Lua loader 恢复。
- HEP 发送异常:绑定错误期间 OpenSIPS 记录 HEP 发送失败,但 SIP 降级响应和 CDR 热路径继续执行;绑定修正后无新增 HEP 发送错误。
## 回滚
S20 初始备份目录:
```text
/var/backups/lisglosips-s20/20260621T061900Z
```
后续验证阶段重启 OpenSIPS 前,也在该目录下的 `post-verify/` 保存了当时的 `/etc/opensips/opensips.cfg` 副本。
回滚步骤:
1. 停止 S20 新增服务:
```bash
sudo systemctl stop lisglosips-s20-metrics.timer lisglosips-s20-metrics.service
sudo systemctl stop lisglosips-redis-auth-proxy.service
```
2. 从备份恢复 `/etc/opensips/opensips.cfg`、OpenSIPS drop-in、Node Exporter 默认文件和 S20 systemd/nftables 文件。
3. 执行 OpenSIPS 语法检查:
```bash
sudo opensips -C -f /etc/opensips/opensips.cfg
```
4. 重载 systemd 并重启相关服务:
```bash
sudo systemctl daemon-reload
sudo systemctl restart opensips
sudo systemctl restart lisglosips-node-exporter
sudo systemctl restart lisglosips-a-firewall
```
5. 验证 `systemctl is-system-running`、SIP `OPTIONS`、Node Exporter 和 nftables 表。
回滚不删除 Redis Stream 历史数据;如需清理测试事件,只按明确 Call-ID 精确 `XDEL`。
@@ -0,0 +1,135 @@
# Server A RTPEngine 与录音 tmpfs Runbook
适用阶段:S19
服务器:Server A `100.90.90.90`
备份目录:`/var/backups/lisglosips-s19/20260621T054645Z`
## 部署内容
- 安装并固定 Ubuntu 仓库 RTPEngine 包 `11.5.1.18-1ubuntu1.2`
- `rtpengine`
- `rtpengine-daemon`
- `rtpengine-recording-daemon`
- `rtpengine-iptables`
- `rtpengine-kernel-dkms`
- `rtpengine-utils`
- 安装匹配当前内核的 headers,DKMS 已为 `6.17.0-22-generic``6.17.0-35-generic` 构建 `rtpengine/11.5.1.18`
- 加载内核模块 `xt_RTPENGINE`RTPEngine 使用 kernel forwarding。
- 创建 `/dev/shm/voip_rec` 3 GiB tmpfs,子目录:
- `spool`
- `incoming`
- `ready`
- `failed`
- `tmp`
- 部署录音 finalizer,将 `incoming` 下完成文件移动为 `ready/<path>.part`,再原子重命名为 `ready/<path>.ready`
- 新增 `lisglosips-a-firewall.service`,重启后自动加载 S18/S19 nftables 规则。
- 新增 OpenSIPS drop-in,等待 Tailscale 地址 `100.90.90.90` 后再启动,避免开机绑定失败。
- 覆盖 `rtpengine-recording-daemon.service`,移除包默认 NFS/rpcbind 依赖,改为依赖本机 tmpfs 和 `rtpengine-daemon`
- 停止并 mask `rpcbind.service``rpcbind.socket`。S19 录音缓冲不使用 NFS。
## 关键文件
服务器文件:
- `/etc/rtpengine/rtpengine.conf`
- `/etc/rtpengine/rtpengine-recording.conf`
- `/etc/default/rtpengine-daemon`
- `/etc/default/rtpengine-recording-daemon`
- `/etc/nftables.d/lisglosips-s19-rtpengine.nft`
- `/etc/systemd/system/lisglosips-a-firewall.service`
- `/etc/systemd/system/opensips.service.d/10-lisglosips-wait-tailscale.conf`
- `/etc/systemd/system/rtpengine-recording-daemon.service`
- `/etc/systemd/system/lisglosips-recording-finalize.service`
- `/etc/systemd/system/lisglosips-recording-finalize.timer`
- `/etc/tmpfiles.d/lisglosips-voip-rec.conf`
- `/usr/local/sbin/lisglosips-recording-finalize`
- `/etc/fstab`
仓库副本:
- `infra/server-a/s19/rtpengine/`
- `infra/server-a/s19/nftables/`
- `infra/server-a/s19/systemd/`
- `infra/server-a/s19/tmpfiles/`
- `infra/server-a/s19/scripts/`
## 运行参数
RTPEngine
- 媒体地址:`100.90.90.90`
- RTP 端口范围:`30000-40000/udp`
- NG 控制口:`127.0.0.1:2223/udp`
- CLI`127.0.0.1:2224/tcp`
- HTTP/metrics`127.0.0.1:2225/tcp`
- 录音 spool`/dev/shm/voip_rec/spool`
防火墙:
- 允许 T `100.93.185.30` 到 A `30000-40000/udp`
- 丢弃其他来源到 A `30000-40000/udp`
- 阻断非本地访问 `2223/udp``2224/tcp``2225/tcp`
## 验收记录
- 重启后 `systemctl is-system-running``running`,无 failed unit。
- `opensips``rtpengine-daemon``rtpengine-recording-daemon``lisglosips-recording-finalize.timer``lisglosips-a-firewall.service``lisglosips-node-exporter.service` 均 active/enabled。
- `/dev/shm/voip_rec` 为 3.0 GiB tmpfs,目录属主 `rtpengine:rtpengine`,权限 `0750`
- `xt_RTPENGINE` 已加载;DKMS 状态为 installed。
- `rtpengine-ctl -ip 127.0.0.1 -port 2224 list interfaces` 显示 `100.90.90.90`,端口段 `30000 - 40000`
- nftables 表 `lisglosips_s19_rtpengine` 重启后存在。
- 从 T 向 A `30000/udp``40000/udp` 发送探针,A 在 `tailscale0` 抓包可见。
- 从 T 发 SIP `OPTIONS` 到 A `15060/udp` 返回 `SIP/2.0 200 Keepalive`
- 从 T 发 SIP `REGISTER` 到 A `15060/udp` 返回 `SIP/2.0 401 Authentication Required``WWW-Authenticate`
- finalizer 冒烟:测试 `.wav``incoming` 移动到 `ready/...wav.ready`,权限 `0640`,无 `.part` 残留,测试文件已删除。
说明:S19 尚未把 OpenSIPS 呼叫路由接入 RTPEngine,真实双向通话、NAT 媒体锚定和完整录音由 S20/S21/S28 联调完成。本阶段已验证 RTPEngine 服务、内核模块、RTP 端口范围、tmpfs 和录音 ready 生命周期。
## 常用检查
```bash
systemctl is-system-running
systemctl is-active opensips rtpengine-daemon rtpengine-recording-daemon lisglosips-recording-finalize.timer
findmnt /dev/shm/voip_rec
df -h /dev/shm/voip_rec
lsmod | grep RTPENGINE
dkms status | grep rtpengine
rtpengine-ctl -ip 127.0.0.1 -port 2224 list numsessions
rtpengine-ctl -ip 127.0.0.1 -port 2224 list interfaces
nft list table inet lisglosips_s19_rtpengine
iptables -t mangle -L -n -v
```
## 回滚
1. 停止 RTPEngine 与录音服务:
```bash
sudo systemctl stop rtpengine-recording-daemon lisglosips-recording-finalize.timer rtpengine-daemon
```
2. 从备份目录恢复被覆盖的配置:
```bash
sudo rsync -a /var/backups/lisglosips-s19/20260621T054645Z/etc/ /
```
3. 移除 S19 新增防火墙表和服务:
```bash
sudo nft delete table inet lisglosips_s19_rtpengine || true
sudo systemctl disable --now lisglosips-a-firewall.service || true
sudo rm -f /etc/systemd/system/lisglosips-a-firewall.service
sudo systemctl daemon-reload
```
4. 如需卸载 RTPEngine 包,先解除 hold,再卸载:
```bash
sudo apt-mark unhold rtpengine rtpengine-daemon rtpengine-recording-daemon rtpengine-iptables rtpengine-kernel-dkms rtpengine-utils
sudo apt-get remove --purge rtpengine rtpengine-daemon rtpengine-recording-daemon rtpengine-iptables rtpengine-kernel-dkms rtpengine-utils
```
5. 如需移除 tmpfs,先确认无待搬运录音,再卸载并删除 `/etc/fstab` 中对应行。
回滚不涉及 SSH 管理策略,不禁用 SSH 密钥登录。
+162
View File
@@ -0,0 +1,162 @@
# Server B 基础系统 Runbook
> 对应任务:S03
> 完成日期:2026-06-20
> 环境:本地开发 Server BTailscale IP 100.90.90.91
## 1. 当前基线
| 项目 | 状态 |
| --- | --- |
| OS | Ubuntu 24.04.4 LTS |
| 内核 | 6.17.0-35-generic |
| 时间同步 | chrony 4.5,阿里云 NTP 优先、腾讯云 NTP 备用 |
| 防火墙 | nftables 1.0.9,默认拒绝入站 |
| SSH 防护 | fail2ban 1.0.2sshd jail |
| 系统指标 | sysstat enabled |
| 数据盘 | 无独立盘;开发阶段 `/data` 位于根分区 |
| NFS | `/home/hector/share` 经 Tailscale automount |
完整变更前备份位于 B
`/var/backups/lisglosips-s03/20260620184404`
目录权限为 0700,包含变更前 tar、包清单、网络/防火墙快照、变更后配置和 SHA-256 清单。
## 2. 系统用户
| 用户 | 主组 | 用途 | 登录 |
| --- | --- | --- | --- |
| lisglosips | lisglosips | API 与通用 Worker | nologin |
| lisglo-recorder | lisglosips | 录音搬运 | nologin |
| lisglo-monitor | lisglosips | 自定义监控采集 | nologin |
不要依赖当前数字 UID/GID;部署脚本应按名称解析。
## 3. 目录权限
| 目录 | 所有者 | 模式/说明 |
| --- | --- | --- |
| `/data/mysql` | root:root | 0750S04 安装后交给 mysql |
| `/data/redis` | root:root | 0750S04 安装后交给 redis |
| `/data/recordings` | lisglo-recorder:lisglosips | 2750 |
| `/data/prometheus` | root:root | 0750S06 后调整 |
| `/data/grafana` | root:root | 0750S06 后调整 |
| `/data/homer` | root:root | 0750S06 后调整 |
| `/data/backups` | root:root | 0700 |
| `/opt/lisglosips` | root:lisglosips | 2750 |
| `/etc/lisglosips` | root:lisglosips | 0750 |
| `/var/log/lisglosips` | lisglosips:lisglosips | 2750 |
`/run/lisglosips``/run/lisglo-recorder``/run/lisglo-monitor``/etc/tmpfiles.d/lisglosips.conf` 在启动时重建。
## 4. NFS 自动挂载
`/etc/fstab`
```fstab
100.120.96.71:/mnt/share /home/hector/share nfs _netdev,nofail,noatime,x-systemd.automount,x-systemd.requires=tailscaled.service,x-systemd.after=tailscaled.service,x-systemd.mount-timeout=30s 0 0
```
验证:
```bash
systemctl status home-hector-share.automount
ls /home/hector/share
findmnt /home/hector/share
```
业务数据库和录音暂不使用该共享。迁移阿里云时重新设计独立数据盘,不复制本地 fstab。
## 5. 时间同步
配置文件:
- `/etc/chrony/sources.d/lisglosips.sources`
- Ubuntu 默认 pool 与 DHCP 动态 NTP 源已在 `chrony.conf` 中禁用。
当前网络会把部分公网 NTP 请求透明转发到不同响应地址,严格 conntrack 会拒绝该回复。因此选用能原地址响应的两个时间源,没有放宽 UDP 123 入站。
```bash
chronyc tracking
chronyc sources -v
timedatectl
```
验收标准:Stratum 非 0、Leap status 为 Normal、`System clock synchronized: yes`
## 6. 防火墙
配置文件:
- `/etc/nftables.conf`
- `/etc/nftables.d/lisglosips.nft`
- 仓库副本:`infra/server-b/s03/`
允许项:
| 来源 | 目标端口 | 用途 |
| --- | --- | --- |
| 100.91.249.119、100.98.167.119 | TCP `<SSH_ADMIN_PORT>`、443 | 两台现有管理设备;SSH 保留 Key 登录 |
| 100.90.90.90 | TCP 6379 | A 到 Redis |
| 100.90.90.90 | UDP 9060 | A 到 HEP |
| 任意来源 | UDP 41641 | Tailscale 外层直连 |
| 任意来源 | ICMP/ICMPv6 | 诊断与 IPv6 必需控制报文 |
除此之外的入站流量默认丢弃。MySQL 3306、Prometheus 9090、Grafana 3000 和应用内部端口不得开放。
修改规则时:
```bash
sudo nft -c -f /etc/nftables.conf
sudo systemctl restart nftables
sudo nft list table inet lisglosips_filter
```
修改 SSH 端口或防火墙时,必须保持一个已验证 SSH 会话,并在另一个新会话中用 Key 登录验证成功后才能关闭旧会话。
## 7. Fail2ban
`/etc/fail2ban/jail.d/lisglosips.local` 启用 sshd jail
- 10 分钟内失败 5 次;
- 封禁 1 小时;
- 使用 nftables multiport action
- 当前开发机 100.91.249.119 在 ignoreip 中。
```bash
sudo fail2ban-client -t
sudo fail2ban-client status sshd
```
## 8. 回滚
仅回滚防火墙:
```bash
sudo /var/backups/lisglosips-s03/20260620184404/rollback-firewall.sh
```
恢复 fstab
```bash
sudo cp -a /var/backups/lisglosips-s03/20260620184404/fstab.before-nfs-fix /etc/fstab
sudo systemctl daemon-reload
```
恢复 chrony 配置:
```bash
sudo cp -a /var/backups/lisglosips-s03/20260620184404/chrony.conf.before-lisglosips /etc/chrony/chrony.conf
sudo rm -f /etc/chrony/sources.d/lisglosips.sources
sudo systemctl restart chrony
```
系统包升级不建议逐包降级。若升级导致系统级故障,应从变更前虚拟机快照恢复;远端备份主要用于配置审计和局部恢复。
## 9. 已知事项
- SSH 由 `ssh.socket` 启动:socket enabled、service active`ssh.service` 显示 disabled 属正常状态。
- `findmnt --verify` 对 swapfile 给出常规文件警告,不是解析错误。
- 桌面、CUPS、Avahi、rpcbind 等服务仍存在,但已被主机防火墙隔离;精简桌面不属于 S03。
- 本地 `/data` 只是开发布局。迁移阿里云前必须创建独立数据盘并重新验收容量、IOPS、挂载和备份。
+234
View File
@@ -0,0 +1,234 @@
# Server B MySQL 与 Redis Runbook
> 对应任务:S04
> 完成日期:2026-06-20
> 环境:本地开发 Server B
## 1. 版本与状态
| 服务 | 固定版本 | 监听 |
| --- | --- | --- |
| MySQL | 8.0.46-0ubuntu0.24.04.2 | 127.0.0.1:3306、127.0.0.1:33060 |
| Redis | 7.0.15-1ubuntu0.24.04.4 | 127.0.0.1:6379、100.90.90.91:6379 |
固定包:
```bash
apt-mark showhold | grep -E 'mysql|redis'
```
更新前必须先阅读安全公告、备份并执行恢复演练,然后显式 `apt-mark unhold`。不要长期忽略安全更新。
版本清单位于:
- B`/etc/lisglosips/versions.env`
- 仓库:`infra/server-b/s04/versions.env`
## 2. 数据目录
开发服务器没有独立数据盘。实际数据写入:
- `/data/mysql`
- `/data/redis`
为兼容 Ubuntu 包、AppArmor 和 systemd,使用 bind mount 保留标准服务路径:
```fstab
/data/mysql /var/lib/mysql none bind 0 0
/data/redis /var/lib/redis none bind 0 0
```
服务分别依赖 `var-lib-mysql.mount``var-lib-redis.mount`。迁移阿里云时必须改用独立数据盘,不能直接复制本地根盘布局。
## 3. MySQL 基线
配置:`/etc/mysql/mysql.conf.d/99-lisglosips.cnf`
- 仅绑定回环地址;
- 业务库:`lisglosips`utf8mb4
- binlog 已启用,格式 ROW,保留 7 天;
- `sync_binlog=1`
- `innodb_flush_log_at_trx_commit=1`
- 数据库默认时区 UTC
- 禁止 `local_infile` 和 DNS 主机名解析;
- 慢查询阈值 1 秒;
- InnoDB buffer pool 1 GiB,最大连接数 200。
账号:
| 账号 | 来源 | 权限 |
| --- | --- | --- |
| lisglosips_app | 127.0.0.1 | SELECT/INSERT/UPDATE/DELETE/EXECUTE |
| lisglosips_migrate | 127.0.0.1 | `lisglosips.*` 全权限,用于迁移 |
应用账号不能 DDL;S04 已用实际 ALTER 拒绝测试验证。
## 4. Redis 基线
配置:
- `/etc/redis/redis-lisglosips.conf`
- `/etc/redis/users.acl`
关键策略:
- protected mode 开启;
- AOF everysec 与 RDB 快照同时启用;
- 最大内存 1536 MiB
- `maxmemory-policy=noeviction`,业务热数据和 Stream 不静默淘汰;
- 默认用户关闭;
- `lisglosips` 允许业务命令但移除 dangerous 类命令;
- `lisglo-backup` 仅允许 PING、INFO、BGSAVE、LASTSAVE。
Redis 绑定 Tailscale 地址。`lisglosips-tailscale-ready.service` 会先等待 100.90.90.91/32 出现,避免 Redis 开机首次启动竞态。
## 5. 凭据
本地 DPAPI 加密文件:
`.codex-private/SERVICE_CREDENTIALS.clixml`
包含:
- MYSQL_APP
- MYSQL_MIGRATE
- REDIS_APP
- REDIS_BACKUP
B 机:
| 文件 | 权限 |
| --- | --- |
| `/etc/lisglosips/secrets/mysql-app.env` | root:lisglosips 0640 |
| `/etc/lisglosips/secrets/mysql-migrate.env` | root:root 0600 |
| `/etc/lisglosips/secrets/redis.env` | root:lisglosips 0640 |
| `/etc/lisglosips/secrets/redis-backup.env` | root:root 0600 |
| `/etc/redis/users.acl` | root:redis 0640 |
禁止在聊天、日志、状态文件、仓库或命令参数中输出密码。
## 6. 网络安全
- MySQL 不允许远程连接。
- Redis 仅允许 A 的 Tailscale IP 100.90.90.90 通过 B 的 nftables 访问。
- T 对 3306/6379 的连接已验证为超时拒绝。
- A 第一次访问 B 时可能需要先建立 Tailscale direct 路径:
```bash
tailscale ping --c 1 100.90.90.91
```
稳态 direct 后 Redis 认证连接正常。
## 7. 自动备份
脚本:
- `/usr/local/sbin/lisglosips-mysql-backup`
- `/usr/local/sbin/lisglosips-redis-backup`
systemd
- `lisglosips-backup.service`
- `lisglosips-backup.timer`
每天 03:15 执行,随机延迟最多 15 分钟,保留 14 天。
备份目录:
```text
/data/backups/mysql/YYYYMMDDTHHMMSSZ/
all-databases.sql.gz
lisglosips.sql.gz
metadata.tsv
SHA256SUMS
/data/backups/redis/YYYYMMDDTHHMMSSZ/
dump.rdb
redis-check-rdb.txt
metadata.txt
SHA256SUMS
```
首次有效备份:`20260620T120955Z`
检查:
```bash
sudo systemctl start lisglosips-backup.service
sudo systemctl show -p Result lisglosips-backup.service
sudo systemctl list-timers lisglosips-backup.timer
cd /data/backups/mysql/LATEST_DIRECTORY && sudo sha256sum -c SHA256SUMS
cd /data/backups/redis/LATEST_DIRECTORY && sudo sha256sum -c SHA256SUMS
```
MySQL 全库备份包含账号密码哈希,只允许 root 读取。Redis ACL 明文密码不进入自动备份,灾难恢复时从 DPAPI 凭据重新生成。
## 8. 恢复
### MySQL 业务库
先创建空的临时数据库进行验证:
```bash
sudo mysql -e 'CREATE DATABASE lisglosips_restore_test CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;'
gzip -dc /data/backups/mysql/TIMESTAMP/lisglosips.sql.gz | sudo mysql lisglosips_restore_test
sudo mysql -e 'SHOW TABLES FROM lisglosips_restore_test;'
```
确认数据后再安排正式恢复窗口。禁止直接将 `all-databases.sql.gz` 覆盖到运行中的生产实例。
### Redis RDB
将备份 `dump.rdb` 复制到独立临时目录,用 `port 0` 和独立 Unix socket 启动临时 Redis,验证关键 key 后关闭。不要替换运行中实例的 RDB/AOF。
S04 已完成:
- MySQL 备份后删除源表,恢复到临时库并验证 token;
- Redis 备份后删除源 key,恢复到独立实例并验证 token;
- 两类探针和临时实例均已清理。
## 9. 健康检查
```bash
systemctl is-active mysql redis-server lisglosips-backup.timer
mysqladmin --protocol=socket ping
sudo mysql -e 'SELECT @@version, @@log_bin, @@binlog_format, @@time_zone;'
sudo findmnt /var/lib/mysql
sudo findmnt /var/lib/redis
sudo systemctl show redis-server -p Result -p NRestarts
sudo journalctl -b -u mysql -u redis-server -p err
```
验收状态:
- 重启后 MySQL/Redis active
- Redis 首次启动 `NRestarts=0`
- 没有 failed systemd unit
- AOF 写入和 RDB BGSAVE 均正常;
- 重启持久化探针通过并已清理。
## 10. 回滚
S04 前备份:
`/var/backups/lisglosips-s04/20260620195847`
包含配置基线、迁移前 MySQL/Redis 数据目录压缩包、变更后配置、失败演练样本和 SHA-256 清单。
回滚数据目录必须在维护窗口执行:
1. 停止 `lisglosips-backup.timer`、MySQL 和 Redis。
2. 卸载 `/var/lib/mysql``/var/lib/redis`
3. 恢复 S04 前 fstab。
4. 将当前数据目录另行归档,不直接覆盖。
5. 解压 `mysql.pre-s04.tar.gz``redis.pre-s04.tar.gz``/var/lib`
6. 恢复配置后执行语法检查,再逐个启动服务。
不要在没有完整备份与控制台访问的情况下卸载、覆盖或降级数据库包。
## 11. 已知事项
- 早期一次备份使用绝对校验路径,已移至 S04 备份目录下的 `failed-backups` 留档,不参与恢复。
- 本地根盘适合开发,不满足生产数据库与录音故障隔离要求。
- 软件包处于 hold;上线前需建立定期安全更新和预生产验证流程。
+123
View File
@@ -0,0 +1,123 @@
# Server B HOMER 与基础监控 Runbook
本文记录 S06 在 Server B 建立的 HOMER、HEP 接收、Prometheus、Grafana 和 Exporter 基线,以及 Server A 的 Node Exporter 采集入口。当前环境是本地 KVM 开发环境,不是阿里云生产环境。
## 1. 固定版本
| 组件 | 版本/来源 | 管理方式 |
| --- | --- | --- |
| PostgreSQL | `16.14` | Ubuntu 官方包,用于 HOMER 数据库 |
| Heplify Server | `github.com/sipcapture/heplify-server@v1.60.2-0.20260512101233-c74dc3d216ac` | Go 构建,二进制 SHA-256 记录在 `infra/server-b/s06/versions.env` |
| Homer App | `github.com/sipcapture/homer-app@v0.0.0-20251021161517-9b1336352aa0` | Go 构建,监听 `127.0.0.1:9080` |
| Prometheus | `2.45.3` | Ubuntu security/updates 包,自定义 `lisglosips-prometheus.service` |
| Grafana | `13.0.2` | Tsinghua HTTPS 镜像包,包哈希来自官方 Packages 元数据 |
| Node Exporter | `1.7.0` | A/B 两机 Ubuntu 包,自定义 `lisglosips-node-exporter.service` |
| MySQL Exporter | `0.15.0` | Ubuntu 包,监听 `127.0.0.1:9104` |
| PostgreSQL Exporter | `0.15.0` | Ubuntu 包,监听 `127.0.0.1:9187` |
| Redis Exporter | `1.86.0` | Go 构建,监听 `127.0.0.1:9121` |
Docker 与 Compose 已安装但运行时禁用。最初计划使用 HOMER10 Docker 架构,但 Docker/GitHub 对象存储在当前代理链路下下载 blob 反复 EOFS06 改为原生 HOMER7/PostgreSQL/Heplify 部署。不要在后续会话里反复重试 Docker 镜像,除非网络条件已明确修复。
## 2. 目录与端口
| 项目 | 路径/端口 |
| --- | --- |
| HOMER PostgreSQL 数据 | `/data/homer/postgresql` |
| HOMER 配置 | `/etc/homer/webapp_config.json` |
| Heplify HEP UDP | `100.90.90.91:9060/udp` |
| Heplify Metrics | `127.0.0.1:9096` |
| Homer App | `127.0.0.1:9080`,经 `https://homer.lisglosips.local/` 暴露 |
| Prometheus | `127.0.0.1:9090` |
| Grafana | `127.0.0.1:3001`,经 `https://grafana.lisglosips.local/` 暴露 |
| B Node Exporter | `127.0.0.1:9100` |
| A Node Exporter | `100.90.90.90:9100`,仅允许 B 访问 |
开发证书已重新签发,SAN 包含 `grafana.lisglosips.local``homer.lisglosips.local`。本地 CA 公钥副本仍在 `.codex-private/tls/lisglosips-dev-ca.crt`
## 3. 关键服务
Server B
```text
postgresql.service
heplify-server.service
homer-app.service
lisglosips-prometheus.service
grafana-server.service
lisglosips-node-exporter.service
lisglosips-mysqld-exporter.service
lisglosips-postgres-exporter.service
lisglosips-redis-exporter.service
nginx.service
```
Server A
```text
lisglosips-exporter-firewall.service
lisglosips-node-exporter.service
```
A 机使用独立 `nftables``inet lisglosips_s06` 保护 9100,只允许 `100.90.90.91` 抓取。Ubuntu 包自带的 `prometheus-node-exporter.service` 已 mask,避免抢占端口。A/B 两机的 `openipmi.service` 已 mask,因为 KVM 虚拟机不支持该驱动。
## 4. 验收命令
Prometheus targets
```bash
curl -fsS http://127.0.0.1:9090/api/v1/targets
```
预期 7 个 target 全部 `up``prometheus``node/server-a``node/server-b``mysql``postgresql_homer``redis``heplify_server`
HEP 入库测试:从 A 发送 HEPv3 UDP 到 B 的 `9060`,然后在 B 查询 `homer_data.hep_proto_1_call`。S06 验收已确认测试 `INVITE` 入库,`method=INVITE``srcIp=100.90.90.90``dstIp=100.90.90.91`
HTTPS
```bash
curl --fail --cacert /etc/lisglosips/pki/ca/lisglosips-dev-ca.crt \
https://127.0.0.1/api/health
```
Windows 本机可使用:
```powershell
curl.exe --ssl-no-revoke --cacert .codex-private/tls/lisglosips-dev-ca.crt \
--resolve grafana.lisglosips.local:443:100.90.90.91 \
https://grafana.lisglosips.local/api/health
```
Grafana 已预置两个 datasource`Prometheus``HOMER PostgreSQL`;已预置 Dashboard`LisgloSIPS Infrastructure Overview`
## 5. 回滚
S06 变更前备份:
```text
B: /var/backups/lisglosips-s06/20260620T132440Z
A: /var/backups/lisglosips-s06/20260620225517
```
回滚原则:
1. 先停止 S06 新增服务,再恢复备份配置。
2. PostgreSQL 数据目录 `/data/homer/postgresql` 不直接删除;需要退回时先备份当前目录。
3. Nginx 只移除 `grafana.lisglosips.local``homer.lisglosips.local` 站点,不影响 S05 Web 入口。
4. A 机只删除 `inet lisglosips_s06` 表和两个 S06 服务,不碰 OpenSIPS/RTP 配置。
常用命令:
```bash
sudo systemctl stop heplify-server homer-app lisglosips-prometheus grafana-server
sudo systemctl stop lisglosips-mysqld-exporter lisglosips-postgres-exporter lisglosips-redis-exporter
sudo systemctl disable --now lisglosips-exporter-firewall lisglosips-node-exporter
sudo nft delete table inet lisglosips_s06
```
## 6. 已知事项
- 当前 HOMER Web 为最小状态页,HOMER API、HEP 接收和 PostgreSQL 入库已可用;完整 HOMER UI 静态包等网络条件稳定后再补。
- Grafana 插件在线检查已关闭,避免受限网络下反复报错。
- Docker 已安装但禁用,S06 不依赖 Docker 运行。
- A 机 `nftables` 表只保护 Node Exporter 9100,不替代后续 S18/S20 的 Server A 完整防火墙设计。
- 生产迁移时必须替换开发 CA、正式域名、正式安全组和云监控告警策略。
+173
View File
@@ -0,0 +1,173 @@
# Server B Web 运行时 Runbook
本文记录 S05 在 Server B 上建立的 Node.js、pnpm、Nginx、systemd 与开发 TLS 基线。当前环境是本地 KVM 开发环境,不是阿里云生产环境。
## 1. 固定版本
| 组件 | 版本 | 管理方式 |
| --- | --- | --- |
| Node.js | `v22.22.2` / Debian 包 `22.22.2-1nodesource1` | NodeSource 22.x`apt-mark hold` |
| Corepack | `0.34.6` | Node 全局工具 |
| pnpm | `10.33.0` | 全局固定版本;项目应再用 `packageManager` 固定 |
| Nginx | `1.24.0-2ubuntu7.12` | Ubuntu 24.04 security/updates`apt-mark hold` |
Node.js 22 当前属于 LTS 线。固定包在升级前必须先核对 Node/Nginx 安全公告、NestJS/Prisma 兼容性并完成回归测试。
版本记录:`/etc/lisglosips/web-runtime-versions.env`
## 2. 目录与发布模型
```text
/opt/lisglosips/
├── releases/
│ └── s05-placeholder-20260620/
│ ├── public/index.html
│ └── server.mjs
└── current -> /opt/lisglosips/releases/s05-placeholder-20260620
```
- 发布目录由 `root:lisglosips` 持有,程序只读。
- `current` 必须是指向完整 release 的符号链接。
- 正式发布先写入新 release,完成校验后再原子切换 `current`
- 不允许直接在 `current` 指向的 release 内在线修改代码。
- systemd 可写状态目录为 `/var/lib/lisglosips`,运行目录为 `/run/lisglosips`
## 3. systemd 服务模板
模板:`/etc/systemd/system/lisglosips@.service`
实例环境文件:`/etc/lisglosips/<实例名>.env`,权限必须为 `0640 root:lisglosips`。当前实例:
```text
lisglosips@api.service -> /etc/lisglosips/api.env
```
模板默认使用 `lisglosips` 系统用户,启用只读系统、私有临时目录、空 capability、设备隔离和内核保护。后续 Worker 如需写录音目录,应使用实例 drop-in 精确增加 `ReadWritePaths`,不要放宽整个模板。
常用命令:
```bash
sudo systemctl status lisglosips@api
sudo systemctl restart lisglosips@api
sudo journalctl -u lisglosips@api -n 100 --no-pager
sudo systemd-analyze security lisglosips@api.service
```
## 4. Nginx 路由
| 路径 | 行为 |
| --- | --- |
| `/` | `/opt/lisglosips/current/public` 静态站点,支持 SPA fallback |
| `/healthz` | 反向代理到 `127.0.0.1:3000` |
| `/api/` | API 代理,单 IP `20 r/s`、burst 40 |
| `/api/auth/login` | 登录专用限制,单 IP `5 r/min`、burst 3 |
| `/_recordings/` | 仅供 `X-Accel-Redirect` 内部访问,外部请求返回 404 |
关键文件:
```text
/etc/nginx/conf.d/lisglosips-global.conf
/etc/nginx/sites-available/lisglosips.conf
/etc/nginx/snippets/lisglosips-proxy.conf
/etc/nginx/snippets/lisglosips-security-headers.conf
/etc/nginx/snippets/lisglosips-tls.conf
```
应用仅监听 `127.0.0.1:3000`。Nginx 监听 80/443,但 nftables 仅允许已登记管理端访问 443;开发阶段没有对外开放 80。
配置变更:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
## 5. 开发 TLS
开发 CA 和证书位于:
```text
/etc/lisglosips/pki/ca/lisglosips-dev-ca.crt
/etc/lisglosips/pki/ca/lisglosips-dev-ca.key
/etc/lisglosips/pki/certs/server.crt
/etc/lisglosips/pki/private/server.key
```
CA 私钥为 `0600 root:root`,服务器私钥为 `0640 root:www-data`。证书 SAN 包含 `lisglosips.local``yanzi``100.90.90.91``127.0.0.1`
重新签发开发证书:
```bash
sudo /usr/local/sbin/lisglosips-issue-dev-cert
sudo nginx -t
sudo systemctl reload nginx
```
本机公有 CA 副本保存在 `.codex-private/tls/lisglosips-dev-ca.crt`,不得把 CA 私钥复制出 Server B。Windows 使用私有 CA 测试时,因开发 CA 不发布 CRL,curl 需附加 `--ssl-no-revoke`;这不会关闭证书链、主机名或签名校验。
迁移阿里云后必须替换为正式域名证书,并完成:
1. 把证书和私钥放入受控 Secret 路径。
2. 更新 `lisglosips-tls.conf`
3. 将 HSTS 从开发值 `300` 调整为生产值,建议先观察再提升至 `31536000`
4. 验证自动续期、续期失败告警和回滚证书。
5. 开放 80 仅用于 ACME/跳转,或使用 DNS-01 后继续关闭 80。
## 6. 健康检查
```bash
curl --fail http://127.0.0.1:3000/healthz
curl --fail --cacert /etc/lisglosips/pki/ca/lisglosips-dev-ca.crt \
https://127.0.0.1/healthz
sudo ss -lntp | grep -E ':(80|443|3000)\b'
sudo systemctl --failed
```
预期:
- `/healthz` 返回 `status=ok`
- 3000 仅出现在 `127.0.0.1`
- 443 由 Nginx 监听。
- 首页响应包含 HSTS、CSP、X-Frame-Options、X-Content-Type-Options 和 Permissions-Policy。
- TLS 1.2/1.3 可用,TLS 1.1 被拒绝。
## 7. 新版本发布
```bash
release="2026xxxxTxxxxxxZ"
sudo install -d -o root -g lisglosips -m 0750 "/opt/lisglosips/releases/$release"
# 将已经构建和校验的产物放入新 release。
sudo chown -R root:lisglosips "/opt/lisglosips/releases/$release"
sudo find "/opt/lisglosips/releases/$release" -type d -exec chmod 0750 {} +
sudo find "/opt/lisglosips/releases/$release" -type f -exec chmod 0640 {} +
sudo ln -sfn "/opt/lisglosips/releases/$release" /opt/lisglosips/current
sudo systemctl restart lisglosips@api
sudo nginx -t && sudo systemctl reload nginx
```
发布后必须检查内部健康、HTTPS 健康、日志、应用端口监听和前端静态文件。数据库迁移从 S08 起必须在切换 `current` 前按迁移 Runbook 执行。
## 8. 回滚
应用回滚:把 `current` 指回上一 release,重启应用并检查 HTTPS 健康。
```bash
sudo ln -sfn /opt/lisglosips/releases/<previous> /opt/lisglosips/current
sudo systemctl restart lisglosips@api
```
S05 变更前备份:
```text
/var/backups/lisglosips-s05/20260620T123432Z
```
其中 `pre-change.tar.gz` 包含变更前应用与配置,`SHA256SUMS` 用于恢复前校验。恢复 Nginx/systemd 配置后必须执行 `nginx -t``systemctl daemon-reload`,再重启对应服务。卸载 Nginx或解除包固定不是常规回滚动作,只有确认恢复目标需要时才执行。
## 9. 已知事项
- NodeSource 仓库在本地代理链路上偶尔出现 TLS 握手中断;现有索引和已安装包可用。升级前必须先确认仓库恢复并校验候选版本。
- 当前证书仅供开发,不能用于阿里云生产。
- 80 已监听但被 nftables 拒绝;生产是否开放取决于证书签发方案。
- 当前 API 是 S05 无依赖占位服务,S07 后端骨架完成后替换。
@@ -0,0 +1,81 @@
# Server T 客户模拟配置 Runbook
> 任务:S21 - Server T 客户模拟配置
> 完成时间:2026-06-21 15:28 +08:00
> 环境:本地 KVM Server T `100.93.185.30`
## 1. 变更范围
- 在 Server T 部署独立脚本目录 `/opt/lisglosips-s21`
- 新增受控配置文件 `/etc/lisglosips-s21/sip-accounts.env`,权限 `0640 root:hector`
- 新增 OpenSIPS 测试域 `s21.lisglosips.test`
- 新增两套 SIP 注册测试账号:
- `s21-reg-1001@s21.lisglosips.test`
- `s21-reg-1002@s21.lisglosips.test`
- 新增两套 IP 认证测试条目:
- `s21-ip-1001``100.93.185.30/32`
- `s21-ip-1002``100.93.185.30/32`
- 将 T 的 OpenSIPS `auth_db calculate_ha1` 调整为 `0`,账号只保存 HA1,不保存 SIP 明文密码。
- 将 T 的 OpenSIPS `mi_fifo fifo_mode` 调整为 `0660`
- 将 T 的 OpenSIPS TLS 私钥文件权限收敛为 `0600`
## 2. 脚本清单
脚本均位于 `/opt/lisglosips-s21`
| 脚本 | 用途 |
| --- | --- |
| `s21-register-primary` | 主 SIP 注册账号成功注册 |
| `s21-register-secondary` | 备用 SIP 注册账号成功注册 |
| `s21-register-wrong-ha1` | 错误鉴权失败场景 |
| `s21-call-a-ip` | 从 T 向 A `100.90.90.90:15060` 发起 IP 认证模拟 INVITE |
| `s21-call-a-sip` | 从 T 向 A 发起 SIP 注册账号模拟 INVITE |
| `s21-local-success` | T 本地 200 OK 呼叫场景 |
| `s21-local-busy` | T 本地 486 Busy Here 场景 |
| `s21-local-reject` | T 本地 603 Decline 场景 |
| `s21-local-timeout` | T 本地超时场景 |
| `s21-over-cps-a` | 从 T 向 A 突发 INVITE,模拟超 CPS/突发压测输入 |
所有脚本都会输出 `call_id=...` 和 SIP 状态,便于后续在 A、B、HOMER、Redis Stream 中按 Call-ID 串联排查。
## 3. 验证结果
- `opensips -C -f /etc/opensips/opensips.cfg` 通过。
- `opensips.service``active`
- `s21-register-primary`:先收到 `401 Unauthorized` challenge,最终 `200 OK`
- `s21-register-secondary`:先收到 `401 Unauthorized` challenge,最终 `200 OK`
- `s21-register-wrong-ha1`:最终保持 `401 Unauthorized`
- `location` 表已有 `s21-reg-1001``s21-reg-1002` 注册位置。
- `subscriber.password` 长度为 `0``subscriber.ha1` 长度为 `32`,未保存 SIP 明文密码。
- 本地场景:
- `s21-local-success` 返回 `SIP/2.0 200 OK`
- `s21-local-busy` 返回 `SIP/2.0 486 Busy Here`
- `s21-local-reject` 返回 `SIP/2.0 603 Decline`
- `s21-local-timeout` 返回 `NO RESPONSE`
- 到 A 的探针:
- `s21-call-a-ip` 返回 `SIP/2.0 503 Config Missing`
- `s21-call-a-sip` 返回 `SIP/2.0 503 Config Missing`
- `s21-over-cps-a 5 0.01` 5 个 Call-ID 均返回 `SIP/2.0 503 Config Missing`
说明:A 端当前处于 S20 热路径阶段,尚未发布真实客户/路由配置,因此到 A 的 INVITE 返回 `503 Config Missing` 是当前预期;S21 验收重点是 T 端脚本可重复执行并输出 Call-ID。
## 4. 回滚方式
回滚点:
```text
/var/backups/lisglosips-s21/20260621T072316Z
```
回滚步骤:
```bash
sudo systemctl stop opensips
sudo cp -a /var/backups/lisglosips-s21/20260621T072316Z/opensips /etc/opensips
sudo mysql --protocol=socket -uroot opensips < /var/backups/lisglosips-s21/20260621T072316Z/opensips.sql
sudo rm -rf /opt/lisglosips-s21 /etc/lisglosips-s21
sudo opensips -C -f /etc/opensips/opensips.cfg
sudo systemctl start opensips
```
回滚会删除 S21 的测试账号、注册位置、IP 认证测试数据和脚本。若后续任务已依赖这些脚本,应先暂停相关测试。
+81
View File
@@ -0,0 +1,81 @@
# LisgloSIPS SSH 访问与恢复 Runbook
> S00 完成日期:2026-06-20。本文不包含密码。
## 1. 当前访问方式
- 用户:`hector`
- SSH 管理端口:`<SSH_ADMIN_PORT>`,端口号由用户确认后实施;开发环境未迁移前可能仍为 `22`
- 项目 Key`.codex-private/ssh/lisglosips_codex_ed25519`
- Key 指纹:`SHA256:avGNUh+0t4tM9Odcaz+NHERAfESQl+ybzPi5oM39oYI`
- SSH 配置:`.codex-private/ssh/config`
- known_hosts`.codex-private/ssh/known_hosts`
- sudo 密码:`.codex-private/SERVER_CREDENTIALS.clixml`,由 Windows DPAPI 加密。
```powershell
ssh -F .codex-private\ssh\config lisglosips-a
ssh -F .codex-private\ssh\config lisglosips-b
ssh -F .codex-private\ssh\config lisglosips-t
```
## 2. 主机指纹
| 主机 | 地址 | ED25519 指纹 |
| --- | --- | --- |
| A / TonyHouse | `100.90.90.90` | `SHA256:BmJVzUlHD2LPj/SlCnC611DnsHgedC79/eJ7pAMmbtM` |
| B / YanZi | `100.90.90.91` | `SHA256:Kh1EmcUfG4S+TPDtZ8p+D3A4pGlFc4B8lDMcPeLbxoI` |
| T / SteveTable | `100.93.185.30` | `SHA256:hlq2q/HIvyZzo2PxcDzzN5vMrfzQAzBW5W9qaeAEkGU` |
三台机器原本因镜像克隆共享同一套 SSH host key,S00 已分别轮换。任何后续指纹变化都必须停止连接并通过云控制台或已认证会话核验。
## 3. SSH 加固状态
当前原则:只调整 SSH 管理端口和允许来源,不禁止 SSH 密钥登录。`PubkeyAuthentication` 必须保持开启;密码登录策略按开发/生产环境另行确认,不再作为 S00 强制禁用项写入需求。
`/etc/ssh/sshd_config.d/00-lisglosips-hardening.conf`
```text
Port <SSH_ADMIN_PORT>
PubkeyAuthentication yes
MaxAuthTries 4
LoginGraceTime 30
```
三台服务器均已通过:
- `sshd -t`
- Key 新会话登录,且不因端口变更被禁用
- SSH 管理端口仅允许受控管理来源访问
- `hector` 新密码 sudo 验证
## 4. 远端备份
| 主机 | host key 轮换前 | SSH 加固前 |
| --- | --- | --- |
| A | `/var/backups/lisglosips-s00/etc-ssh-before-hostkey-rotation-20260620150150.tar.gz` | `/var/backups/lisglosips-s00/etc-ssh-before-hardening-20260620150326.tar.gz` |
| B | `/var/backups/lisglosips-s00/etc-ssh-before-hostkey-rotation-20260620150152.tar.gz` | `/var/backups/lisglosips-s00/etc-ssh-before-hardening-20260620150328.tar.gz` |
| T | `/var/backups/lisglosips-s00/etc-ssh-before-hostkey-rotation-20260620150153.tar.gz` | `/var/backups/lisglosips-s00/etc-ssh-before-hardening-20260620150329.tar.gz` |
## 5. 配置回滚
必须在一个已验证的 Key 会话中执行,并保持第二个会话不关闭:
```bash
sudo mkdir -p /var/backups/lisglosips-s00/current-ssh
sudo cp -a /etc/ssh/. /var/backups/lisglosips-s00/current-ssh/
sudo tar -xzf /var/backups/lisglosips-s00/etc-ssh-before-hardening-YYYYMMDDHHMMSS.tar.gz -C /
sudo sshd -t
sudo systemctl restart ssh
```
若 Key 登录完全失效,必须通过阿里云控制台/VNC 登录恢复,不要删除本地项目 Key 或 known_hosts。
## 6. DPAPI 凭据使用
```powershell
$servers = Import-Clixml .codex-private\SERVER_CREDENTIALS.clixml
$item = $servers | Where-Object Code -eq 'A'
$sudoPassword = $item.Credential.GetNetworkCredential().Password
```
禁止输出 `$sudoPassword`。需要自动化 sudo 时,将其通过进程内存或 SSH 标准输入传递,完成后立即清理环境变量。
+152
View File
@@ -0,0 +1,152 @@
# S02 网络与端口验收报告
> 验收时间:2026-06-20
> 环境:本地 KVM 开发服务器,Tailscale 三机专网
> 结论:开发网络满足当前联调要求;阿里云安全组尚未创建,本报告给出未来迁移清单。
## 1. 环境边界
| 代号 | Tailscale IP | 开发阶段角色 |
| --- | --- | --- |
| A | 100.90.90.90 | 核心通信网关 |
| B | 100.90.90.91 | API、缓存、数据库、录音和监控 |
| T | 100.93.185.30 | 客户/供应商 SIP 测试端 |
| 开发机 | 100.91.249.119 | SSH 与运营 Web 管理端 |
- 用户确认 A/B/T 均为本地开发服务器,开发完成后再迁移阿里云。
- 开发阶段服务间通信统一使用 Tailscale IP,不依赖 ens18 上的两个局域网地址。
- 本次未修改 Tailscale ACL、主机防火墙、路由、云安全组或服务配置。
- 尚未部署的端口使用绑定到指定 Tailscale IP 的短时测试监听器;测试后已确认进程退出。
## 2. 双向 RTT
每个方向先建立 Tailscale 直连,再发送 20 个 ICMP 样本:
| 来源 | 目标 | 丢包 | 平均 RTT | 最大 RTT |
| --- | --- | ---: | ---: | ---: |
| A | B | 0% | 0.648 ms | 2.389 ms |
| B | A | 0% | 0.595 ms | 0.741 ms |
| A | T | 0% | 0.434 ms | 0.668 ms |
| T | A | 0% | 0.559 ms | 1.711 ms |
| B | T | 0% | 0.593 ms | 0.757 ms |
| T | B | 0% | 0.561 ms | 1.496 ms |
结论:三机稳态 RTT 均小于 1 ms,丢包为 0,满足开发联调目标。
Tailscale 接口 MTU 为 1280。A 到 B/T 的 1200 字节 ICMP payload、DF 模式各 5 次均为 0% 丢包,平均 RTT 分别为 0.634 ms 和 0.657 ms。
## 3. 路径稳定性
- 三机稳态均为局域网 direct 路径,没有经过 DERP。
- 当前开发机到 A/B/T 建立直连后的延迟约为 10/9/3 ms。
- 测试过程中开发机到三机的 Tailscale 路径曾发生一次重新协商,约一分钟内 SSH 新连接超时;恢复后无需重启服务。
- 开发机测得最近 DERP 为 San Francisco,约 166 ms。若局域网 direct 失败并退化到 DERP,SIP/RTP 联调体验会明显下降。
- 建议开发时保持局域网 UDP 41641 可达,并监控 `tailscale ping` 是否显示 `direct`
## 4. 已执行端口验收
| 来源 | 目标 | 协议/端口 | 用途 | 结果 |
| --- | --- | --- | --- | --- |
| T | A | UDP 15060 | 规划中的客户 SIP 入口 | PASS,收到双向 ACK |
| T | A | UDP 30000 | RTP 范围下界 | PASS,收到双向 ACK |
| T | A | UDP 40000 | RTP 范围上界 | PASS,收到双向 ACK |
| A | T | UDP 30000 | RTP 反向下界 | PASS,收到双向 ACK |
| A | T | UDP 40000 | RTP 反向上界 | PASS,收到双向 ACK |
| A | T | UDP 5060 | T 机现有 OpenSIPS | PASSOPTIONS 返回 SIP 484 |
| 开发机 | B | TCP 443 | 规划中的运营 Web HTTPS | PASS,收到 ACK |
| A | B | UDP 9060 | HEP | PASS,收到 ACK |
| A | B | TCP 6379 | Redis 热路径 | PASS,收到 ACK |
| A | B | TCP 3306 | 网络路径测试 | PASS;安全策略仍禁止远程直连 |
| B | A | TCP 9100 | Prometheus 抓取 Exporter | PASS,收到 ACK |
| B | A | TCP 22 | 录音拉取/运维路径 | PASS,TCP 建连成功;历史开发端口,后续迁移为 `<SSH_ADMIN_PORT>` |
T 机 SIP OPTIONS 测试产生 1 条 `sip_trace` 记录;`acc``missed_calls` 未增加。该测试记录保留用于审计。
## 5. 开发阶段最小暴露矩阵
### Server A 入站
| 协议/端口 | 允许来源 | 要求 |
| --- | --- | --- |
| TCP `<SSH_ADMIN_PORT>` | 开发机、B | SSH 管理与 B 拉取录音;保留 Key 登录 |
| UDP 15060 | T | 开发 SIP 信令 |
| UDP 30000-40000 | T | 开发 RTP;与 RTPEngine 配置一致 |
| TCP 9100 | B | Node Exporter,仅监控抓取 |
| RTPEngine 控制端口 | 本机回环 | 不允许 Tailscale 或局域网访问 |
### Server B 入站
| 协议/端口 | 允许来源 | 要求 |
| --- | --- | --- |
| TCP `<SSH_ADMIN_PORT>` | 开发机 | SSH,保留 Key 登录 |
| TCP 443 | 开发机 | 运营端 HTTPS |
| UDP 9060 | A | HEP |
| TCP 6379 | A、B 本机 | Redis;密码/ACL、专网绑定 |
| TCP 3306 | B 本机 | MySQL 仅回环地址,不开放给 A |
| TCP 3000/9090 | B 本机 | Grafana/Prometheus 经反向代理或 SSH 隧道访问 |
| 应用内部端口 | B 本机 | 由 Nginx 代理,不直接暴露 |
### Server T 入站
| 协议/端口 | 允许来源 | 要求 |
| --- | --- | --- |
| TCP `<SSH_ADMIN_PORT>` | 开发机 | SSH,保留 Key 登录 |
| UDP 5060 | A | 测试 SIP |
| UDP 30000-40000 | A | 测试 RTP |
| TCP 80、TCP/UDP 111 | 默认禁止 | 现有 Apache/rpcbind 非 SIP 联调必需 |
## 6. Tailscale 与主机防火墙要求
- Tailscale ACL 应只允许上述源目标组合;当前主机侧无法读取 tailnet 管理端 ACL,需在 Tailscale 管理台复核。
- 主机防火墙仍必须实施默认拒绝,不能只依赖 Tailscale ACL。
- 服务应绑定 Tailscale IP 或 `127.0.0.1`,避免因 ens18 双地址选择错误而暴露到局域网。
- 开发环境也不得将 Redis、MySQL、RTPEngine 控制端口和 OpenSIPS MI 暴露给整个 tailnet。
- 防火墙落地分别属于 S03(B)和 S18/S19A),不在 S02 修改。
## 7. 未来阿里云安全组清单
### 迁移前网络
1. A、B 放在同一 VPC,优先同一可用区;业务间使用私网 IP。
2. 为 A/B 分配唯一私网地址和唯一默认路由,不复制本地 machine-id、网卡 UUID 或磁盘 UUID。
3. T 继续作为外部测试端,通过 A 的公网 SIP/RTP 入口验收。
4. 数据库、Redis、HEP、Exporter 和录音搬运走 VPC 私网。
### 安全组 A
开发阶段采用 UDP 30000-40000;迁移阿里云前应根据并发量重新计算并尽量缩小范围。
| 方向 | 协议/端口 | 来源/目标 |
| --- | --- | --- |
| 入站 | UDP 15060 | 已登记客户、供应商和 T 的公网 IP |
| 入站 | UDP 30000-40000 | 已登记 RTP 对端;无法固定时配合限速和监控 |
| 入站 | TCP `<SSH_ADMIN_PORT>` | 管理 VPN/堡垒机安全组;B 私网 IP 仅用于录音拉取;保留 Key 登录 |
| 入站 | TCP 9100 | B 的安全组或私网 IP |
| 出站 | TCP 6379 | B 私网 IP |
| 出站 | UDP 9060 | B 私网 IP |
### 安全组 B
| 方向 | 协议/端口 | 来源/目标 |
| --- | --- | --- |
| 入站 | TCP 443 | 运营人员允许地址;上线后可按产品策略扩大 |
| 入站 | TCP 80 | 可选,仅跳转 HTTPS |
| 入站 | TCP `<SSH_ADMIN_PORT>` | 管理 VPN/堡垒机安全组;保留 Key 登录 |
| 入站 | UDP 9060 | A 的安全组或私网 IP |
| 入站 | TCP 6379 | A 的安全组或私网 IP |
| 入站 | TCP 3306 | 禁止公网;同机应用优先 127.0.0.1 |
| 入站 | TCP 3000/9090 | 不开放公网,经 Nginx/VPN/隧道访问 |
| 出站 | TCP `<SSH_ADMIN_PORT>` | A 私网 IP,仅录音拉取需要时 |
实际创建安全组时应先记录规则截图/导出,按最小权限逐条添加,并从允许和禁止来源分别复测。
## 8. 验收结论
- A/B/T 双向可达、稳态 RTT 和 MTUPASS。
- SIP 15060、T SIP 5060、RTP 30000-40000 双向路径:PASS。
- B 的 Web 443、HEP 9060、Redis 6379 路径:PASS。
- B 到 A 的监控与 SSH 路径:PASS。
- 最小暴露矩阵:已形成。
- 阿里云控制台操作清单:已形成,未执行。
S02 验收通过。开发环境的主机防火墙和服务绑定将在后续任务按服务器逐步实施。
+86
View File
@@ -0,0 +1,86 @@
# Server A 资产盘点
> 采集时间:2026-06-20 17:55-17:59 +08:00
> 采集方式:SSH 只读命令;未安装软件、未修改配置、未重启服务。
## 1. 定位与结论
- 规划角色:核心通信网关机,后续部署 OpenSIPS、RTPEngine、录音内存盘、HEP Client 和指标 Exporter。
- 当前状态:通用 Ubuntu 桌面虚拟机,尚未安装通信栈。
- 就绪判断:可作为后续安装目标,但网络身份、防火墙和克隆残留必须先在 S02/S18 前处理。
## 2. 系统与容量
| 项目 | 当前值 |
| --- | --- |
| 主机名 | TonyHouse |
| 虚拟化 | KVM/QEMU |
| OS | Ubuntu 24.04.4 LTS |
| 内核 | 6.17.0-20-generic |
| CPU | 4 vCPUQEMU Virtual CPU1 socket/4 cores |
| 内存 | 7.8 GiB,总可用约 5.7 GiB |
| Swap | 4.0 GiB,当前未使用 |
| 系统盘 | 200 GiB ext4,根分区 196 GiB |
| 磁盘使用 | 已用 21 GiB,可用 166 GiB,使用率 12% |
| 时间 | Asia/Shanghaisystemd-timesyncd 已同步 |
| HugePages | 0,尚未配置 |
系统仅有单块系统盘,没有独立数据盘。Ubuntu 安装 ISO 仍挂载在光驱路径。
## 3. 网络
| 接口 | 地址 |
| --- | --- |
| ens18 | 192.168.71.90/24、192.168.71.2/24 |
| tailscale0 | 100.90.90.90/32 |
- ens18 同时存在静态地址和 DHCP 地址。
- 主路由表同时存在两个相同 metric 的默认路由:192.168.71.253 和 192.168.71.1。
- 项目 SSH 地址 100.90.90.90 是 Tailscale 地址,不是从本机识别出的公网地址。
- DNS 同时使用局域网 DNS、公共 DNS 和 Tailscale DNS。
- 机器没有阿里云实例元数据标识,cloud-init instance-id 为 `iid-datasource-none`
## 4. 服务、端口与软件
### 监听端口
- TCP 22SSH,对所有 IPv4/IPv6 地址监听。
- TCP 18789、18791Node/OpenClaw,仅回环地址监听。
- UDP 41641Tailscale。
- UDP/TCP 5353、TCP 631:桌面环境的 mDNS/CUPS。
- 当前没有 SIP 15060、RTPEngine 控制端口或 RTP 端口段监听。
### 软件与服务
- Node.js 22.22.2 已安装。
- Tailscale 1.96.4 已安装并运行。
- Docker、OpenSIPS、RTPEngine、Redis、MySQL、Nginx、Prometheus、Grafana、Fail2ban 未发现。
- GNOME、GDM、CUPS、Avahi、ModemManager 等桌面服务正在运行。
- systemd 没有失败单元。
## 5. 安全与风险
1. **高:主机防火墙未启用。** UFW 为 inactivenftables INPUT/FORWARD 默认策略为 accept,仅包含 Tailscale 自动规则。
2. **高:网络出口不确定。** 双 IPv4、双默认路由且 metric 相同,SIP Contact、Record-Route、SDP 和 RTP 回包可能选择错误源地址。
3. **高:克隆身份未清理。** A/B/T 的 `/etc/machine-id`、根分区 UUID 和 NetworkManager 连接 UUID 完全相同。
4. **中:开发与生产网络需分阶段管理。** 用户已确认当前为 KVM 本地开发机加 Tailscale,开发完成后再迁移阿里云;本机配置不能直接原样复制到云端。
5. **中:资源低于原架构图描述。** 当前为 4 vCPU/8 GiB,与“4 核 8G”一致,但没有独立录音盘;3 GiB tmpfs 会显著压缩系统可用内存。
6. **低:桌面服务占用资源并扩大攻击面。** 后续应在维护窗口按需精简,不在 S01 处理。
## 6. 后续安装前备份清单
- `/etc/netplan/`、NetworkManager 活动连接导出、`ip addr/route/rule` 快照。
- `/etc/fstab``lsblk`、文件系统 UUID 与挂载快照。
- `/etc/ssh/` 和 S00 既有备份。
- `ufw status``nft list ruleset``iptables-save``ip6tables-save`
- `sysctl -a` 中网络、conntrack、文件句柄相关基线。
- 已安装包列表、enabled/running/failed systemd 单元列表。
- `/etc/systemd/` 本地覆盖项、`/opt/` 和现有 Node/OpenClaw 服务定义。
- 变更 machine-id 或网络连接 UUID 前制作虚拟机快照并验证云/虚拟化控制台可达。
## 7. S02 输入
- 明确生产访问是公网/VPC、局域网端口映射还是继续使用 Tailscale。
- 明确 SIP 15060/UDP 的入站地址和 RTP UDP 端口段。
- 消除双地址、双默认路由歧义后再配置 OpenSIPS/RTPEngine。
- 建立默认拒绝的主机防火墙和外部安全组端口矩阵。
+86
View File
@@ -0,0 +1,86 @@
# Server B 资产盘点
> 采集时间:2026-06-20 17:55-17:59 +08:00
> 采集方式:SSH 只读命令;未安装软件、未修改配置、未重启服务。
## 1. 定位与结论
- 规划角色:业务、缓存与监控中心,后续部署 API/Worker、Redis、MySQL、HOMER、Prometheus、Grafana 和 Web。
- 当前状态:通用 Ubuntu 桌面虚拟机,只有 Node.js 和本地 OpenClaw 进程,业务基础设施尚未安装。
- 就绪判断:容量可支持原型和小规模联调;生产化之前必须解决网络身份、失败的 NFS 挂载、防火墙和单盘故障域。
## 2. 系统与容量
| 项目 | 当前值 |
| --- | --- |
| 主机名/FQDN | YanZi / YanZi.taileedb6e.ts.net |
| 虚拟化 | KVM/QEMU |
| OS | Ubuntu 24.04.4 LTS |
| 内核 | 6.17.0-23-generic |
| CPU | 4 vCPUQEMU Virtual CPU1 socket/4 cores |
| 内存 | 7.8 GiB,总可用约 6.4 GiB |
| Swap | 4.0 GiB,当前未使用 |
| 系统盘 | 200 GiB ext4,根分区 196 GiB |
| 磁盘使用 | 已用 26 GiB,可用 161 GiB,使用率 14% |
| 时间 | Asia/Shanghaisystemd-timesyncd 已同步 |
| HugePages | 0 |
只有单块系统盘,数据库、Redis、录音、日志和监控数据目前没有独立磁盘或独立故障域。
## 3. 网络
| 接口 | 地址 |
| --- | --- |
| ens18 | 192.168.71.91/24、192.168.71.3/24 |
| tailscale0 | 100.90.90.91/32 |
- ens18 同时存在静态地址和 DHCP 地址。
- 同时存在经 192.168.71.253 与 192.168.71.1 的两个等 metric 默认路由。
- 项目 SSH 地址 100.90.90.91 是 Tailscale 地址。
- cloud-init instance-id 为 `iid-datasource-none`,本机未识别到阿里云实例环境。
- `/etc/fstab` 配置了 `100.120.96.71:/mnt/share``/home/hector/share`,当前 mount 单元失败。
## 4. 服务、端口与软件
### 监听端口
- TCP 22SSH,对所有 IPv4/IPv6 地址监听。
- TCP/UDP 111rpcbind,对所有地址监听。
- TCP 18789、18791Node/OpenClaw,仅回环地址监听。
- UDP 41641Tailscale。
- 当前没有 80/443、3306、6379、9060、Prometheus 或 Grafana 端口。
### 软件与服务
- Node.js 22.22.2 已安装。
- Tailscale 1.96.4 已安装并运行。
- Docker、Redis、MySQL/MariaDB、Nginx、HOMER、Prometheus、Grafana、Fail2ban 未发现。
- GNOME、GDM、CUPS、Avahi、rpcbind 等桌面/通用服务运行中。
- 失败单元:`home-hector-share.mount`
## 5. 安全与风险
1. **高:主机防火墙未启用。** UFW inactivenftables INPUT/FORWARD 默认 accept。
2. **高:网络出口不确定。** 双 IPv4、双默认路由会影响服务绑定、回调地址和 A/B 数据链路。
3. **高:克隆身份未清理。** 与 A/T 共享 machine-id、根文件系统 UUID 和 NetworkManager 连接 UUID。
4. **高:单盘承载全部数据。** MySQL、Redis AOF、录音、HOMER 和监控数据若共用根盘,容量竞争和故障影响过大。
5. **中:NFS 开机挂载失败。** 需确认该共享是否继续承担录音或备份;生产服务不能依赖未设超时/依赖关系的失败 mount。
6. **中:rpcbind 对所有地址监听。** 若不再使用 NFS,应移除暴露;若保留,只允许可信网段。
7. **中:开发与生产网络需分阶段管理。** 当前本地 Tailscale 配置不能直接作为未来阿里云 VPC/安全组配置。
8. **低:桌面服务会消耗业务机资源并扩大攻击面。**
## 6. 后续安装前备份清单
- A 机同类的网络、路由、DNS、SSH、防火墙、sysctl、包和 systemd 基线。
- `/etc/fstab`、NFS mount 单元状态及共享端现状。
- `/opt/`、现有 Node/OpenClaw 进程的服务定义与配置。
- MySQL/Redis 安装前的磁盘布局、I/O 基线和容量分配表。
- 部署数据库前制作虚拟机快照;后续数据库备份不得只保存在同一根盘。
- 修改 machine-id、文件系统 UUID 或 NetworkManager 连接前保留控制台恢复路径。
## 7. S02/S03 输入
- 先确定 B 的稳定业务地址、A/B 私网链路以及 Web 对外入口。
- 决定录音与数据库是新增独立磁盘、使用可靠 NFS,还是仅在 PoC 阶段共用根盘。
- 明确 NFS 失败挂载的去留。
- 建立默认拒绝防火墙;3306/6379 只绑定本地或 A/B 专用内网,不对公网/Tailscale 全网开放。
+108
View File
@@ -0,0 +1,108 @@
# Server T 资产与 OpenSIPS 盘点
> 采集时间:2026-06-20 17:55-17:59 +08:00
> 采集方式:SSH、OpenSIPS MI、RTPEngine CLI 和 MariaDB 只读查询;未修改配置、数据或服务状态,未发起 SIP 呼叫。
## 1. 定位与结论
- 规划角色:模拟客户 SIP 注册、IP 认证和呼叫的测试机。
- 当前状态:OpenSIPS、RTPEngine、录音守护进程、MariaDB、Apache/PHP 和 Monit 已运行。
- 配置健康:`opensips -C` 通过;OpenSIPS MI 可用;RTPEngine 当前被 OpenSIPS 标记为 enabled。
- 测试就绪度:基础进程就绪,但数据库中没有 subscriber、location、dispatcher、落地网关或路由规则,当前还不能完成真实注册/呼叫验收。
## 2. 系统与容量
| 项目 | 当前值 |
| --- | --- |
| 主机名 | SteveTable |
| 虚拟化 | KVM/QEMU |
| OS | Ubuntu 24.04.4 LTS |
| 内核 | 6.17.0-35-generic |
| CPU | 4 vCPUAMD Ryzen 7 3700X 型号透传 |
| 内存 | 7.8 GiB,总可用约 6.5 GiB |
| Swap | 4.0 GiB,当前未使用 |
| 系统盘 | 200 GiB ext4,根分区可用 164 GiB,使用率 13% |
| NFS | 100.120.96.71:/mnt/share,已挂载,可用约 345 GiB |
| 时间 | Asia/Shanghaisystemd-timesyncd 已同步 |
## 3. 网络与端口
| 接口 | 地址 |
| --- | --- |
| ens18 | 192.168.71.105/24、192.168.71.239/24 |
| tailscale0 | 100.93.185.30/32 |
- 与 A/B 相同,存在双 IPv4 和两个等 metric 默认路由。
- OpenSIPS 仅监听 `100.93.185.30:5060/UDP`
- RTPEngine 控制端口 2223/UDP、2224/TCP、2225/TCP 只监听回环地址。
- MariaDB 3306 只监听回环地址。
- OpenSIPS MI HTTP 8888 只监听回环地址。
- Apache 80/TCP、SSH 22/TCP、rpcbind 111/TCP/UDP 对所有地址监听。
- RTPEngine RTP 范围配置为 UDP 30000-40000,但空闲时不表现为固定监听 socket。
- UFW inactivenftables INPUT/FORWARD 默认 accept。
## 4. 通信栈
| 组件 | 版本/状态 |
| --- | --- |
| OpenSIPS | 3.6.6active/enabled |
| RTPEngine | 11.5.1.18active/enabled |
| RTPEngine Recording | 11.5.1.18active/enabled |
| MariaDB | 10.11.14active/enabled |
| Apache | 2.4.58active/enabled |
| PHP | 8.3.6 |
| Monit | 5.33.0active/enabled |
### OpenSIPS 配置能力
- UDP SIP socket100.93.185.30:5060。
- 已加载 auth/auth_db、domain、permissions、usrloc/registrar、dispatcher、drouting、dialog、acc、tracer、rtpengine、rtpproxy。
- MI HTTP127.0.0.1:8888MI FIFO`/run/opensips/opensips_fifo`
- 数据库中已有 domain 2 条、address 1 条、rtpengine 1 条。
- subscriber、location、dispatcher、dr_gateways、dr_rules、sip_trace 均为 0 条。
- 自本次启动以来:接收 SIP 请求/回复为 0,注册成功/失败均为 0,dialog 为 0。
- 64 MiB OpenSIPS shared memory 当前实际使用约 3.8 MiB。
### RTPEngine 与录音
- 控制端:127.0.0.1:2223CLI127.0.0.1:2224HTTP127.0.0.1:2225。
- RTP 范围:30000-40000;声明接口为 100.93.185.30。
- 当前会话 0,总处理会话 0,说明尚未完成过媒体联调。
- `xt_RTPENGINE` 内核模块不存在,启动日志明确显示 kernel forwarding disabled;目前只能依赖 userspace forwarding。
- 录音目录 `/var/spool/rtpengine``/var/lib/rtpengine-recording` 均为空,权限为 rtpengine 用户/组 0770。
- NFS 共享已挂载,但当前 RTPEngine 录音目录并未直接挂到该 NFS。
## 5. 安全与运行风险
1. **高:TLS 私钥权限错误。** `cakey.pem``user-privkey.pem` 为 0644,所有本机用户可读。
2. **高:MI FIFO 为 0666。** 任意本机用户可向 OpenSIPS 管理接口发送命令。
3. **高:防火墙默认放行。** Apache、SSH、rpcbind 等暴露范围依赖外部网络控制,主机本身没有默认拒绝策略。
4. **高:克隆身份未清理。** A/B/T 共享 machine-id、根文件系统 UUID 和 NetworkManager 连接 UUID。
5. **中:RTPEngine 无内核加速。** `xt_RTPENGINE` 与当前 6.17 内核不匹配,无法满足架构图中的内核态加速目标。
6. **中:服务启动顺序存在竞态。** OpenSIPS 比 RTPEngine 早约 8 秒启动,启动日志中一度禁用 RTPEngine;当前已自动恢复为 enabled。
7. **中:测试数据为空。** 尚无 SIP 注册账号、注册位置、dispatcher 或路由,T 机目前只能证明进程健康,不能证明呼叫链路健康。
8. **中:Apache 80 与 rpcbind 111 对所有接口开放。**
9. **低:fwupd 刷新任务失败。** `fwupd-refresh.service` 当前失败,与 SIP 主链路无关但应清理告警。
## 6. 配置基线与备份清单
正式执行 S21 前至少备份:
- `/etc/opensips/` 全目录,当前主配置 SHA-256`987498fca01160ba07f380bf870e38c2140e61ac51e0f93c154b56b1480d37af`
- `/etc/rtpengine/``/etc/default/rtpengine-*` 和相关 systemd unit。
- `/etc/mysql/``mysqldump --single-transaction --routines --events --all-databases`
- OpenSIPS 数据库各表行数、schema 和现有 domain/address/rtpengine 数据导出。
- `/etc/apache2/``/etc/php/``/etc/monit/`
- 当前 nftables/iptables、RTPEngine chain、端口和路由快照。
- NFS/fstab、录音目录权限和挂载状态。
- 修改 TLS 权限、MI 权限、启动依赖或内核前制作虚拟机快照。
私钥、数据库连接串和 SIP 密码不得写入项目文档或普通日志。
## 7. 后续任务输入
- S02:验证 A/T 的 15060/5060 SIP 可达性、RTP 30000-40000 双向可达性和 Tailscale/真实公网路径。
- S21:新增专用测试账号与 IP 认证数据,保留失败场景;不要复用生产密码。
- S21 前先修复 TLS 私钥和 MI FIFO 权限。
- 决定 T 机是否需要 RTPEngine 内核态加速;若只模拟客户侧信令,可不把它作为 S21 阻塞项。
- 调整 OpenSIPS 对 RTPEngine 的 systemd 启动顺序,或确认启动后的自动重检策略。
+45
View File
@@ -0,0 +1,45 @@
# S01 三机资产盘点汇总
> 结论日期:2026-06-20。详细数据见 `inventory-A.md`、`inventory-B.md`、`inventory-T.md`。
## 1. 差异总览
| 项目 | A | B | T |
| --- | --- | --- | --- |
| OS | Ubuntu 24.04.4 | Ubuntu 24.04.4 | Ubuntu 24.04.4 |
| 内核 | 6.17.0-20 | 6.17.0-23 | 6.17.0-35 |
| CPU/内存 | 4 vCPU/7.8 GiB | 4 vCPU/7.8 GiB | 4 vCPU/7.8 GiB |
| 根盘可用 | 166 GiB | 161 GiB | 164 GiB |
| NFS | 未配置 | 已配置但挂载失败 | 已挂载 |
| 通信栈 | 未安装 | 未安装 | OpenSIPS/RTPEngine 已运行 |
| 数据库/缓存 | 无 | 无 | MariaDB 10.11,仅供现有 OpenSIPS |
| UFW | inactive | inactive | inactive |
| systemd 失败项 | 无 | NFS mount | fwupd refresh |
## 2. 共同问题
- 三机源于同一镜像,machine-id、根文件系统 UUID、NetworkManager 连接 UUID 完全相同。
- 三机 ens18 均同时存在静态和 DHCP IPv4,并有两个等 metric 默认路由。
- 三个项目地址均为 Tailscale 100.64.0.0/10 地址;用户已确认三机是本地开发服务器,完成开发后再迁移阿里云。
- 三机均为带 GNOME 的桌面安装,不是最小化服务器系统。
- 三机 UFW 均未启用,nftables 默认接受入站。
- 三机均仅有一块 200 GiB 系统盘;B 的数据库、录音与监控存储规划尚未落盘。
## 3. S02 前置决策
1. 开发阶段使用 Tailscale 三机专网;另行保留阿里云 VPC/公网迁移清单。
2. 确认 A 的 SIP 15060 和 RTP UDP 范围,T 的测试 SIP 5060 和 RTP 范围。
3. 确认是否保留 ens18 DHCP 地址;生产服务必须绑定稳定地址并有唯一默认路由。
4. 确认 B 的数据盘/录音盘/NFS 方案。
5. S02 只验收连通和端口矩阵;涉及 machine-id、路由、防火墙的修改应形成独立可回滚步骤。
## 4. 立即风险排序
1. T 的 TLS 私钥 0644 与 OpenSIPS MI FIFO 0666。
2. A/B/T 主机防火墙默认放行。
3. 双地址、双默认路由可能导致 SIP/RTP 非对称。
4. 克隆身份冲突可能影响日志、DHCP、监控和后续自动化。
5. T 的 RTPEngine 内核模块缺失,当前没有内核态转发能力。
6. B 的 NFS mount 失败,且没有独立数据盘。
S01 只记录上述事实,没有进行任何修复。