部署:bubblewrap 沙箱、编译检查、访问口令、Docker Compose

- 学生程序每次运行放进 bubblewrap:断网,只读挂载系统文件、运行时和自己的工程目录,
  看不到其他进程和服务器环境变量;限制堆内存 128 MB、CPU 时间 120 秒、运行 60 分钟,
  nice 10,全站同时运行上限 150;启动时自检沙箱,不可用时拒绝运行
- 编译检查(SIM001):网络、反射、启动进程、调用本机代码、加载程序集、dynamic 给出中文报错
- 访问口令:cookie 保存口令哈希,按真实 IP 限制输错次数;前端口令页
- 服务端配置集中到 config.ts,可用环境变量覆盖;Bun 直接提供打包好的网页
- Dockerfile(多阶段,服务端打包成单文件)、compose.yaml(Caddy 只收 HTTP,接入 NPM 的
  npm_proxy 网络)、deploy/deploy.sh(本机构建后经 ssh 传到服务器)和部署说明
- 测试:沙箱隔离、内存上限、CPU 时间上限、编译检查

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 15:40:09 +08:00
1 parent 081ac6f924
commit 697f31e9ce
25 files changed
+869 -52

No files matched your search

+21 -16
View File
@@ -29,7 +29,7 @@
| 学生代码在哪运行 | 服务器(沙箱隔离) | 只供本校一两个班同时使用;原生 .NET 多线程行为真实;浏览器轻量 |
| 前端 | Vue 3 + Naive UI + Chart.js | 开发者熟悉 TypeScript 生态 |
| 后端 | Bun + SQLite | 一人开发,单机足够 |
| 部署 | 公网云服务器 | 需办 ICP 备案 |
| 部署 | 公网云服务器(2 核 8G,已备案);HTTPS 由服务器上已有的 Nginx Proxy Manager 负责 | 本平台只在 Docker 网络里暴露 HTTP |
| 开发人力 | 一人 | 范围和路线图按此控制 |
## 学生使用体验
@@ -113,7 +113,7 @@ flowchart TB
BoardState["实训台状态<br/>传感器输入、继电器输出、LED 文字"]
Data["账号、班级、实验、评测(SQLite)"]
end
subgraph Runtime[".NET 10 运行时(部署时放进 Docker 沙箱)"]
subgraph Runtime[".NET 10 运行时(运行进程在 bubblewrap 沙箱里)"]
Compiler["编译服务(Roslyn)<br/>全服务器共用一个常驻进程"]
Runner["运行进程(每次运行一个)<br/>仿真 WinForms + 教材设备类库"]
end
@@ -170,28 +170,31 @@ IOT/
| C# 运行时 | 代码补全 | 计划:Roslyn `CompletionService` |
| 工程 | 仓库 | Bun workspaces 单仓库 |
| 工程 | 测试 | `bun test`(端到端、单元、压力测试);计划:用教材参考工程做回归测试、Playwright |
| 部署 | 服务器 | 一台 Linux 云服务器,4 核 8 GB 起步 |
| 部署 | 沙箱 | 计划:Docker(断网、只读、限制资源) |
| 部署 | HTTPS / 静态文件 | 计划:Caddy |
| 部署 | 进程管理 | 计划:Docker Compose |
| 部署 | 服务器 | 一台 Linux 云服务器,2 核 8 GB |
| 部署 | 沙箱 | bubblewrap:每次运行一个独立命名空间(断网、只读、看不到其他进程),加 .NET 堆上限、CPU 时间上限 |
| 部署 | HTTPS | Nginx Proxy Manager(服务器上已有),经外部网络 `npm_proxy` 转发 |
| 部署 | 反向代理 / 压缩 | Caddy(只收 HTTP),网页文件由 Bun 直接提供 |
| 部署 | 进程管理 | Docker Compose;镜像在开发机构建后经 ssh 传到服务器(`deploy/deploy.sh`) |
| 部署 | 备份 | 计划:定时 `sqlite3 .backup` + 异地拷贝 |
## 安全与服务器资源
在公网上运行学生代码,必须做三层防护;第 0 阶段实测,100 个学生程序同时运行合计约 1.1 GB 内存。
**安全措施**(部署前完成)
**安全措施**(1、2 已完成;3 在第 2 阶段之前先用访问口令代替)
1. **沙箱隔离**:每次运行放进 Docker 容器,禁止联网、文件系统只读,限制内存(如 128 MB)、CPU、进程数和运行时长。
2. **编译时白名单**:只允许使用白名单内的命名空间;用到文件读写、`Process`、反射等时,直接给出中文报错。
3. **账号管控**:不开放自由注册,由老师用班级码或批量导入创建账号。
1. **沙箱隔离**:每次运行用 bubblewrap 放进独立的命名空间:断网,只能看到系统只读文件、运行时程序和自己的工程目录(只读),/tmp 是独立内存盘,看不到其他进程,不继承服务器的环境变量。另外限制托管堆 128 MB、累计 CPU 时间 120 秒(防死循环占满 CPU)、运行时长 60 分钟,降低优先级(nice 10),全站同时运行上限 150 个。整个平台再放在 Docker 容器里:文件系统只读、去掉全部 capabilities、限制内存和进程数。服务器启动时自检沙箱,不可用时拒绝运行学生代码。
- 没有选"每次运行一个 Docker 容器":每次多 0.3–0.5 秒,而且应用要能操作 Docker 守护进程,这本身权限过大。代价是应用容器要放开 seccomp / AppArmor,才能让 bubblewrap 创建命名空间。
2. **编译检查**:网络(System.Net)、反射(System.Reflection,AssemblyInfo.cs 里的特性除外)、启动其他程序(Process)、调用本机代码(DllImport、Marshal)、加载程序集、进程间通信、dynamic,编译时直接给出中文报错(SIM001)。文件读写由沙箱限制,不在编译时禁止(项目 7、8 要读图片)。
3. **账号管控**:不开放自由注册,由老师用班级码或批量导入创建账号(第 2 阶段)。在此之前,全站用一个访问口令(`.env` 里设置,换口令后所有人要重新输入),按 IP 限制输错次数。
**资源估算**
- 编译器做成常驻进程,全服务器共用一个,约占 150 MB;学生程序运行时不加载编译器。
- 每个运行中的学生程序平均约 11 MB(按 PSS 计,共享的 .NET 运行库按进程数平摊;单看 RSS 约 47 MB)。
- 每个学生同时只运行一个程序,闲置超时自动结束。
- 两个班约 100 人同时运行,合计约 1.3 GB;4 核 8 GB 的服务器有充足余量。加上 Docker 沙箱后需要复测。
- 两个班约 100 人同时运行,合计约 1.3 GB;8 GB 内存有充足余量。
- 2026-10-22 复测(Docker 容器限制为 2 核,加 bubblewrap 沙箱):50 人同一瞬间点"运行",4.4 秒内全部显示窗体(中位 2.3 秒),容器共 643 MB;100 人 8.4 秒(中位 4.1 秒),1.1 GB。瓶颈是 CPU(编译和启动),实际上课时点击会分散开。
- SQLite 开启 WAL 模式,定时备份。
## 实验规划
@@ -227,7 +230,7 @@ IOT/
```mermaid
flowchart TB
P0["阶段 0 · 技术验证(已完成 2026-10-22)<br/>教材项目 2 任务 1 原样运行;实测编译、延迟、内存"]
P1["阶段 1 · MVP(进行中,约 3 个月)<br/>虚拟实训台 ✔、代码编辑器 ✔、窗体设计器 ✔、多窗体 ✔<br/>教材示例:项目 1–3 全部 ✔,项目 4–5 中平台已支持的 3 个任务 ✔"]
P1["阶段 1 · MVP(进行中,约 3 个月)<br/>虚拟实训台 ✔、代码编辑器 ✔、窗体设计器 ✔、多窗体 ✔<br/>教材示例:项目 1–3 全部 ✔,项目 4–5 中平台已支持的 3 个任务 ✔<br/>部署(沙箱、访问口令、Docker Compose)✔"]
P2["阶段 2 · 教学闭环(约 2 个月)<br/>班级码登录、提交作业、自动评测、老师看板、成绩导出;一个班试用"]
P3["阶段 3 · 扩展(下学期)<br/>教材项目 4–8:摄像头、线程、SQLite 模拟数据库、图表、虚拟时钟"]
P4["阶段 4 · 进阶(之后)<br/>校园街景场景、故障注入题库、导出 VS 工程"]
@@ -249,7 +252,7 @@ flowchart TB
| 100 个程序同时运行 | 合计约 1.1 GB,平均每个 11 MB,另加编译服务约 150 MB |
| 100 个程序同时点击 | 往返中位数约 60 ms,P95 约 75 ms |
验证中的调整:第一版每个运行进程都加载 Roslyn,单个约 170 MB;改为常驻编译服务后,运行进程不再加载编译器,编译也从约 0.9 s 降到几十毫秒。沙箱尚未接入,以上数字需在接入 Docker 后复测。
验证中的调整:第一版每个运行进程都加载 Roslyn,单个约 170 MB;改为常驻编译服务后,运行进程不再加载编译器,编译也从约 0.9 s 降到几十毫秒。接入沙箱后在 2 核环境下的复测见上文"资源估算"。
**第 1 阶段进展**:
@@ -259,6 +262,8 @@ flowchart TB
- 2026-10-22:设计器撤销 / 重做(撤销"生成事件处理方法"时连同插入的方法一起撤掉,不覆盖学生已写的代码);工具箱、属性、事件窗口全部显示中文,英文名放在悬停提示里;支持中文的控件名和类名。
- 2026-10-22:多窗体:新建窗体(与 VS 新建窗体相同的两个文件,沿用工程命名空间)、删除窗体(启动窗体不能删);标签页按窗体分组;运行时点哪个窗口哪个到最前。新增示例:项目 1 任务 1 登录与增加用户(两个窗体的 Designer.cs 由设计器引擎生成)。
- 2026-10-22:整理教材示例:项目 1 任务 2(计算器、多态)、项目 4 任务 1(红外对射)、项目 4 任务 3(报警灯)、项目 5 任务 1(火焰)。界面取自配套参考工程,用设计器引擎按书上的控件名改名。至此 10 个示例,全部有"能编译"检查和端到端行为测试。项目 4–8 其余任务需要的能力(摄像头、线程 API、ListBox、数据库、TrackBar、PictureBox、图表)放到第 3 阶段。
- 2026-10-22:虚拟实训台按自身宽度自适应排列,可以收起成一行状态(传感器开关仍可点),给窗体显示区腾地方。
- 2026-10-22:部署:学生程序放进 bubblewrap 沙箱(断网、只读、看不到其他进程和服务器环境变量;限制堆内存、CPU 时间、运行时长,全站并发上限);编译检查禁止网络、反射、启动进程等 API;访问口令;Bun 直接提供打包好的网页;Dockerfile + Docker Compose(Caddy 接入 NPM 的 npm_proxy 网络)+ 部署脚本,本机按服务器配置(2 核)完整验证。
## 技术风险与待确认事项
@@ -274,9 +279,9 @@ flowchart TB
| 图片处理 | System.Drawing 在 Linux 上不受支持 | 自实现 `Image` / `Bitmap` 等少量类型,只传递和保存图片 |
| 时间相关逻辑 | 项目 3 起就有按时刻开关灯的练习,部分定时器间隔长达 2 小时 | 提供可调、可加速的虚拟时钟 |
| 事件延迟 | 浏览器和服务器之间来回传递事件,点击是否有卡顿感 | 已实测:单次约 15 ms,100 个程序同时操作中位数约 60 ms |
| 并发资源 | 100 个程序同时运行时的内存和 CPU | 已压测:100 个程序合计约 1.1 GB;闲置超时回收 |
| 公网安全 | 恶意或无意的危险代码 | 沙箱 + 白名单 + 账号管控 |
| ICP 备案 | 办理需要数周 | 尽早启动 |
| 并发资源 | 100 个程序同时运行时的内存和 CPU | 已压测:100 个程序合计约 1.1 GB;2 核下 100 人同时启动约 8 秒;CPU 时间上限防死循环,运行超时回收 |
| 公网安全 | 恶意或无意的危险代码 | bubblewrap 沙箱 + 编译检查(已完成)+ 访问口令,第 2 阶段换成账号 |
| ICP 备案 | 办理需要数周 | 已备案 |
**待确认**