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
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.db
|
||||
/deploy/systemd/*.service.override
|
||||
/services/privacy_gateway/__pycache__/
|
||||
@@ -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=<name> 查看指定服务日志"
|
||||
@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
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
# /etc/tmpfiles.d/kvm-ocr.conf
|
||||
# 设备重启后自动重建 OCR 快照目录
|
||||
# 格式: type path mode user group age
|
||||
d /tmp/kvm-ocr 0755 root root -
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 已封装 |
|
||||
@@ -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
|
||||
```
|
||||
Executable
+34
@@ -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 页面查看实时统计"
|
||||
Executable
+21
@@ -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"
|
||||
Executable
+38
@@ -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 <token>' -o kvm-ca.crt"
|
||||
@@ -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()]
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user