Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ target/
/.ostool-server
/ostool-server/web/dist
/ostool-server/webui/node_modules
/ostool-server/webui/playwright-report
/ostool-server/webui/test-results
.build.toml
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ serde_json = "1"
toml = "1.0"
url = {version = "2", features = ["serde"]}

httpboot-protocol = { version = "0.1", path = "./httpboot-protocol" }
httpboot-protocol = { version = "0.2", path = "./httpboot-protocol" }

# Error handling
thiserror = "2"
Expand Down
2 changes: 2 additions & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

## 📖 Project Overview

See [docs/axloader-network-control.md](docs/axloader-network-control.md) for the axloader 0.2 network control, persistent MAC binding, web administration, and built-in QEMU virtual board design. The complete API contract is documented in [docs/api.md](docs/api.md).

**ostool** is a Rust toolset designed specifically for operating system development, aiming to provide OS developers with convenient build, configuration, and startup environments. It's particularly suitable for embedded system development, supporting system testing and debugging through Qemu virtual machines and U-Boot bootloader.

### ✨ Core Features
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

## 📖 项目简介

axloader 0.2 的网络控制、持久 MAC 绑定、Web 管理和内建 QEMU 虚拟板设计见 [docs/axloader-network-control.md](docs/axloader-network-control.md),完整接口契约见 [docs/api.md](docs/api.md)。

**ostool** 是一个专为操作系统开发而设计的 Rust 工具集,旨在为 OS 开发者提供便捷的构建、配置和启动环境。它特别适合嵌入式系统开发,支持通过 Qemu 虚拟机和 U-Boot 引导程序进行系统测试和调试。

### ✨ 核心特性
Expand Down
43 changes: 41 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ token=<refresh_token>
| 开发板电源状态 | `GET /api/v1/admin/boards/{board_id}/power-status` |
| 开发板租约状态 | `GET /api/v1/admin/boards/{board_id}/runtime-status` |
| 串口与网卡发现 | `GET /api/v1/admin/serial-ports`;`GET /api/v1/admin/network-interfaces` |
| axloader 设备发现 | `GET /api/v1/admin/loader-devices` |
| QEMU 虚拟设备 | `GET /api/v1/admin/virtual-devices`;`POST /api/v1/admin/virtual-devices`;`DELETE /api/v1/admin/virtual-devices/{device_id}` |
| DTB 列表与创建 | `GET /api/v1/admin/dtbs`;`POST /api/v1/admin/dtbs` |
| 单个 DTB | `GET /api/v1/admin/dtbs/{dtb_name}`;`PUT /api/v1/admin/dtbs/{dtb_name}`;`DELETE /api/v1/admin/dtbs/{dtb_name}` |
| 活动会话 | `GET /api/v1/admin/sessions`;`DELETE /api/v1/admin/sessions/{session_id}` |
Expand Down Expand Up @@ -288,7 +290,7 @@ Content-Type: application/json
- 创建时 `id` 为 `null` 或空字符串,服务端自动选择首个可用的 `{board_type}-{number}`;指定的 ID 已存在时返回 `409 Conflict`。
- 更新时 `id` 为 `null` 保持路径中的 `board_id`,指定不同 ID 表示重命名。只有租约状态为 `idle` 的开发板可以更新或删除,否则返回 `409 Conflict`。
- `board_type`、串口 key、Custom 电源命令不能为空;配置串口时 `baud_rate` 必须大于 0。请求中的 `resolved_device_path` 和 `resolved_usb_path` 会被清除,由服务端重新发现。
- `serial.key.kind` 可为 `serial_number` 或 `usb_path`
- `serial.key.kind` 可为 `serial_number`、`usb_path` 或 `qemu`。`qemu` 的 value 是虚拟设备 ID
- `power_management.kind` 可为上例的 `custom`,或中盛继电器配置:

```json
Expand All @@ -298,7 +300,10 @@ Content-Type: application/json
}
```

启用内建虚拟板时也可使用 `{"kind":"qemu","virtual_device_id":"..."}`。此时串口必须为同一个虚拟设备 ID 的 `qemu` key,并配置相同虚拟设备的 MAC。

- `boot.kind` 可为上例的 `uboot`、`{"kind":"pxe","notes":null}`,或 `{"kind":"httpboot","boot_arch":"aarch64"}`。`boot_arch` 可为 `x86_64`、`aarch64`、`loongarch64`、`riscv64` 或 `other`。
- `httpboot` 板卡必须提供 `network_identity: {"mac_address":"02:00:00:00:00:01"}`。MAC 会规范化为小写六字节冒号格式并在全部板卡配置中保持唯一;重复绑定返回 `409` 和错误码 `mac_already_bound`。`board_type` 始终由管理员填写,不根据 SMBIOS 或架构推断。
- U-Boot `network_mode` 可为 `dhcp` 或 `static_ip`。未启用 TFTP 或使用 DHCP 时服务端清除静态网络字段;使用 `static_ip` 时 `board_ip` 必填,所有已提供的网络字段必须是 IPv4 地址。`dtb_name` 必须符合单层 DTB 文件名格式,但创建或更新开发板时不会检查对应文件是否已经上传。

创建成功返回 `201 Created` 和规范化后的 `BoardConfig`;更新成功返回 `200 OK`。删除请求没有请求体,成功返回 `204 No Content`:
Expand Down Expand Up @@ -382,6 +387,38 @@ GET /api/v1/admin/network-interfaces

枚举失败时返回 `503 Service Unavailable`。

### axloader 发现与虚拟设备

```http
GET /api/v1/admin/loader-devices
```

返回当前内存探测表,包括永久/当前 MAC、IP、架构、loader 版本、SMBIOS Type 1 摘要、最近出现时间、在线状态、冲突状态、当前注册代次和实时解析出的 `bound_board_id`。10 秒未上报视为离线,记录保留 24 小时;绑定关系始终从板卡 TOML 按 MAC 计算,不单独持久化。Web UI 只为 `bound_board_id = null` 的设备提供“创建配置”。

内建 QEMU 默认关闭。启用后可管理真实 QEMU 进程:

```http
GET /api/v1/admin/virtual-devices
POST /api/v1/admin/virtual-devices
Content-Type: application/json

{"mac_address":"02:00:00:00:00:09"}

DELETE /api/v1/admin/virtual-devices/{device_id}
```

GET 返回 `{ "enabled": false, "devices": [] }` 或当前设备列表。POST 的 MAC 可省略,由服务生成本地管理、单播 MAC;成功返回 `201 Created`。虚拟设备必须经真实 UDP/HTTP 发现后才能绑定,管理接口不会注入探测记录。已被板卡配置引用的设备不能删除。

虚拟环境由服务端子命令幂等管理:

```bash
ostool-server --config .ostool-server.toml virtual-lab up
ostool-server --config .ostool-server.toml virtual-lab status
ostool-server --config .ostool-server.toml virtual-lab down
```

默认创建独立 network namespace、bridge、veth、dnsmasq 和当前用户拥有的 TAP 池,客户机网段为 `10.77.0.0/24`,服务端地址为 `10.77.0.1`。该操作需要 Linux `CAP_NET_ADMIN`。

### DTB 管理

```http
Expand Down Expand Up @@ -559,6 +596,8 @@ Content-Type: application/json

本节定义两种后端共用的开发板服务契约:本地局域网模式由 `ostool-server` 直接提供,认证模式由独立认证后端提供受认证的对应接口。这里覆盖 `ostool-server` 的全部公开、非管理 REST 接口。`ostool` 当前命令会使用会话文件上传,但不会直接调用会话详情、会话文件列表/查询/删除、显式电源控制和普通 HTTP Boot 文件上传;后者仍属于公开 board 服务契约,其中显式电源控制和普通 HTTP Boot 文件上传也已有 `BoardServerClient` 方法。

axloader 0.2 另使用 `POST /api/v1/loaders/poll`、`POST /api/v1/loaders/status` 和 `GET /api/v1/sessions/{session_id}/loader-status`。poll/status 由 UDP 发现返回的一次性 `registration_id` 关联本次固件启动;状态以 `session_id + boot_id + registration_id` 定位,旧代次迟到上报不能覆盖新代次。Session 释放时删除启动清单和 loader 状态。

### 查询开发板类型

```http
Expand Down Expand Up @@ -723,7 +762,7 @@ GET /api/v1/sessions/{session_id}/boot-profile
}
```

`boot.kind` 可为 `uboot`、`pxe` 或 `httpboot`(客户端也接受别名 `uefi_http`)。`pxe` 的对象仅含可选 `notes`;`httpboot` 的对象含可选 `boot_arch`(`x86_64`、`aarch64`、`loongarch64`、`riscv64` 或 `other`)。客户端还兼容认证后端返回可选 `mac`,但当前 `ostool-server` 不序列化该字段。顶层 `server_ip`、`netmask`、`interface`、`http_base_url` 均可为 `null`。`server_ip` 和 `http_base_url` 使用板端可访问的网络地址,不一定等于管理网地址。
`boot.kind` 可为 `uboot`、`pxe` 或 `httpboot`(客户端也接受别名 `uefi_http`)。`pxe` 的对象仅含可选 `notes`;`httpboot` 的对象含可选 `boot_arch`(`x86_64`、`aarch64`、`loongarch64`、`riscv64` 或 `other`)。HTTP Boot 的 MAC 位于顶层 `network_identity.mac_address`,不属于 boot profile。顶层 `server_ip`、`netmask`、`interface`、`http_base_url` 均可为 `null`。`server_ip` 和 `http_base_url` 使用板端可访问的网络地址,不一定等于管理网地址。

### 获取串口状态

Expand Down
186 changes: 186 additions & 0 deletions docs/axloader-network-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# axloader 网络控制与虚拟板

本文说明 `httpboot-protocol 0.2`、`axloader`、`ostool-server`、`ostool` CLI 和管理页面之间的网络启动契约。该契约不兼容旧版串口 `READY/BOOT` 协议。

## 设计边界

- 控制面只使用 UDP 发现和 HTTP。串口只承载目标系统的原始输入输出。
- `BoardConfig.network_identity.mac_address` 是板卡和配置之间唯一持久绑定;探测记录只驻留内存。
- `board_type`、板卡 ID、电源、串口和启动配置始终由管理员填写。SMBIOS 仅辅助辨认硬件,不推断 `board_type`。
- MAC 是绑定键,不是认证凭据。当前协议用于受信实验室二层网络;HTTP 和镜像 SHA-256 不抵抗同网段主动攻击。
- 服务重启后会重新读取板卡 TOML,但不恢复旧 Session、启动清单、loader 状态或客户端连接。

## 启动流程

```mermaid
sequenceDiagram
participant L as axloader
participant S as ostool-server
participant C as ostool CLI
participant T as 目标系统串口

L->>L: UEFI 同一控制器取得 SNP/IP4/UDP4/HTTP
L->>S: UDP :2998 DiscoveryProbe
S-->>L: DiscoveryOffer + registration_id
loop 未绑定或没有启动命令
L->>S: POST /api/v1/loaders/poll
S-->>L: unbound / bound_idle / reject
end
C->>S: 创建 Session 并上传 ELF
C->>S: 连接串口 WebSocket(自动上电)
L->>S: POST /api/v1/loaders/poll
S-->>L: boot + session_id + boot_id + 摘要
L->>S: accepted / downloading
L->>S: GET 相对 kernel_path
L->>L: 校验长度和 SHA-256,装载 ELF
L->>S: verified / ready_to_handoff
L->>L: 销毁 UDP/HTTP/IP 对象并 ExitBootServices
T-->>C: 目标系统原始串口输出
C--xS: WebSocket 关闭
S->>S: SerialClosed → releasing → 断电 → idle
```

`ready_to_handoff` 是最后一个可靠网络状态。它不会消费启动清单;同一 Session 内板卡重启后,新 `registration_id` 会重新取得相同 `boot_id`。上传新内核才会以新 `boot_id` 替换旧命令。Session 释放时,启动命令和 loader 状态一起删除。

## 发现和注册代次

axloader 向当前 IPv4 子网定向广播地址的 UDP `2998` 发送 JSON。数据报不得超过 1400 字节,只含协议版本、永久 MAC、当前链路 MAC、架构和 loader 版本。SMBIOS Type 1 详情在 HTTP poll 中上报。

server 为每次发现签发一次性 `registration_id`,默认有效期为 30 秒。第一次 poll 后它代表当前 loader 启动代次:

- 新代次可以替换不再上报的旧代次;
- 被替换的旧代次若再次上报,则该 MAC 进入冲突状态;
- 冲突期间 server 返回 `duplicate_mac`,不下发启动命令;
- 10 秒没有 poll 的设备显示为离线,探测记录保留 24 小时;
- 每次 poll 都从当前板卡 TOML 重新计算 `bound_board_id`,不保存第二份绑定关系。

axloader 如果发现两个不同的 `server_id`,不会随机选择其中一个,而是按 1、2、4、8、10 秒封顶退避重新发现。

## HTTP 接口

| 方法与路径 | 调用者 | 含义 |
| --- | --- | --- |
| `POST /api/v1/loaders/poll` | axloader | 注册或刷新设备,返回 `unbound`、`bound_idle`、`boot` 或 `reject` |
| `POST /api/v1/loaders/status` | axloader | 上报 `accepted`、`downloading`、`verified`、`ready_to_handoff` 或 `failed` |
| `GET /api/v1/admin/loader-devices` | 管理页面 | 查询探测设备、绑定、在线和冲突状态 |
| `GET /api/v1/sessions/{id}/loader-status` | CLI/管理工具 | 查询当前 Session 的 `boot_id`、`registration_id` 和状态 |
| `GET /api/v1/admin/virtual-devices` | 管理页面 | 查询虚拟板功能开关和进程状态 |
| `POST /api/v1/admin/virtual-devices` | 管理页面 | 创建并启动一个尚未绑定的 QEMU 设备 |
| `DELETE /api/v1/admin/virtual-devices/{id}` | 管理页面 | 停止并删除未绑定虚拟设备 |

`boot` 响应中的内核路径必须是当前 server 下的相对路径,同时包含大小、SHA-256、架构、`elf64` 格式和可选入口符号。状态更新由 `session_id + boot_id + registration_id` 定位;旧启动命令或旧注册代次的迟到状态返回冲突,不能覆盖当前状态。

## 板卡配置

HTTP Boot 实体板示例:

```toml
id = "rk3568-01"
board_type = "RK3568"
disabled = false
tags = ["arm64"]

[network_identity]
mac_address = "02:11:22:33:44:55"

[serial]
baud_rate = 1500000

[serial.key]
kind = "serial_number"
value = "USB-UART-01"

[power_management]
kind = "custom"
power_on_cmd = "board-power rk3568-01 on"
power_off_cmd = "board-power rk3568-01 off"

[boot]
kind = "httpboot"
boot_arch = "aarch64"
```

MAC 保存为小写六字节冒号格式并全局唯一。HTTP Boot 板卡缺少 MAC 时配置无效。只有板卡处于 `idle` 时才允许修改 MAC、重命名或删除;使用中和释放中返回 `409 Conflict`。重复绑定返回错误码 `mac_already_bound`。

管理页面的未绑定设备区域每 5 秒刷新。选择探测设备只会把 MAC 带入编辑器并展示 IP、架构、loader 版本和 SMBIOS;不会自动填写板卡 ID、`board_type`、电源、串口或启动设置。也可以手工输入 MAC。

## ostool CLI 行为

HTTP Boot runner 按以下顺序工作:

1. 按人工配置的 `board_type` 创建 Session;
2. 上传 ELF,server 发布新的 `boot_id`;
3. 立即连接串口 WebSocket,由现有串口生命周期自动上电;
4. 并行读取原始串口并轮询 loader status;
5. 活动 Session 内板卡重启时继续等待新注册代次,不重建 Session;
6. loader 报告 `failed` 时结束;正常成功仍以目标系统串口成功条件为准;
7. WebSocket 关闭后沿用 `SerialClosed` 释放流程并断电。

## 内建 QEMU 虚拟板

虚拟板默认关闭。第一阶段固定为 `x86_64 + OVMF + q35`,使用 TCG,协议类型仍保留其他架构值。示例 server 配置:

```toml
[loader_network]
enabled = true
bind_addr = "0.0.0.0:2998"
public_base_url = "http://10.77.0.1:2999"

[virtual_qemu]
enabled = true
qemu_binary = "/usr/bin/qemu-system-x86_64"
ovmf_code = "/usr/share/OVMF/OVMF_CODE_4M.fd"
ovmf_vars = "/usr/share/OVMF/OVMF_VARS_4M.fd"
axloader_efi = "/opt/ostool/BOOTX64.EFI"
runtime_dir = "/var/lib/ostool-server/qemu"
network_namespace = "ostool-qemu"
bridge = "ostool-br0"
tap_pool = ["ostool-tap0", "ostool-tap1"]
memory_mib = 512
cpus = 2
```

本地网络环境命令需要创建 network namespace、veth、bridge、TAP 和 dnsmasq,因此应以具备 `CAP_NET_ADMIN` 的身份运行:

```bash
ostool-server --config /etc/ostool-server/config.toml virtual-lab up
ostool-server --config /etc/ostool-server/config.toml virtual-lab status
ostool-server --config /etc/ostool-server/config.toml virtual-lab down
```

默认客户机网段为 `10.77.0.0/24`,server 为 `10.77.0.1`,dnsmasq/bridge 为 `10.77.0.254`,DHCP 池为 `10.77.0.100-200`。`up`、`status` 和 `down` 可以重复调用;`up` 检测已存在环境,`down` 忽略已删除资源。

管理页面启动虚拟设备后,QEMU 必须通过真实 UDP/HTTP 流程出现在未绑定列表。绑定时三个位置必须引用同一虚拟设备及其 MAC:

```toml
[network_identity]
mac_address = "02:aa:bb:cc:dd:ee"

[serial]
baud_rate = 115200

[serial.key]
kind = "qemu"
value = "<virtual_device_id>"

[power_management]
kind = "qemu"
virtual_device_id = "<virtual_device_id>"

[boot]
kind = "httpboot"
boot_arch = "x86_64"
```

`VirtualBoardManager` 持有 QEMU 子进程、TAP、独立 OVMF VARS、QMP socket 和串口 hub。`On`/`Off` 幂等;关闭先发 QMP `quit`,3 秒后仍未退出才终止进程。串口 hub 不随 QEMU 子进程退出,保留最近 64 KiB 输出,使同一 WebSocket 能跨 `Off → On`。新 QEMU 启动会生成新的 loader 注册代次,并从同一活动 Session 重新取得原 `boot_id`。

## 验收顺序

1. 启动未绑定 QEMU,确认它通过真实发现出现在管理页面。
2. 人工填写板卡 ID、名称/类型、QEMU 电源和串口,并选择探测 MAC。
3. 使用 `ostool` 创建 Session、上传 ELF并连接串口,确认下载、摘要校验、handoff 和串口成功标志。
4. 保持 WebSocket 和 Session,执行虚拟板 `Off → On`,确认新 `registration_id` 取得相同 `boot_id` 并再次 handoff。
5. 关闭 WebSocket,确认 Session 经 `releasing` 回到 `idle` 且 QEMU 退出。
6. 新建 Session 再运行一次,全程不修改绑定。

实体板按相同顺序验收首次绑定、活动 Session 内重启、释放和新 Session;差异只在电源与串口后端。
13 changes: 7 additions & 6 deletions httpboot-protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ keywords = ["httpboot", "uefi", "bootloader"]
license = "MIT OR Apache-2.0"
name = "httpboot-protocol"
repository = "https://github.com/drivercraft/ostool"
version = "0.1.1"
version = "0.2.0"

[features]
default = ["std", "serde"]
serde = ["dep:serde", "dep:serde_json"]
std = []
default = ["std", "json"]
alloc = []
json = ["alloc", "dep:serde", "dep:serde_json"]
std = ["alloc", "serde?/std", "serde_json?/std"]

[dependencies]
serde = { workspace = true, features = ["derive"], optional = true }
serde_json = { workspace = true, optional = true }
serde = { version = "1", default-features = false, features = ["alloc", "derive"], optional = true }
serde_json = { version = "1", default-features = false, features = ["alloc"], optional = true }
Loading
Loading