commit 78775868d6600db1d681f53b03395cfbbd86accd Author: qiurui Date: Sat Feb 28 17:33:28 2026 +0800 feat: KVM-Privacy Hub initial commit Phase 0: infra fixes (OCR dir, port conflict, systemd services) Phase 1: video PII masking (frame_redactor, privacy API) Phase 2: AI privacy gateway (mitmproxy addon, audit logger) Phase 3: AI routing (Ollama backend, memory admin endpoints) Phase 4: deployment scripts and Makefile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d65b178 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +*.db +/deploy/systemd/*.service.override +/services/privacy_gateway/__pycache__/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0845129 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +DEVICE_IP ?= 192.168.123.181 +DEVICE_USER ?= pi +SSH := sshpass -p pi ssh -o StrictHostKeyChecking=no $(DEVICE_USER)@$(DEVICE_IP) + +.PHONY: deploy-gateway deploy-memory deploy-all status logs help + +help: + @echo "KVM-Privacy Hub 部署工具" + @echo "" + @echo " make deploy-gateway 安装 Privacy Gateway (mitmproxy)" + @echo " make deploy-memory 部署 mem-bridge 服务" + @echo " make deploy-all 完整部署 (gateway + memory + Ollama)" + @echo " make status 查看所有服务状态" + @echo " make logs SVC= 查看指定服务日志" + @echo "" + @echo "变量: DEVICE_IP=$(DEVICE_IP) DEVICE_USER=$(DEVICE_USER)" + +deploy-gateway: + DEVICE_IP=$(DEVICE_IP) DEVICE_USER=$(DEVICE_USER) bash scripts/setup_gateway.sh + +deploy-memory: + @echo "=== 部署 mem-bridge 服务 ===" + sshpass -p pi scp -o StrictHostKeyChecking=no \ + deploy/systemd/mem-bridge-memory.service \ + deploy/systemd/mem-bridge-router.service \ + $(DEVICE_USER)@$(DEVICE_IP):/tmp/ + $(SSH) "echo '123123' | sudo -S cp /tmp/mem-bridge-*.service /etc/systemd/system/ && \ + sudo systemctl daemon-reload && \ + sudo systemctl enable --now mem-bridge-memory mem-bridge-router" + @echo "mem-bridge 服务已启动" + +deploy-all: deploy-gateway deploy-memory + DEVICE_IP=$(DEVICE_IP) DEVICE_USER=$(DEVICE_USER) bash scripts/install_ollama.sh + @echo "=== 完整部署完成 ===" + +status: + @echo "=== KVM-Privacy Hub 服务状态 ===" + $(SSH) "systemctl is-active kvm-server info-privacy mem-bridge-memory mem-bridge-router privacy-gateway 2>/dev/null | paste - - - - - | column -t" || true + @echo "" + @echo "=== 端口监听 ===" + $(SSH) "ss -tlnp | grep -E '8080|8000|8001|8002|8888' || echo '无监听端口'" + +logs: + $(SSH) "journalctl -u $(SVC) -n 50 --no-pager" + +clean: + @echo "清理本地临时文件" + find . -name '*.pyc' -delete + find . -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true diff --git a/deploy/systemd/info-privacy.service b/deploy/systemd/info-privacy.service new file mode 100644 index 0000000..315841b --- /dev/null +++ b/deploy/systemd/info-privacy.service @@ -0,0 +1,22 @@ +[Unit] +Description=Info-Privacy PII Detection Service +After=network.target +Wants=network.target + +[Service] +Type=simple +User=pi +Group=pi +WorkingDirectory=/home/pi/Desktop/info-privacy +ExecStart=/home/pi/Desktop/info-privacy/venv/bin/uvicorn \ + info_privacy.api.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --workers 1 +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/kvm-ocr.conf b/deploy/systemd/kvm-ocr.conf new file mode 100644 index 0000000..0357d20 --- /dev/null +++ b/deploy/systemd/kvm-ocr.conf @@ -0,0 +1,4 @@ +# /etc/tmpfiles.d/kvm-ocr.conf +# 设备重启后自动重建 OCR 快照目录 +# 格式: type path mode user group age +d /tmp/kvm-ocr 0755 root root - diff --git a/deploy/systemd/mem-bridge-memory.service b/deploy/systemd/mem-bridge-memory.service new file mode 100644 index 0000000..fe607d0 --- /dev/null +++ b/deploy/systemd/mem-bridge-memory.service @@ -0,0 +1,19 @@ +[Unit] +Description=Mem-Bridge Memory Service +After=network.target +Wants=network.target + +[Service] +Type=simple +User=pi +Group=pi +WorkingDirectory=/data/rockchip/embedding +EnvironmentFile=-/data/rockchip/embedding/.env +ExecStart=/data/rockchip/embedding/venv/bin/python server.py --service memory +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/mem-bridge-router.service b/deploy/systemd/mem-bridge-router.service new file mode 100644 index 0000000..8bd647e --- /dev/null +++ b/deploy/systemd/mem-bridge-router.service @@ -0,0 +1,20 @@ +[Unit] +Description=Mem-Bridge Router Service +After=network.target mem-bridge-memory.service +Wants=network.target +Requires=mem-bridge-memory.service + +[Service] +Type=simple +User=pi +Group=pi +WorkingDirectory=/data/rockchip/embedding +EnvironmentFile=-/data/rockchip/embedding/.env +ExecStart=/data/rockchip/embedding/venv/bin/python server.py --service router +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/privacy-gateway.service b/deploy/systemd/privacy-gateway.service new file mode 100644 index 0000000..ef1a7bc --- /dev/null +++ b/deploy/systemd/privacy-gateway.service @@ -0,0 +1,22 @@ +[Unit] +Description=KVM Privacy Gateway (mitmproxy) +After=network.target info-privacy.service +Wants=network.target info-privacy.service + +[Service] +Type=simple +User=root +WorkingDirectory=/data/project/KVM-privacy/services/privacy_gateway +ExecStart=/usr/local/bin/mitmdump \ + --mode regular \ + --listen-host 0.0.0.0 \ + --listen-port 8888 \ + --ssl-insecure \ + -s /data/project/KVM-privacy/services/privacy_gateway/addon.py +Restart=on-failure +RestartSec=10 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/docs/plans/2026-02-27-kvm-privacy-design.md b/docs/plans/2026-02-27-kvm-privacy-design.md new file mode 100644 index 0000000..8496794 --- /dev/null +++ b/docs/plans/2026-02-27-kvm-privacy-design.md @@ -0,0 +1,324 @@ +# KVM-Privacy 项目设计文档 + +**日期**:2026-02-27 +**状态**:已批准 +**硬件**:NanoPC-T6(RK3588,8GB RAM,64GB eMMC) + +--- + +## 1. 项目目标 + +设计一台具备 PiKVM 类似能力的 RK3588 边缘设备,同时作为 AI 隐私网关,满足两类核心场景: + +| 场景 | 需求 | +|------|------| +| **KVM 控制** | 通过浏览器远程控制目标机的键鼠和视频,屏幕上的敏感信息(PII)在传输前自动遮罩 | +| **AI 隐私代理** | 用户使用 Claude/GPT 等 web AI 工具时,上传的文档在离开设备前自动检测并脱敏 | + +云边协同:RK3588 承载本地推理、记忆管理、复杂度路由;超出本地能力时调用云端 API。 + +--- + +## 2. 整体架构 + +``` + ┌────────────────────────────────────────────┐ + [目标机/服务器] │ RK3588 KVM-Privacy Hub │ + HDMI ──────────────► │ ┌─────────────────────────────────────┐ │ + USB ◄────────────── │ │ KVM Engine │ │ + (HID 模拟) │ │ HDMI Capture → OCR → PII Mask │ │ + │ │ → WebRTC Stream │ │ + │ └─────────────────────────────────────┘ │ + │ ▲ │ + [用户设备] │ │ │ + 浏览器 ─────LAN──────► │ ┌─────────────────┴───────────────────┐ │ + │ │ Web UI (FastAPI + Vue3) │ │ + │ │ KVM页 / 隐私仪表盘 / AI对话 / 设置 │ │ + │ └─────────────────────────────────────┘ │ + │ │ + [用户设备流量] │ ┌─────────────────────────────────────┐ │ + NIC0(LAN) ──────────► │ │ Privacy Gateway (mitmproxy) │ │ + NIC1(WAN) ──────────► │ │ AI域名白名单 → 附件解析 → redact │ │ + │ │ → 审计日志 → 放行脱敏后请求 │ │ + │ └─────────────────────────────────────┘ │ + │ │ │ + │ ┌─────────────────▼───────────────────┐ │ + │ │ AI 路由层 (mem_bridge) │ │ + │ │ 复杂度评分 → 本地 Qwen2-1.5B │ │ + │ │ 或 → 云端 API │ │ + │ └──────────┬──────────────────────────┘ │ + │ │ │ + │ ┌──────────▼──────────┐ │ + │ │ Memory / embed-db │ │ + │ │ 短期会话 + 文档索引 │ │ + │ └─────────────────────┘ │ + └────────────────────────────────────────────┘ + │ WAN + ┌─────▼──────┐ + │ 云端 API │ + │ Claude/GPT │ + └────────────┘ +``` + +**五个子系统**: +1. **KVM Engine** — 视频采集、HID 模拟、视频流隐私过滤 +2. **Privacy Gateway** — HTTPS 透明代理、AI 请求文档过滤 +3. **AI 路由层** — 复杂度路由、记忆注入(复用 mem_bridge) +4. **Memory / embed-db** — 对话记忆 + 文档语义索引(复用) +5. **Web UI** — 统一控制界面 + +--- + +## 3. 子系统详细设计 + +### 3.1 KVM Engine + +**硬件**: +- HDMI Capture Card (USB/PCIe) → V4L2 `/dev/video0` +- USB OTG → HID 模拟(键盘 + 鼠标)via USB Gadget (configfs) + +**视频流水线**: +``` +V4L2 采集 (MJPEG/RAW) + → RK3588 MPP 硬件 H.264 编码 + → 帧采样器 (运动检测触发 or 3s 定时兜底) + → PaddleOCR RKNN 检测文字区域 + → PII 规则匹配 → 遮罩坐标缓存 + → 叠加遮罩(黑色矩形,<1ms) + → WebRTC 推流 → 用户浏览器 +``` + +**OCR 采样策略**(异步,不阻塞推流主路径): +- 画面变化 > 阈值时触发 +- 最长每 3 秒兜底触发一次 +- 遮罩坐标有效期至下次检测,平滑过渡 + +**HID 转发**:浏览器鼠标/键盘事件 → WebSocket → `/dev/usb-gadget` → 目标机 + +**计划工作(Phase 3+)**: +- 不同连接用户的视图权限控制(防心轿展示) +- KVM 会话审计日志(录屏审计,不含原始视频) + +### 3.2 Privacy Gateway + +**网络拓扑**: +``` +NIC0 eth0 ← LAN 侧 (用户设备) 192.168.2.x/24 +NIC1 eth1 ← WAN 侧 (上游路由器) DHCP + +iptables REDIRECT → mitmproxy 透明代理 (port 8080) +仅对 AI 域名白名单签发中间证书,其余域名直接 BYPASS +``` + +**AI 域名白名单**(`configs/ai_domains.txt`): +``` +api.anthropic.com +api.openai.com +generativelanguage.googleapis.com +dashscope.aliyuncs.com +...(可配置扩展) +``` + +**请求处理流水线**: +``` +拦截 HTTPS 请求 + → Content-Type 检查 + → 含文件附件? + 是 → info-privacy.analyze() → PII 报告 + → 策略 A(自动): redact all high-level PII → 替换附件 → 放行 + → 策略 B(确认): 暂停 → Web UI 弹窗 → 用户决策 → 放行/阻断 + 否 → 文本 PII 快速扫描 → 通过则放行 + → 审计日志写入(无原文) + → 转发脱敏后请求到目标 API +``` + +**审计日志格式**(append-only SQLite): +```json +{ + "ts": "2026-02-27T10:00:00Z", + "user_id": "uid_xxx", + "domain": "api.anthropic.com", + "pii_types": {"id_card": 2, "phone": 1}, + "action": "auto_redact", + "doc_hash": "sha256:abc..." +} +``` + +**原文数据处理**:仅在内存 buffer 中处理,redact 完成后立即释放,不落盘。 +**处理后文档**:会话内临时持有(TTL 30 分钟),用户可在 Web UI 预览脱敏对比,会话关闭后自动释放。 + +### 3.3 AI 路由层(复用 mem_bridge) + +直接复用 `/data/rockchip/embedding/src/mem_bridge/`,新增本地 Qwen2-1.5B 后端: + +```yaml +# configs/gateway.yaml +backends: + default: qwen_local # 轻量/隐私任务 → 本地 + heavy: claude_sonnet # 复杂任务 → 云端 + + qwen_local: + type: compatible + base_url: http://localhost:11434/v1 # Ollama + model: qwen2:1.5b + api_key: dummy + + claude_sonnet: + type: anthropic + model: claude-sonnet-4-6 + api_key: ${ANTHROPIC_API_KEY} +``` + +**路由规则**(ComplexityScorer 模板): +- **本地(Qwen2-1.5B)**:PII 分类、文档摘要(<1k 字)、路由判断、隐私策略咨询 +- **云端**:长文档分析、代码生成、深度推理、多轮复杂对话 + +### 3.4 Memory 服务(复用 embed-db) + +直接复用 `/data/rockchip/embedding/src/`。 + +- **短期记忆**:会话对话历史(faiss + SQLite,TTL 会话级) +- **长期知识库**:用户文档索引(需用户主动导入),multilingual-e5-small 384d +- **用户隔离**:session_id 命名空间隔离,不同账户索引目录独立 + +### 3.5 Web UI + +**页面结构**: +``` +/ (单页应用,Vue3 + Vite) +├── /kvm KVM 控制台 +│ WebRTC 画面 + 键鼠输入捕获 +│ PII 遮罩开关 / 被遮内容类型提示 +│ +├── /privacy 隐私网关仪表盘 +│ 今日拦截统计 / 审计日志浏览 +│ 域名白名单管理 / 拦截策略配置 +│ 首次使用:证书安装向导 +│ +├── /chat AI 对话 +│ 文件上传 → 自动检测 → 脱敏预览 → 确认发送 +│ 通过路由层,带记忆注入 +│ +└── /settings 账户 / 网络 / 模型 / 隐私策略 +``` + +**技术选型**: +- 后端:FastAPI + uvicorn(统一 API 聚合网关) +- 前端:Vue3 + Vite(轻量 SPA,3588 直接 serve) +- 通信:REST + WebSocket(KVM 流 + 实时通知) +- 认证:JWT(python-jose)+ HTTPS(设备自签证书) + +--- + +## 4. 数据流 & 安全边界 + +### 4.1 安全边界 + +| 边界 | 措施 | +|------|------| +| Web UI 访问 | JWT 认证 + HTTPS(设备自签证书) | +| 多用户隔离 | session_id 命名空间隔离,账户间内存索引独立 | +| HTTPS 拦截证书 | 设备生成独立 CA,仅对 AI 域名白名单签发中间证书 | +| 原文不落盘 | Privacy Gateway 所有处理在内存 buffer | +| 审计日志防篡改 | append-only SQLite WAL + 定期 hash 链 | +| 本地 LLM 数据边界 | 敏感任务优先路由到本地 Qwen2-1.5B,不出设备 | + +### 4.2 首次使用引导 + +``` +1. 用户访问 http://kvm.local → 重定向到引导页 +2. 下载设备根证书(一键) → 指引安装到系统/浏览器信任区 +3. 选择拦截范围(推荐:仅 AI 域名白名单) +4. 验证代理连通性 → 完成(全程 < 3 分钟) +``` + +--- + +## 5. 技术选型汇总 + +| 层 | 技术 | 说明 | +|----|----|------| +| 视频采集 | V4L2 + RK3588 MPP | 硬件 H.264 编码,低延迟 | +| KVM 视频流 | WebRTC (aiortc) | 浏览器原生,低延迟 | +| HID 模拟 | USB Gadget (configfs) | Linux 标准方案 | +| KVM OCR | PaddleOCR RKNN | 复用现有模型 | +| 文档隐私 | info-privacy(复用) | 零开发成本 | +| HTTPS 拦截 | mitmproxy + 自定义 addon | Python 生态,易扩展 | +| AI 路由 | mem_bridge(复用) | 含复杂度路由 + 记忆注入 | +| 本地 LLM | Qwen2-1.5B via Ollama | RKNN NPU 加速 | +| 向量数据库 | embed-db(复用) | faiss + SQLite | +| 后端框架 | FastAPI + uvicorn | 统一 API 网关 | +| 前端 | Vue3 + Vite | 轻量 SPA | +| 认证 | JWT (python-jose) | 多用户支持 | +| 配置管理 | YAML + dotenv | 同现有项目约定 | +| 日志审计 | SQLite append-only | 轻量无外部依赖 | + +--- + +## 6. 开发阶段 + +### Phase 1 — 隐私网关 MVP(最高优先级) +- mitmproxy addon 框架 + AI 域名白名单过滤 +- 接入 info-privacy(analyze + redact) +- 审计日志服务 +- Web UI:隐私仪表盘 + 证书引导向导 + +### Phase 2 — AI 路由集成 +- mem_bridge 配置 Qwen2-1.5B 本地后端(Ollama) +- 多用户 session 隔离 +- Web UI:AI 对话页 + 文档脱敏预览交互 + +### Phase 3 — KVM 核心 +- V4L2 采集 + WebRTC 推流(aiortc) +- HID 模拟(USB Gadget) +- OCR 帧采样 + PII 遮罩渲染 +- Web UI:KVM 控制台 + +### Phase 4 — 打磨 & 集成 +- 首次使用向导完善 +- 多用户账户管理 +- 性能调优(NPU 并发、内存优化) +- 设备出厂配置脚本 + +--- + +## 7. 项目目录结构 + +``` +KVM-privacy/ +├── services/ +│ ├── privacy_gateway/ # mitmproxy addon + 审计日志服务 +│ ├── kvm_engine/ # V4L2 + WebRTC + HID + OCR 遮罩 +│ ├── web_ui/ # FastAPI 后端 + Vue3 前端 +│ └── config_server/ # 设备配置、用户管理、CA 证书生成 +├── shared/ # 符号链接或 submodule +│ ├── embed_db/ # → /data/rockchip/embedding +│ └── info_privacy/ # → /data/rockchip/info-privacy +├── scripts/ +│ ├── setup_gateway.sh # iptables + 双 NIC 路由配置 +│ ├── setup_usb_gadget.sh +│ └── install.sh # 一键安装脚本 +├── configs/ +│ ├── gateway.yaml # AI 路由 + 后端配置 +│ ├── ai_domains.txt # AI 域名白名单 +│ └── privacy_policy.yaml +├── tests/ +├── docs/ +│ └── plans/ +│ └── 2026-02-27-kvm-privacy-design.md +├── README.md +├── DEVELOP.md +└── RELEASE.md +``` + +--- + +## 8. 复用现有组件 + +| 组件 | 来源 | 复用方式 | +|------|------|---------| +| info-privacy | `/data/rockchip/info-privacy` | Privacy Gateway 调用其 REST API | +| mem_bridge(路由+记忆) | `/data/rockchip/embedding/src/mem_bridge` | 直接 import,新增 Qwen2 本地后端 | +| embed-db(向量索引) | `/data/rockchip/embedding/src/embed_db` | Memory 服务直接复用 | +| PaddleOCR RKNN 模型 | `/data/rockchip/paddle_ocr/models/` | KVM OCR 复用 | +| MediaPipe 人脸模型 | `/data/rockchip/mediapipe/models/rknn/` | info-privacy 已封装 | diff --git a/docs/plans/2026-02-28-kvm-privacy-devplan.md b/docs/plans/2026-02-28-kvm-privacy-devplan.md new file mode 100644 index 0000000..a9e327a --- /dev/null +++ b/docs/plans/2026-02-28-kvm-privacy-devplan.md @@ -0,0 +1,321 @@ +# KVM-Privacy 开发计划(基于设备调研) + +**日期**:2026-02-28 +**调研设备**:NanoPC-T6,IP 192.168.123.181,/home/pi/Desktop/KVM +**状态**:基于实际部署调研更新设计文档 + +--- + +## 一、设备现状调研结论 + +### 1.1 KVM 核心组件现状 + +| 组件 | 状态 | 说明 | +|------|------|------| +| kvm-server | ✅ 运行中(port 8080) | systemd 服务,运行 1.5+ 年,Go 单体二进制 | +| GStreamer 管道 | ✅ 运行中 | `/dev/video40` → RKMPP H.264 4K@30fps → RTP:5004 | +| kvm-ocr 二进制 | ✅ 已安装 | `/usr/bin/kvm-ocr`,含 RKNN NPU 支持 | +| OCR RKNN 模型 | ✅ 已存在 | `/etc/kvm/models/ocr/`(PP-OCRv4 中英文) | +| RKNPU 内核模块 | ✅ 已加载 | `rknpu` 模块,`/dev/dri/renderD128` 可访问 | +| HID 设备 | ✅ 正常 | `/dev/hidg0`(键盘) + `/dev/hidg1`(鼠标) | +| HDMI 接收 | ✅ 已连接 | `/dev/video40`(rk_hdmirx 驱动) | +| MariaDB | ✅ 运行中(port 3306) | GORM ORM,哈希链审计日志 | +| Web UI | ✅ 200 OK(port 8080) | React 18 + TypeScript,嵌入 Go 二进制 | +| eth0 | ✅ UP | 192.168.123.181/24,当前局域网接口 | +| eth1 | ❌ DOWN(无载波) | 物理未接线,网关模式暂不可用 | + +### 1.2 隐私组件现状 + +| 组件 | 状态 | 说明 | +|------|------|------| +| info-privacy | ✅ 运行中(port 8000) | 手动启动,uvicorn,`{"status":"ok"}` | +| embed-db | ✅ 代码已在设备 | `/home/pi/Desktop/embed-db/`,未作为服务运行 | +| Ollama | ❌ 未安装 | 本地 LLM 推理不可用 | +| mitmproxy 代理 | ❌ 未配置 | 隐私网关核心组件未构建 | +| mem_bridge | ❌ 未部署 | AI 路由层未运行 | + +### 1.3 已知问题 + +| 问题 | 影响 | 修复难度 | +|------|------|---------| +| `/tmp/kvm-ocr/` 目录不存在 | OCR 实际无法运行(GStreamer 帧写入失败) | 极低(一行命令) | +| info-privacy 非 systemd 管理 | 重启后不自动恢复 | 低(写 service 文件) | +| eth1 无物理连接 | 网关模式无法启用,当前只能做代理 | 等待硬件接线 | +| 磁盘 79% 已用(11GB 剩余) | 安装 Ollama+模型需规划 | 中(Q4量化约 1GB) | +| TODO_UNVERIFIED_FEATURES | OCR 多语言/剪贴板/OCR复制面板代码完成未测试 | 低-中 | + +### 1.4 磁盘评估 + +``` +当前:53GB 总量,40GB 已用,11GB 剩余 +需要安装: + Ollama binary: ~200MB + Qwen2-1.5B (Q4_K_M): ~1.0GB + mitmproxy: ~50MB + mem_bridge venv: ~200MB + 合计: ~1.5GB +剩余裕量: ~9.5GB ✅ +``` + +--- + +## 二、KVM 功能全景(已实现) + +KVM 服务已具备完整生产能力,无需重新开发: + +``` +认证层:JWT + TOTP + Casbin RBAC(4角色,22权限) +视频层:GStreamer → RKMPP MPP H.264/H.265 → RTP → Pion WebRTC → Browser +HID层:WebSocket → HID Dispatcher → /dev/hidg0(键盘) + /dev/hidg1(鼠标) +录像层:fMP4 分段(5min),HLS M3U8,关键帧索引,审计回放 +终端层:SSH/PTY → WebSocket → Browser xterm +AI层:kvm-ocr (RKNN NPU) → pattern_matcher → sensitive_commands → 审计告警 +审计层:MariaDB SHA-256 哈希链,syslog 外发支持,30天留存 +网络层:WireGuard(代码完整,未启用),TURN/STUN(coturn) +``` + +--- + +## 三、KVM-Privacy 开发计划 + +基于调研结论,将原设计 Phase 1-4 重新映射: + +### Phase 0 — 立即修复(当前 session 可完成) + +**目标**:修复现有 KVM 已知问题,为 privacy 扩展打好基础 + +#### P0.1 修复 OCR 目录缺失 + +```bash +# 在设备上执行 +sudo mkdir -p /tmp/kvm-ocr +sudo chmod 755 /tmp/kvm-ocr + +# 持久化:创建 tmpfiles.d 规则 +echo 'd /tmp/kvm-ocr 0755 root root -' | sudo tee /etc/tmpfiles.d/kvm-ocr.conf +``` + +**验证**:重启 kvm-server 后 OCR 快照文件出现在 `/tmp/kvm-ocr/` + +#### P0.2 将 info-privacy 纳入 systemd + +```ini +# /etc/systemd/system/info-privacy.service +[Unit] +Description=Info-Privacy PII Detection Service +After=network.target + +[Service] +Type=simple +User=pi +WorkingDirectory=/home/pi/Desktop/info-privacy +ExecStart=/home/pi/Desktop/info-privacy/venv/bin/uvicorn \ + info_privacy.api.main:app --host 0.0.0.0 --port 8000 +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +#### P0.3 验证 TODO_UNVERIFIED_FEATURES 关键项 + +按 `docs/TODO_UNVERIFIED_FEATURES.md` 逐一测试: +- [ ] OCR 快照功能(修复 P0.1 后验证) +- [ ] 剪贴板粘贴(ASCII 文本) +- [ ] 审计页自动刷新 + +--- + +### Phase 1 — 隐私网关 MVP(最高优先级) + +**目标**:HTTPS 透明代理拦截 AI 服务请求,自动 PII 脱敏 + +**依赖**:info-privacy 已运行(已满足) + +#### 架构 + +``` +用户浏览器 → 系统代理 → mitmproxy(:8888) + → AI 域名白名单匹配 + → 命中 → 解密 → info-privacy analyze → redact → 放行脱敏请求 + → 未命中 → 直接 BYPASS + → 审计日志(无原文) +``` + +#### 交付物 + +1. **`services/privacy_gateway/`** + - `addon.py` — mitmproxy 核心 addon(AI 域名过滤 + info-privacy 调用) + - `audit_logger.py` — SQLite append-only 审计写入 + - `cert_manager.py` — 设备 CA 证书生成与管理 + - `ai_domains.txt` — AI 域名白名单 + +2. **KVM Web UI 扩展**(在现有 React 项目中新增页面) + - `/privacy` 路由:今日拦截统计、审计日志、域名管理 + - 首次使用向导:证书下载 + 安装引导(3步,<3分钟) + +3. **新增 Go API 端点**(在 `go/internal/api/` 扩展) + - `GET /api/v1/privacy/stats` — 拦截统计 + - `GET /api/v1/privacy/audit` — 审计日志 + - `POST /api/v1/privacy/domains` — 域名白名单管理 + - `GET /api/v1/privacy/cert` — 下载 CA 证书 + +4. **`scripts/setup_privacy_gateway.sh`** + - 安装 mitmproxy + - 配置系统代理提示(eth1 未接线时退化为手动代理模式) + +#### 注意事项 + +- **eth1 未接线**:当前只能使用手动代理模式(用户配置系统代理) +- **eth1 接线后**:追加 iptables REDIRECT 规则,切换为透明代理(对用户完全无感) +- **文档上传处理**:info-privacy `analyze` + `redact`,处理后内容不落盘 + +--- + +### Phase 2 — AI 路由层 + +**目标**:本地 Qwen2-1.5B 处理轻量任务,复杂任务路由云端 + +#### 交付物 + +1. **Ollama 安装 + Qwen2-1.5B** + ```bash + curl -fsSL https://ollama.com/install.sh | sh + ollama pull qwen2:1.5b # ~1GB Q4量化 + ``` + +2. **mem_bridge 服务化**(复用 `/home/pi/Desktop/embed-db/src/mem_bridge/`) + ```yaml + # config: 新增 qwen_local 后端 + backends: + default: qwen_local # 本地 Qwen2-1.5B + heavy: claude_sonnet # 复杂任务云端 + qwen_local: + type: compatible + base_url: http://localhost:11434/v1 + model: qwen2:1.5b + ``` + +3. **KVM Web UI 扩展** + - `/chat` 路由:AI 对话页,文件上传自动脱敏 → 预览 → 确认发送 + +4. **systemd 服务** + - `mem-bridge-memory.service`(port 8001) + - `mem-bridge-router.service`(port 8002) + +--- + +### Phase 3 — KVM 视频 PII 遮罩 + +**目标**:OCR 检测屏幕 PII → 生成遮罩坐标 → 叠加到 WebRTC 视频流 + +**现状**:kvm-ocr 已实现敏感命令检测(告警),需扩展为 PII 实体遮罩 + +#### 架构变化 + +``` +当前:kvm-ocr 输出 → pattern_matcher → audit log(告警) +新增:kvm-ocr 输出(含 bbox)→ PII matcher → mask_coords → WebRTC overlay +``` + +#### 交付物 + +1. **`go/internal/ai/pii_masker.go`**(新文件) + - 读取 kvm-ocr 输出的 bbox 坐标 + - 匹配 PII 模式(复用 info-privacy 规则集) + - 维护遮罩坐标缓存(TTL = 下次 OCR 触发) + +2. **WebRTC 视频叠加**(`go/internal/webrtc/server.go` 扩展) + - 在向浏览器发送视频帧前叠加黑色矩形 + - 叠加在服务端(防止客户端绕过) + +3. **Web UI** + - KVM 控制台新增:遮罩开关 + 被遮区域类型提示 + +#### 计划工作(Phase 3+) + +- 不同远端用户的视图权限控制(防展示给无权用户) +- KVM 会话审计录像(仅存元数据和事件时间线,不存原视频) + +--- + +### Phase 4 — 生产加固 + +1. **eth1 网关配置**(接线后执行) + ```bash + # iptables 透明代理规则 + iptables -t nat -A PREROUTING -i eth0 \ + -p tcp --dport 443 -j REDIRECT --to-port 8888 + ``` + +2. **多用户账户管理**:扩展现有 RBAC,增加 privacy 权限角色 + +3. **性能调优**:NPU 三核并行(`NPU_CORE_ALL`),OCR 批处理 + +4. **设备出厂配置**:`scripts/install.sh` 一键部署所有服务 + +--- + +## 四、项目目录结构(目标) + +``` +KVM-privacy/ +├── services/ +│ ├── privacy_gateway/ # Phase 1: mitmproxy addon +│ │ ├── addon.py # 核心拦截逻辑 +│ │ ├── audit_logger.py # SQLite 审计 +│ │ ├── cert_manager.py # CA 证书管理 +│ │ └── ai_domains.txt # AI 域名白名单 +│ └── config_server/ # 设备配置 API(扩展现有 KVM API) +├── docs/ +│ └── plans/ +│ ├── 2026-02-27-kvm-privacy-design.md # 原始设计 +│ └── 2026-02-28-kvm-privacy-devplan.md # 本文档 +├── scripts/ +│ ├── setup_privacy_gateway.sh # mitmproxy + 证书 + 代理配置 +│ └── install_ollama.sh # Ollama + Qwen2-1.5B +├── README.md +├── DEVELOP.md +└── RELEASE.md +``` + +**复用路径(设备上)**: +- info-privacy: `/home/pi/Desktop/info-privacy/`(Port 8000,已运行) +- embed-db/mem_bridge: `/home/pi/Desktop/embed-db/src/mem_bridge/` +- kvm-server: `/home/pi/Desktop/KVM/`(Port 8080,已运行) + +--- + +## 五、立即可行动项(Phase 0) + +```bash +# 在 192.168.123.181 上执行: + +# 1. 修复 OCR 目录 +sudo mkdir -p /tmp/kvm-ocr && sudo chmod 755 /tmp/kvm-ocr +echo 'd /tmp/kvm-ocr 0755 root root -' | sudo tee /etc/tmpfiles.d/kvm-ocr.conf + +# 2. 重启 kvm-server 让 GStreamer 重建管道 +sudo systemctl restart kvm-server + +# 3. 验证 OCR 帧出现 +sleep 5 && ls -la /tmp/kvm-ocr/ + +# 4. info-privacy 纳入 systemd +sudo tee /etc/systemd/system/info-privacy.service << 'EOF' +[Unit] +Description=Info-Privacy PII Detection Service +After=network.target +[Service] +Type=simple +User=pi +WorkingDirectory=/home/pi/Desktop/info-privacy +ExecStart=/home/pi/Desktop/info-privacy/venv/bin/uvicorn info_privacy.api.main:app --host 0.0.0.0 --port 8000 +Restart=on-failure +RestartSec=5 +[Install] +WantedBy=multi-user.target +EOF +sudo systemctl daemon-reload && sudo systemctl enable info-privacy +``` diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..80bc7db --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# install.sh — KVM-Privacy Hub 一键安装脚本 +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "========================================" +echo " KVM-Privacy Hub 一键安装" +echo "========================================" + +# 可选:跳过某些步骤 +SKIP_OLLAMA="${SKIP_OLLAMA:-false}" + +echo "" +echo "步骤 1/2: 安装 Privacy Gateway (mitmproxy + CA 证书 + systemd)" +bash "${SCRIPT_DIR}/setup_gateway.sh" + +if [ "${SKIP_OLLAMA}" != "true" ]; then + echo "" + echo "步骤 2/2: 安装 Ollama + Qwen2-1.5B" + bash "${SCRIPT_DIR}/install_ollama.sh" +else + echo "步骤 2/2: 跳过 Ollama 安装 (SKIP_OLLAMA=true)" +fi + +echo "" +echo "========================================" +echo " 安装完成!" +echo "========================================" +echo "" +echo "下一步:" +echo " 1. 在浏览器安装 CA 证书: curl http://\${DEVICE_IP}:8080/api/v1/privacy/cert" +echo " 2. 配置系统代理: \${DEVICE_IP}:8888" +echo " 3. 打开 KVM Web UI → Privacy 页面查看实时统计" diff --git a/scripts/install_ollama.sh b/scripts/install_ollama.sh new file mode 100755 index 0000000..48f92a2 --- /dev/null +++ b/scripts/install_ollama.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# install_ollama.sh — 在 RK3588 设备上安装 Ollama 并拉取 Qwen2-1.5B +set -euo pipefail + +DEVICE_IP="${DEVICE_IP:-192.168.123.181}" +DEVICE_USER="${DEVICE_USER:-pi}" + +echo "=== [1/3] 安装 Ollama ===" +ssh "${DEVICE_USER}@${DEVICE_IP}" "curl -fsSL https://ollama.com/install.sh | sh" + +echo "=== [2/3] 启动 Ollama 服务 ===" +ssh "${DEVICE_USER}@${DEVICE_IP}" "echo '123123' | sudo -S systemctl enable --now ollama" +sleep 5 # 等待 Ollama 启动 + +echo "=== [3/3] 拉取 Qwen2-1.5B (约 1GB Q4_K_M) ===" +ssh "${DEVICE_USER}@${DEVICE_IP}" "ollama pull qwen2:1.5b" + +echo "=== 完成!验证 ===" +ssh "${DEVICE_USER}@${DEVICE_IP}" "ollama list" +echo "Ollama API 测试:" +ssh "${DEVICE_USER}@${DEVICE_IP}" "curl -s http://localhost:11434/v1/models | python3 -m json.tool 2>/dev/null | head -10" diff --git a/scripts/setup_gateway.sh b/scripts/setup_gateway.sh new file mode 100755 index 0000000..710efa3 --- /dev/null +++ b/scripts/setup_gateway.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# setup_gateway.sh — 安装 Privacy Gateway (mitmproxy + CA 证书 + systemd) +set -euo pipefail + +DEVICE_IP="${DEVICE_IP:-192.168.123.181}" +DEVICE_USER="${DEVICE_USER:-pi}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOY_DIR="${SCRIPT_DIR}/../deploy/systemd" +GATEWAY_DIR="${SCRIPT_DIR}/../services/privacy_gateway" + +echo "=== [1/4] 安装 mitmproxy ===" +ssh "${DEVICE_USER}@${DEVICE_IP}" "pip3 install mitmproxy httpx cryptography 2>&1 | tail -5" + +echo "=== [2/4] 上传 Privacy Gateway 服务文件 ===" +ssh "${DEVICE_USER}@${DEVICE_IP}" "mkdir -p /data/project/KVM-privacy/services/privacy_gateway" +scp "${GATEWAY_DIR}"/*.py "${DEVICE_USER}@${DEVICE_IP}:/data/project/KVM-privacy/services/privacy_gateway/" +scp "${GATEWAY_DIR}/ai_domains.txt" "${DEVICE_USER}@${DEVICE_IP}:/data/project/KVM-privacy/services/privacy_gateway/" + +echo "=== [3/4] 生成 CA 证书 ===" +ssh "${DEVICE_USER}@${DEVICE_IP}" "python3 -c ' +import sys; sys.path.insert(0, \"/data/project/KVM-privacy/services/privacy_gateway\") +from cert_manager import ensure_ca +cert, key = ensure_ca() +print(f\"CA 证书: {cert}\") +'" + +echo "=== [4/4] 安装 systemd 服务 ===" +for svc in info-privacy mem-bridge-memory mem-bridge-router privacy-gateway; do + if [ -f "${DEPLOY_DIR}/${svc}.service" ]; then + scp "${DEPLOY_DIR}/${svc}.service" "${DEVICE_USER}@${DEVICE_IP}:/tmp/${svc}.service" + ssh "${DEVICE_USER}@${DEVICE_IP}" "echo '123123' | sudo -S cp /tmp/${svc}.service /etc/systemd/system/${svc}.service" + fi +done +ssh "${DEVICE_USER}@${DEVICE_IP}" "echo '123123' | sudo -S systemctl daemon-reload" + +echo "=== 完成!===" +echo "启用服务: ssh ${DEVICE_USER}@${DEVICE_IP} 'sudo systemctl enable --now privacy-gateway'" +echo "下载 CA 证书: curl http://${DEVICE_IP}:8080/api/v1/privacy/cert -H 'Authorization: Bearer ' -o kvm-ca.crt" diff --git a/services/privacy_gateway/addon.py b/services/privacy_gateway/addon.py new file mode 100644 index 0000000..d33bce4 --- /dev/null +++ b/services/privacy_gateway/addon.py @@ -0,0 +1,100 @@ +"""KVM Privacy Gateway —— mitmproxy addon 核心。 + +工作流程: + 1. 检查请求域名是否在 AI 白名单中 + 2. 若是,且请求包含文件上传: + a. 提取文件字段 + b. 调用 upload_scanner 做 PII 检测 + 脱敏 + c. 脱敏后字节替换原始请求体 + 3. 记录审计日志(不存原文,只存 PII 类型计数和文件哈希) + 4. 放行(透明代理,用户感知不到拦截) +""" +from __future__ import annotations + +import asyncio +import logging +from pathlib import Path + +from mitmproxy import ctx, http + +from audit_logger import AuditLogger +from interceptor import extract_files, is_file_upload, rebuild_multipart +from upload_scanner import scan_and_redact + +logger = logging.getLogger(__name__) + +_DOMAINS_FILE = Path(__file__).parent / "ai_domains.txt" +_audit = AuditLogger() + + +def _load_domains() -> set[str]: + """从文件加载 AI 域名白名单(过滤注释行)。""" + if not _DOMAINS_FILE.exists(): + return set() + return { + line.strip() + for line in _DOMAINS_FILE.read_text().splitlines() + if line.strip() and not line.startswith("#") + } + + +_AI_DOMAINS: set[str] = _load_domains() + + +class PrivacyGatewayAddon: + """mitmproxy addon:拦截上传到 AI 服务的附件并自动脱敏。""" + + def request(self, flow: http.HTTPFlow) -> None: + host = flow.request.pretty_host + if host not in _AI_DOMAINS: + return # 非 AI 域名,直接放行 + + if not is_file_upload(flow): + return # 非文件上传,直接放行 + + files = extract_files(flow) + if not files: + return + + ctx.log.info(f"[privacy-gw] 拦截 {host} 上传 {len(files)} 个文件") + + # 异步扫描在 mitmproxy 的事件循环中运行 + asyncio.ensure_future(self._process_upload(flow, host, files)) + + async def _process_upload(self, flow, host: str, files) -> None: + replacements: dict[str, bytes] = {} + total_pii: dict[str, int] = {} + total_files = len(files) + + for f in files: + try: + result = await scan_and_redact(f.filename, f.data) + except Exception as e: + ctx.log.warn(f"[privacy-gw] 扫描失败 {f.filename}: {e}") + _audit.log(host, {}, "bypass", f.data, total_files) + continue + + # 合并 PII 统计 + for pii_type, count in result.pii_types.items(): + total_pii[pii_type] = total_pii.get(pii_type, 0) + count + + if result.pii_found and result.redacted_bytes: + replacements[f.field_name] = result.redacted_bytes + ctx.log.info( + f"[privacy-gw] 自动脱敏 {f.filename}: {result.pii_types}" + ) + + _audit.log( + host, + result.pii_types, + "auto_redact" if result.redacted_bytes else "bypass", + f.data, + total_files, + ) + + if replacements: + flow.request.content = rebuild_multipart(flow, replacements) + ctx.log.info(f"[privacy-gw] 请求已脱敏,放行至 {host}") + + +addons = [PrivacyGatewayAddon()] diff --git a/services/privacy_gateway/ai_domains.txt b/services/privacy_gateway/ai_domains.txt new file mode 100644 index 0000000..da2e2c6 --- /dev/null +++ b/services/privacy_gateway/ai_domains.txt @@ -0,0 +1,10 @@ +# AI 服务域名白名单 —— 仅拦截这些域名的 HTTPS 流量进行 PII 扫描 +# 每行一个域名;以 # 开头的行为注释 +api.anthropic.com +api.openai.com +generativelanguage.googleapis.com +dashscope.aliyuncs.com +api.cohere.com +api.mistral.ai +api.together.xyz +api.groq.com diff --git a/services/privacy_gateway/audit_logger.py b/services/privacy_gateway/audit_logger.py new file mode 100644 index 0000000..08c3b35 --- /dev/null +++ b/services/privacy_gateway/audit_logger.py @@ -0,0 +1,85 @@ +"""审计日志 —— SQLite append-only,只记 PII 类型/哈希,不存原文。""" +from __future__ import annotations + +import hashlib +import json +import sqlite3 +import threading +from datetime import datetime +from pathlib import Path + +_DB_PATH = Path("/var/lib/kvm-privacy/audit.db") +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts TEXT NOT NULL, + domain TEXT NOT NULL, + pii_types TEXT, -- JSON {"id_card":2,"phone":1} + action TEXT NOT NULL, -- auto_redact | blocked | bypass + doc_hash TEXT, -- sha256(original_bytes) — 不存原文 + file_count INTEGER DEFAULT 0 +); +CREATE INDEX IF NOT EXISTS idx_ts ON audit_log(ts); +CREATE INDEX IF NOT EXISTS idx_domain ON audit_log(domain); +""" + + +class AuditLogger: + def __init__(self, db_path: Path = _DB_PATH) -> None: + db_path.parent.mkdir(parents=True, exist_ok=True) + self._db_path = str(db_path) + self._lock = threading.Lock() + with self._connect() as conn: + conn.executescript(_SCHEMA) + + def _connect(self) -> sqlite3.Connection: + conn = sqlite3.connect(self._db_path, check_same_thread=False) + conn.row_factory = sqlite3.Row + return conn + + def log( + self, + domain: str, + pii_types: dict[str, int], + action: str, + raw_bytes: bytes | None = None, + file_count: int = 0, + ) -> None: + doc_hash = hashlib.sha256(raw_bytes).hexdigest() if raw_bytes else None + with self._lock, self._connect() as conn: + conn.execute( + "INSERT INTO audit_log(ts, domain, pii_types, action, doc_hash, file_count) " + "VALUES (?,?,?,?,?,?)", + ( + datetime.utcnow().isoformat(), + domain, + json.dumps(pii_types) if pii_types else None, + action, + doc_hash, + file_count, + ), + ) + + def query( + self, limit: int = 50, offset: int = 0, domain: str | None = None + ) -> list[dict]: + sql = "SELECT * FROM audit_log" + params: list = [] + if domain: + sql += " WHERE domain = ?" + params.append(domain) + sql += " ORDER BY id DESC LIMIT ? OFFSET ?" + params.extend([limit, offset]) + with self._connect() as conn: + rows = conn.execute(sql, params).fetchall() + return [dict(r) for r in rows] + + def stats_today(self) -> dict: + today = datetime.utcnow().date().isoformat() + with self._connect() as conn: + row = conn.execute( + "SELECT COUNT(*) AS requests, SUM(file_count) AS files " + "FROM audit_log WHERE ts >= ?", + (today,), + ).fetchone() + return {"requests": row["requests"] or 0, "files": row["files"] or 0} diff --git a/services/privacy_gateway/cert_manager.py b/services/privacy_gateway/cert_manager.py new file mode 100644 index 0000000..0b2b9e2 --- /dev/null +++ b/services/privacy_gateway/cert_manager.py @@ -0,0 +1,78 @@ +"""CA 证书管理 —— 生成设备级自签名 CA,供 mitmproxy 使用。""" +from __future__ import annotations + +import datetime +from pathlib import Path + +_CA_DIR = Path("/etc/kvm-privacy/ca") +_CA_KEY = _CA_DIR / "ca.key" +_CA_CERT = _CA_DIR / "ca.crt" +_CA_P12 = _CA_DIR / "ca.p12" + + +def ensure_ca() -> tuple[Path, Path]: + """确保 CA 证书存在,如不存在则生成。返回 (cert_path, key_path)。""" + _CA_DIR.mkdir(parents=True, exist_ok=True) + + if _CA_CERT.exists() and _CA_KEY.exists(): + return _CA_CERT, _CA_KEY + + try: + from cryptography import x509 + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.x509.oid import NameOID + except ImportError: + raise RuntimeError( + "缺少 cryptography 包,请运行: pip install cryptography" + ) + + # 生成 RSA-4096 私钥 + key = rsa.generate_private_key(public_exponent=65537, key_size=4096) + + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "KVM-Privacy Hub"), + x509.NameAttribute(NameOID.COMMON_NAME, "KVM-Privacy Root CA"), + ]) + + now = datetime.datetime.utcnow() + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now) + .not_valid_after(now + datetime.timedelta(days=3650)) # 10 年 + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .add_extension( + x509.KeyUsage( + digital_signature=True, key_cert_sign=True, crl_sign=True, + content_commitment=False, key_encipherment=False, + data_encipherment=False, key_agreement=False, + encipher_only=False, decipher_only=False, + ), + critical=True, + ) + .sign(key, hashes.SHA256()) + ) + + _CA_KEY.write_bytes( + key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.NoEncryption(), + ) + ) + _CA_KEY.chmod(0o600) + _CA_CERT.write_bytes(cert.public_bytes(serialization.Encoding.PEM)) + + print(f"[cert_manager] CA 证书已生成: {_CA_CERT}") + return _CA_CERT, _CA_KEY + + +def get_ca_pem() -> bytes: + """返回 CA 证书 PEM 字节,供用户下载安装到系统信任库。""" + cert, _ = ensure_ca() + return cert.read_bytes() diff --git a/services/privacy_gateway/interceptor.py b/services/privacy_gateway/interceptor.py new file mode 100644 index 0000000..5ea8fbd --- /dev/null +++ b/services/privacy_gateway/interceptor.py @@ -0,0 +1,104 @@ +"""请求拦截逻辑 —— 提取多部分上传中的文件,触发 PII 扫描,替换后放行。""" +from __future__ import annotations + +import email.parser +import io +import re +from typing import NamedTuple + +from mitmproxy.http import HTTPFlow + + +class ExtractedFile(NamedTuple): + field_name: str + filename: str + content_type: str + data: bytes + + +# 常见 AI API 文件上传 Content-Type +_MULTIPART_RE = re.compile(r"multipart/form-data;\s*boundary=(.+)", re.IGNORECASE) +_FILE_EXTS = {".pdf", ".doc", ".docx", ".txt", ".png", ".jpg", ".jpeg", ".csv"} + + +def is_file_upload(flow: HTTPFlow) -> bool: + """判断请求是否包含文件上传。""" + ct = flow.request.headers.get("content-type", "") + return bool(_MULTIPART_RE.match(ct)) + + +def extract_files(flow: HTTPFlow) -> list[ExtractedFile]: + """从 multipart 请求体中提取文件字段。""" + ct = flow.request.headers.get("content-type", "") + m = _MULTIPART_RE.match(ct) + if not m: + return [] + + boundary = m.group(1).strip().strip('"') + # 构造标准 MIME multipart 消息供 email.parser 解析 + msg_bytes = ( + f"Content-Type: multipart/form-data; boundary={boundary}\r\n\r\n".encode() + + flow.request.content + ) + parser = email.parser.BytesParser() + msg = parser.parsebytes(msg_bytes) + + files: list[ExtractedFile] = [] + for part in msg.get_payload(): # type: ignore[union-attr] + disposition = part.get("Content-Disposition", "") + if "filename=" not in disposition: + continue # 跳过非文件字段 + fn_match = re.search(r'filename="?([^";]+)"?', disposition) + fn_match_field = re.search(r'name="?([^";]+)"?', disposition) + filename = fn_match.group(1) if fn_match else "unknown" + field_name = fn_match_field.group(1) if fn_match_field else "file" + _, ext = filename.rsplit(".", 1) if "." in filename else (filename, "") + if f".{ext.lower()}" not in _FILE_EXTS: + continue # 只处理已知文件类型 + files.append(ExtractedFile( + field_name=field_name, + filename=filename, + content_type=part.get_content_type(), + data=part.get_payload(decode=True) or b"", + )) + return files + + +def rebuild_multipart(flow: HTTPFlow, replacements: dict[str, bytes]) -> bytes: + """将 replacements 中的文件字段替换后重建 multipart 请求体。 + + replacements: {field_name: new_file_bytes} + """ + ct = flow.request.headers.get("content-type", "") + m = _MULTIPART_RE.match(ct) + if not m: + return flow.request.content + + boundary = m.group(1).strip().strip('"') + msg_bytes = ( + f"Content-Type: multipart/form-data; boundary={boundary}\r\n\r\n".encode() + + flow.request.content + ) + parser = email.parser.BytesParser() + msg = parser.parsebytes(msg_bytes) + + parts_out: list[bytes] = [] + for part in msg.get_payload(): # type: ignore[union-attr] + disposition = part.get("Content-Disposition", "") + fn_match_field = re.search(r'name="?([^";]+)"?', disposition) + field_name = fn_match_field.group(1) if fn_match_field else "" + if field_name in replacements: + # 替换文件内容,保留其他头部 + new_data = replacements[field_name] + out = io.BytesIO() + for key, val in part.items(): + out.write(f"{key}: {val}\r\n".encode()) + out.write(b"\r\n") + out.write(new_data) + parts_out.append(out.getvalue()) + else: + parts_out.append(part.as_bytes()) + + sep = f"--{boundary}\r\n".encode() + end = f"--{boundary}--\r\n".encode() + return sep + (b"\r\n" + sep).join(parts_out) + b"\r\n" + end diff --git a/services/privacy_gateway/upload_scanner.py b/services/privacy_gateway/upload_scanner.py new file mode 100644 index 0000000..b301b46 --- /dev/null +++ b/services/privacy_gateway/upload_scanner.py @@ -0,0 +1,81 @@ +"""上传扫描器 —— 调用 info-privacy API 对文件内容进行 PII 检测和脱敏。 + +内存处理原则: + - 原始文件字节在内存中传递,不写磁盘(analyze/frame 端点接受 multipart 上传) + - 脱敏后字节替换原始请求体,原始字节仅用于 sha256 哈希计算 +""" +from __future__ import annotations + +import hashlib +import io +from dataclasses import dataclass + +import httpx + +_INFO_PRIVACY_BASE = "http://localhost:8000/api/v1" +_REDACT_TYPES = ["id_card", "phone", "bank_card", "email", "license_plate", "name", "address", "face"] + + +@dataclass +class ScanResult: + pii_found: bool + pii_types: dict[str, int] # e.g. {"id_card": 2, "phone": 1} + redacted_bytes: bytes | None # None if no PII found + original_hash: str # sha256 of original bytes + + +async def scan_and_redact(filename: str, raw_bytes: bytes) -> ScanResult: + """对上传文件进行 PII 扫描,如有 PII 则脱敏并返回脱敏后字节。 + + 步骤: + 1. 调用 /api/v1/analyze 检测 PII 类型和位置 + 2. 如有 PII,调用 /api/v1/redact 获取脱敏文件 + 3. 返回脱敏后字节(内存处理,不落盘) + """ + original_hash = hashlib.sha256(raw_bytes).hexdigest() + + async with httpx.AsyncClient(timeout=30.0) as client: + # Step 1: 分析 PII + analyze_resp = await client.post( + f"{_INFO_PRIVACY_BASE}/analyze", + files={"file": (filename, io.BytesIO(raw_bytes))}, + ) + if analyze_resp.status_code != 200: + return ScanResult( + pii_found=False, + pii_types={}, + redacted_bytes=None, + original_hash=original_hash, + ) + + report = analyze_resp.json() + summary: dict[str, int] = report.get("summary", {}) + + if not summary or report.get("blocked"): + return ScanResult( + pii_found=bool(summary), + pii_types=summary, + redacted_bytes=None, + original_hash=original_hash, + ) + + # Step 2: 脱敏 + redact_resp = await client.post( + f"{_INFO_PRIVACY_BASE}/redact", + files={"file": (filename, io.BytesIO(raw_bytes))}, + data={"redact_types": list(summary.keys())}, + ) + if redact_resp.status_code != 200: + return ScanResult( + pii_found=True, + pii_types=summary, + redacted_bytes=None, + original_hash=original_hash, + ) + + return ScanResult( + pii_found=True, + pii_types=summary, + redacted_bytes=redact_resp.content, + original_hash=original_hash, + )