API 文档
Town OS systemcontroller API 的完整参考——涵盖账户、存储、仓库、软件包、systemd、 设置、审计、页面、DNS、监控、系统服务、语言区域、虚拟机镜像和状态,共 77 个端点。
概览
systemcontroller 是 Town OS 的中枢后端服务。它基于
Echo v5 构建,生产环境中监听 5309 端口(TCP)
或一个 Unix 域套接字。所有请求和响应体都使用 JSON。错误遵循
RFC 9457
(application/problem+json)。
开发环境下启用了 CORS。生产环境中,API 与界面部署在同一来源之下。
身份验证
调用 POST /account/authenticate 并提供用户名和密码即可完成验证。
响应中包含一个 Bearer 令牌。在后续请求中带上它:
Authorization: Bearer <token> 会话在闲置 7 天后过期。共有五种权限级别:
| 级别 | 说明 |
|---|---|
| 公开 | 无需令牌。 |
| 已认证 | 任意有效的会话令牌。 |
| 管理员 | 属于管理员账户的会话令牌。 |
| 授权 | 账户上的某项具体授权可以放行非管理员。对象存储端点使用对象存储授权;对等设备注册使用 wireguard 授权,而按网络的范围与每个对等设备的归属由处理器自身校验。 |
| Localhost | 来自环回地址的请求无需认证即可通过——能够访问环回地址本身就意味着已经在这台机器上——其他来源则需要徽章旁标注的级别。systemd 单元与日志端点使用它,因为控制器自身的工具会读取这些端点。 |
即便分区内部的端点并不限于管理员,创建对象存储分区仍然只保留给管理员: 分区是一棵权限树的根,并且会分配一个带配额的 btrfs 子卷, 因此授权准许你操作分区内部的用户,而不是准许你决定这个分区是否应当存在。
分页
所有列表端点都接受以下查询参数,并返回统一的信封结构:
| 参数 | 类型 | 说明 |
|---|---|---|
sort_by | string | 用于排序的字段名。 |
sort_order | string | asc 或 desc。 |
limit | int | 每页条数(默认 20)。 |
offset | int | 分页偏移量。 |
search | string | 对所有字符串字段做不区分大小写的子串匹配。 |
响应信封
{
"entries": [...],
"has_more": true,
"total_pages": 5,
"total_count": 97
} 状态
/status/ping 公开
健康检查与系统总览。未认证的调用方会得到一个精简响应,只含
status 和 needs_setup。已认证的调用方会得到完整的仪表盘数据,
包括文件系统数量、软件包计数、单元状态摘要、磁盘用量、外部/内部 IP,以及是否有可用升级。
账户
/account/authenticate 公开 用用户名和密码进行验证。返回一个会话令牌和该账户对象。
| 字段 | 类型 | 说明 |
|---|---|---|
username | string | 必填。账户用户名。 |
password | string | 必填。账户密码。 |
/account/create 公开
/
管理员 创建新账户。在引导模式下(尚不存在任何已启用的管理员账户),该端点是公开的; 否则需要管理员身份。创建的第一个账户会成为管理员。密码至少 8 个字符。 电子邮件、电话和真实姓名为必填项。
| 字段 | 类型 | 说明 |
|---|---|---|
username | string | 必填。 |
password | string | 必填。至少 8 个字符。 |
email | string | 必填。 |
phone | string | 必填。 |
real_name | string | 必填。 |
admin | boolean | 该账户是否具有管理员权限。 |
/account 已认证
按用户名获取单个账户。请求体:{"username": "alice"}。
/account 已认证 列出所有账户。支持分页参数。
/account/update 已认证
更新账户字段。传入 username 以指明账户,并附上一个
fields 对象,其中可任意组合 password、
email、phone、real_name 和 admin。
只有提供的字段会被修改。
/account/me 已认证 返回与 Authorization 头中令牌相关联的用户名。
/account/sessions 已认证 列出当前已认证用户的所有活动会话。每个会话包含其 ID、用户名、创建时间和最后使用时间。
/account/session/revoke 已认证
按 ID 吊销一个会话。请求体:{"session_id": "..."}。
/account/disable 管理员
禁用一个账户。请求体:{"username": "bob"}。
/account/enable 管理员
重新启用一个已禁用的账户。请求体:{"username": "bob"}。
存储
/storage 已认证
列出文件系统。除分页参数外,请求体中还可传入可选的 name
(前缀过滤)和 state(user、installed
或 uninstalled)。
/storage/create 已认证
创建一个新的 btrfs 子卷。传入 name 和可选的 quota
(字节)。如果配额为 0 或省略,则使用系统默认值(50 GB)。保留名称
(installed、uninstalled、archives)会被拒绝。
/storage/modify 已认证
修改已有的文件系统。传入 name 以指明对象,并附上一个
filesystem 对象,其中含更新后的 name 和/或 quota。
/storage/remove 已认证
移除一个文件系统。请求体:{"name": "mydata"}。
/storage/upload-archive 管理员
上传归档并解包到目标子卷中。接受 multipart/form-data,
含一个 subvolume 字段和一个 archive 文件。支持
.tar.gz、.tgz、.tar.bz2、.tbz2、
.tar.xz、.txz、.tar、.zip 和
.7z。
| 字段 | 类型 | 说明 |
|---|---|---|
subvolume | string | 必填。目标子卷路径。 |
archive | file | 必填。要上传的归档文件。 |
subpath | string | 可选。卷内用于解包的相对路径;会按需创建。 |
stop_service | string | 可选。解包前停止、完成后重启的 systemd 单元名称。 |
| 设置项 | 默认值 | 说明 |
|---|---|---|
max_archive_size | 1 GB | 上传大小上限。 |
archive_unpack_timeout | 600 秒 | 解包的最长耗时。 |
/storage/download-archive 管理员 下载子卷内容的归档。会以所请求的格式返回一个流式归档。
| 字段 | 类型 | 说明 |
|---|---|---|
subvolume | string | 必填。源子卷路径。 |
paths | string[] | 可选。子卷内要包含的具体路径数组。 |
stop_service | string | 可选。归档期间停止、之后重启的 systemd 单元名称。 |
format | string | 可选。压缩格式:tar.gz(默认)、tar.bz2 或 tar.xz。 |
filename | string | 可选。下载文件的自定义主文件名,服务端会补上相应扩展名。默认为 download。 |
/storage/package-volumes 已认证 按软件包分组列出软件包卷,可选择是否包含已卸载的卷。
/storage/remove-package-volume 管理员 按内部名称删除某个特定的软件包卷。
/storage/remove-package-volume-group 管理员 一次调用即删除属于某个软件包的全部卷,而不必逐个按内部名称移除。
仓库
/repository 已认证 列出所有已配置的软件包仓库,含名称、URL 和任何错误状态。支持分页参数。
/repository/add 已认证 添加一个新的软件包仓库。会立即触发一次刷新。
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 必填。仓库的显示名称。 |
url | string | 必填。仓库的 Git URL。 |
username | string | 可选。私有仓库的认证用户名。 |
password | string | 可选。私有仓库的认证密码。 |
/repository/remove 已认证
按名称移除一个仓库。会立即触发一次刷新。
请求体:{"name": "my-repo"}。
/repository/move 管理员
把某个仓库重新排到新的位置(从 0 开始计数)。当软件包重名时,靠后的仓库会覆盖靠前的。
请求体:{"name": "my-repo", "position": 0}。
/repository/refresh 已认证 强制立即刷新所有仓库的元数据。成功时返回空响应体;若有仓库失败, 则返回一个把仓库名映射到错误字符串的 JSON 对象。
软件包
/packages 已认证 列出所有仓库中全部可用的软件包。每个条目包含仓库、名称、版本、描述、supplies 标签、 安装状态,以及是否有可用升级。支持分页参数。
/packages/by-repo 已认证
按仓库分组列出软件包。接受可选的 search 查询参数。
返回一个由 {"repo": "...", "packages": [...]} 组成的数组。
/packages/installed 已认证 列出已安装软件包的标识符。支持分页参数。
/packages/installed/info 已认证
获取某个已安装软件包的详细信息。传入 repo、name
和 version。返回提问、用户回答、说明及说明类型。
/packages/responses 已认证
获取某个已安装软件包保存的提问回答。传入 repo、
name 和 version。返回一个键值映射。
/packages/versions 已认证
列出某个软件包的可用版本。请求体:{"name": "nginx"}。
返回一个版本标识符的字符串数组。
/packages/children 已认证
列出子软件包。传入 repo 和 name。返回一个字符串数组。
/packages/questions 管理员
获取某个软件包的安装提问。请求体:{"name": "nginx"}。
返回一个从提问键到 {"query": "...", "type": "..."} 的映射。
/packages/questions/identity 管理员
获取特定软件包版本的提问。传入 repo、name
和 version。
/packages/oauth/start 管理员
为某个 oauth 提问启动 OAuth 设备流程。传入 repo、
name、version 和 question。系统控制器
会向服务商执行该流程的起始步骤,并返回
flow_id、approve_url(请在用户浏览器中打开它)、
可选的 user_code,以及 interval_ms——轮询间隔。
服务商的各个 URL 来自软件包而非 Town OS,因此在调用前会被检查:
只允许 https,且绝不能是宿主机自身网络上的地址。
/packages/oauth/poll 管理员
轮询上面启动的流程。传入 flow_id。返回 status:
用户尚未授权时为 pending,授权完成时为 approved
并附带 token,而流程超时或其令牌已被取走后则为 expired——
每个流程只能使用一次。取得的令牌随后会作为该提问的回答提交给
/packages/install,与手动输入的回答完全一样。
/packages/install-preview 管理员
在真正安装之前预览这次安装会做什么。传入 repo、
name 和 version。返回卷的详情、端口映射、
磁盘占用、配额信息、升级的来源版本,以及一段易读的摘要。
/packages/install 管理员 安装一个软件包。
| 字段 | 类型 | 说明 |
|---|---|---|
repo | string | 必填。仓库名称。 |
name | string | 必填。软件包名称。 |
version | string | 必填。要安装的版本。 |
responses | object | 必填。对安装提问的键值回答。 |
reuse_volumes | boolean | 复用先前安装遗留的数据卷。 |
import_from_version | string | 升级时要从哪个版本导入卷。 |
/packages/uninstall 管理员
卸载一个软件包。传入 repo、name、version,
以及可选的 purge_volumes(布尔值)以删除相关数据。
/packages/disable 管理员
禁用一个已安装的软件包(停止其服务)。传入 repo 和 name。
/packages/enable 管理员
重新启用一个已禁用的软件包(启动其服务)。传入 repo 和 name。
/packages/purge-volumes 管理员
删除某个已安装软件包的全部数据卷。传入 repo 和 name。
/packages/uninstalled-volumes 管理员
检查某个软件包是否有先前安装遗留下来的卷。传入
repo 和 name。返回 has_uninstalled_volumes、
uninstalled_versions 和 installed_versions。
/packages/purge-uninstalled-volumes 管理员
删除先前已卸载版本遗留下来的卷。传入 repo
和 name。
/packages/upgrades 已认证
列出已安装软件包的可用升级。每个条目包含
installed_version、latest_version,以及软件包定义是否
changed。
/packages/upgrades/dismiss 管理员 忽略当前的升级提示。传入一个空的 JSON 对象。
/packages/manifest 已认证
返回原始的 YAML 软件包定义。传入 repo、name
和 version。以 Content-Type: text/x-yaml 返回文件内容。
若该软件包文件不存在则返回 404。
/packages/featured 已认证 列出所有仓库中的精选软件包。
/packages/last-responses 已认证
取回某个软件包缓存的 last 回答。传入 repo 和
name。返回上一次卸载时保存的回答,供重新安装时复用。
/packages/clear-last-responses 管理员
删除某个软件包缓存的 last 回答文件。传入 repo 和
name。
/packages/rebuild-git 管理员
为某个已安装软件包中由 git 填充的卷拉取最新改动,并重启依赖它的服务。
传入 repo、name 和 version。
重建之前会依据已保存的回答重新计算模板变量。
Systemd
/systemd/units 已认证
/
Localhost 列出由 Town OS 管理的 systemd 单元。每个条目包含单元名称、描述、 load/active/sub 状态、关联的软件包标识符与描述,以及一个失败标志。支持分页参数。
/systemd/units-tree 已认证
/
Localhost
与扁平列表相同的单元,但按依赖关系组织成树:根软件包在最上层,依赖逐层嵌套在其父级之下——
与 /storage/package-volumes 采用的结构相同。每一行都带有扁平端点返回的同样的状态数据,
因此客户端无需再发一次请求来补全信息。
/systemd/status 管理员
控制某个 systemd 单元。传入 name(单元名)和 action
(start、stop、restart、enable
或 disable)。
/systemd/status/tree 管理员
一次调用即按依赖顺序对某个软件包及其整棵依赖树施加同一操作。
这里同样拒绝 enable 和 disable,原因与 /systemd/status 一致:
级联执行 enable 会把那些已经通过父级关联的依赖重复启用一次。
/systemd/logs 管理员
/
Localhost
通过 Server-Sent Events 实时推送某个单元的 journal 条目。传入
unit 查询参数;留空或使用 __system__ 会返回全系统日志。
每个 SSE 事件都包含一条 JSON 编码的 journal 条目,字段如
Message、Priority、RealtimeTimestamp 和
SystemdUnit。
/systemd/logs/tail 管理员
/
Localhost 获取一页 journal 条目,支持基于游标的分页和过滤。
| 参数 | 类型 | 说明 |
|---|---|---|
unit | string | systemd 单元名。留空或使用 __system__ 表示全系统日志。 |
lines | int | 要返回的条目数(默认 100)。 |
before | string | 游标——返回该位置之前的条目。 |
after | string | 游标——返回该位置之后的条目。 |
grep | string | 对消息文本做不区分大小写的子串过滤。 |
since | int | Unix 时间戳——返回该时间之后的条目。 |
until | int | Unix 时间戳——收集到该时间为止。 |
priority | int | syslog 严重级别过滤(0 表示不过滤)。 |
返回 entries、cursor(首条)和
end_cursor(末条),供后续分页使用。
/systemd/logs/tree 管理员
/
Localhost /systemd/logs 的树形对应端点:用一条 Server-Sent Events 流承载某个软件包
及其之下所有单元的日志,并按时间顺序合并。即便是没有安装记录的未知根,
也会得到一条打开但没有条目的流,而不是 404,因此日志查看器对单个单元和整棵树的处理方式完全一致。
/systemd/logs/tree/tail 管理员
/
Localhost
合并后的树形日志的分页形式,接受与 /systemd/logs/tail 相同的游标、过滤与时间范围参数。
设置
/settings 管理员 以键值对象的形式获取全部设置。
/settings/get 管理员
获取单个设置项。请求体:{"key": "default_quota"}。
返回 key 和 value。
/settings/set 管理员
设置某个配置项的值。请求体:{"key": "default_quota", "value": "107374182400"}。
默认设置
| 键 | 默认值 | 说明 |
|---|---|---|
default_quota | 53687091200(50 GB) | 新建文件系统的默认配额。 |
max_archive_size | 1073741824(1 GB) | 归档上传大小上限。 |
archive_unpack_timeout | 600(秒) | 归档解包的最长耗时。 |
locale | en-US | 用于国际化的全系统语言区域。 |
proton_image | quay.io/town/proton:latest | Proton/Wine 运行器容器镜像。 |
dns_tld | home | 本地 DNS 解析所用的顶级域名。 |
审计日志
/audit/log 管理员 列出审计日志条目。请求体中的所有字段都是可选的。
| 字段 | 类型 | 说明 |
|---|---|---|
before_id | int | 键集分页——返回 ID 小于该值的条目。 |
account | string | 按账户用户名过滤。 |
sort_by | string | 用于排序的字段。 |
sort_order | string | asc 或 desc。 |
limit | int | 每页条数。 |
offset | int | 分页偏移量。 |
search | string | 搜索过滤条件。 |
每条审计记录包含 id、account、action、
path、detail、success、error
和 created_at。被审计的操作包括:身份验证,创建/更新/禁用账户,
吊销会话,安装/卸载/禁用/启用软件包,创建/修改/移除文件系统,
添加/移除/移动/刷新仓库,上传/下载归档,更新设置,忽略升级,以及清除卷。
页面
静态网站托管,支持三种内容来源类型:上传归档、容器镜像和 git 仓库。 用户为其指定一个域名,系统则通过 Caddy 容器提供内容服务。 所有会产生改动的端点都需要管理员身份;列表端点只需普通认证。
/pages 已认证 列出所有页面,支持排序、搜索和分页。可按名称、仓库 URL、 分支、域名、来源类型、状态和时间戳排序。
/pages/create 管理员
创建一个新页面。接受名称、来源类型(archive、
container_image 或 git)、仓库 URL、分支、域名、
容器镜像和镜像内目录。来源类型默认为 archive。
git 和容器镜像类型的页面会异步准备。
/pages/upload 管理员
为归档类型的页面上传内容的 tar 包。接受包含 name 和
archive 文件的 multipart 表单。仅对来源类型为
archive 的页面有效;其他来源类型返回 400。
/pages/update 管理员 对页面的仓库 URL、分支、域名、来源类型、容器镜像或镜像内目录做局部更新。 只有提供的字段会被修改。
/pages/remove 管理员 从数据库中删除一个页面,移除 webroot 符号链接,并删除对应的 btrfs 子卷。
/pages/rebuild 管理员
从来源重新构建页面内容。git 页面会拉取最新改动;容器镜像页面会从镜像中重新提取。
归档页面返回 400(请改用 /pages/upload 重新上传)。
网络
网络是一个具名的 WireGuard 覆盖网,并与一个 DNS TLD 配对。 软件包安装到某个网络中,对等设备加入这个网络,而 TLD 决定谁能解析什么。 网络名称必须是合法的 DNS 标签,且不超过 32 个字符,因为它们同时会被用作 WireGuard 接口后缀和 systemd 单元名称。
home 网络始终存在——它随数据库一同种入,而不是在启动时创建——并且在三个方面很特殊:
它无法删除,也不能被第二次创建;它只提供 DNS
(没有 WireGuard 接口、没有子网、没有对等设备);而且在它上面注册对等设备会被拒绝,
返回 400。每个账户都属于 home 网络,因此若在此接受注册,仅凭成员身份就等于获得了进入隧道的途径,
而且保存下来的对等设备所描述的隧道根本不存在。
禁用网络只会停掉传输层:WireGuard 接口不会被拉起,远程访问因此中断, 而本地 DNS 解析和容器本身照常运行。
/networks 已认证 列出网络。每个条目包含名称、TLD、子网、本机在覆盖网中的地址、公钥、监听端口和启用标志。 私钥永远不会被序列化。
/networks/create 管理员
创建网络。子网由本机身份种子与网络名称确定性地推导而来,取自
10.64.0.0/10,以避开家用路由器常用的地址段。
以本机身份为依据,意味着两台都在提供对等接入的 Town OS 主机会选出不同的子网,
因此同时加入两者的设备永远不会遇到冲突。创建 home 会因 TLD 冲突检查而返回 409。
/networks/remove 管理员
删除网络。会拒绝 home 网络。
/networks/enable 管理员 拉起该网络的 WireGuard 传输层。
/networks/disable 管理员 停掉传输层,同时让 DNS 和容器继续运行。
对等设备
/networks/peers 已认证 列出已在某个网络中注册的对等设备。
/networks/peers/connected 管理员 列出当前确实处于连接状态的对等设备,而不只是已注册的。
/networks/peers/add 授权
注册一个对等设备。wireguard 授权是放行非管理员的依据;按网络的范围与每个对等设备的归属由处理器校验。
对 home 网络返回 400,因为它只提供 DNS。
/networks/peers/refresh 授权 在对等设备的 TTL 到期之前续期其注册。注册是有时效的,回收器会清除已过期的条目。
/networks/peers/remove 管理员 把某个对等设备从网络中移除。
本地 CA
/tls/ca.crt 公开 以 PEM 格式下载本机的本地证书颁发机构。Town OS 会为软件包名称签发自己的证书, 因此信任这张证书正是让那些名称在浏览器中不再报警的关键。它被刻意设为公开: CA 证书本来就是用来分发的那一部分,而且客户端在持有任何可用于认证的凭据之前就需要它。
对象存储
Town OS 通过 gfeh 提供对象存储。一个分区包含一个 btrfs 子卷、
一个 gfehd 进程、一个管理套接字,以及它自己的一套用户。
每个 Town OS 网络恰好对应一个分区,因此对象存储的命名空间与 DNS、WireGuard 沿着同一条边界划分:
office 分区中的主体、授权或公开暴露,在 home 中毫无意义。
每个分区在固定的容器端口上提供四种 HTTP 视图——S3 在 9000、HTTP 在 9001、云盘在 9002、IPFS 在 9003—— 并且完全不发布宿主机端口。这正是固定端口之所以安全的原因: 每个分区都有自己的网络命名空间,ingress 通过容器名称访问它,与访问软件包的方式完全一样, 所以两个都在 9000 上提供 S3 的分区不可能冲突。
分区
这四条路由之所以独立于 /storage/*,是因为 /storage/create
会无条件地把提交上来的名称改写为 user/<名称>,因此无法生成
gfeh/ 前缀下的卷。它们的报文形态是 gfeh 客户端会解析的公开契约,而非内部细节。
有两处细节至关重要。前缀是不对称的:请求携带的是裸名称,
响应携带的是 gfeh/<名称>,因为该前缀是 Town OS 的命名空间产物,
并不属于分区自身的身份。以及列表返回的是裸 JSON 数组,而不是分页信封,
这与本 API 中其他所有列表端点都不同:gfeh 客户端直接反序列化一个普通列表,
而分页包装会导致解码失败。
| 路由 | 权限 | 请求 | 响应 |
|---|---|---|---|
POST /gfeh/partitions/create | 管理员 | name(不带前缀)、quota | Filesystem,名称为 gfeh/<n> |
POST /gfeh/partitions/modify | 管理员 | name、quota | Filesystem |
POST /gfeh/partitions/remove | 管理员 | name | 200,空响应 |
POST /gfeh/partitions | 已认证 | 无请求体 | Filesystem 的普通数组 |
客户端应当据以分支的状态码:409 已存在(gfeh 的置备是「创建或调整大小」,
正是靠这个状态区分两者)、404 不存在、400 名称非法、
403 非管理员。含有路径分隔符的名称在这里就会被拒绝,
因为 gfehd 在它自己的边界上也会拒绝——双方对「什么是合法分区名」若不一致,
就会让 ../user/something 这样的名称指向对象存储根之外的卷。
创建分区仅限管理员,且无法通过授权取得: 它是一棵权限树的根,并且会分配一个带配额的 btrfs 子卷, 因此持有授权的账户会在任何处理器运行之前就被拒绝。
浏览
/gfeh 已认证 对象存储总览:存在哪些分区,以及它们各自处于什么状态。
主体
分区中的用户。创建一个主体需要名称、父级和权限上限——但不需要密码,
这正是界面从不索要密码的原因。上限遵循 gfeh 的投影规则:
Town OS 管理员为 all,其余为读/写。
/gfeh/principals 已认证 列出某个分区中的主体。
/gfeh/principals/add 授权 在某个父级之下创建主体,并设定其权限上限。
/gfeh/principals/remove 授权 删除一个主体。
授权
即访问控制列表。gfehd 会把授权收窄到主体的权限上限,
因此客户端应当展示返回回来的权限,而不是自己发出去的那些:
管理员必须能够看出某项授权被收窄了。
/gfeh/grants 已认证 列出授权,也可以只列出某一个主体的。
/gfeh/grants/add 授权 给某个主体授予访问权限。响应中带有实际存储下来的权限。
/gfeh/grants/revoke 授权 按 id 撤销一项授权。
公开暴露
一条已发布的文件链接,服务于 /f/<token>。
/gfeh/exposures 已认证 列出某个分区中已发布的链接。
/gfeh/exposures/withdraw 授权 按 token 撤回一条已发布的链接,使该 URL 不再可用。
DNS
由 rolodex-dns 容器驱动的一体化本地 DNS 解析器。它为已安装的软件包
管理区域文件和记录,并通过 gRPC Unix 套接字接口提供本地名称解析。
/dns/status 已认证 返回 DNS 状态,包括启用标志、运行状态、TLD 和记录数量。
/dns/records 已认证 列出所有 DNS 记录。
/dns/records/add 管理员 添加一条 DNS 记录。接受名称、记录类型、取值和 TTL。
/dns/records/remove 管理员 按名称和类型移除一条 DNS 记录。
/dns/tld 已认证 获取当前的顶级域名设置。
/dns/tld 管理员 设置 TLD。会更改现有的 TLD 并重新注册所有已安装的软件包。
/dns/setup 管理员 初始化或重启 DNS 服务器,并注册所有已安装的软件包。
屏蔽列表
这是两份互相独立的列表。DNSBL 基于订阅——由 rolodex 拉取并应用的上游屏蔽列表—— 并配有一份放行列表,用来豁免那些无论上游列表怎么说都希望能够解析的名称。 本地屏蔽列表(RBL)则是本机自己的列表,逐条编辑。
/dns/dnsbl 已认证 获取 DNSBL 配置:订阅了哪些上游屏蔽列表,以及如何应用它们。
/dns/dnsbl 管理员 替换 DNSBL 配置。
/dns/dnsbl/allowlist 已认证 列出已从订阅屏蔽列表中豁免的名称。
/dns/dnsbl/allowlist/add 管理员 把某个名称从订阅的屏蔽列表中豁免出来。
/dns/dnsbl/allowlist/remove 管理员 删除一条放行记录,让订阅的屏蔽列表重新对该名称生效。
/dns/rbl/local 已认证 列出本机自有屏蔽列表中的条目。
/dns/rbl/local/add 管理员 向本地屏蔽列表添加一个名称。
/dns/rbl/local/remove 管理员 从本地屏蔽列表中移除一个名称。
按服务发布 DNS
/dns/services 已认证 列出已安装的服务,以及每个服务是否发布了 DNS 名称。
/dns/services/set 管理员 为某个服务开启或关闭 DNS 发布,使软件包可以在不占用网络名称的情况下运行。
监控
一体化的 Prometheus、Node Exporter 与 Grafana 组合,用于系统监控。
整套组件以受 systemd 监管、带 Restart=always 的 podman 容器运行。
/monitoring/status 已认证
返回每个监控服务的容器状态(名称、镜像、运行状态、端口)。
当未配置监控时返回 {"status": "disabled"}。
如何取得面板数据
系统控制器上并不存在任何反向代理。监控数据在自己专用的
5308 端口上提供,浏览器直接与该端口通信;控制器自身的端口(5309)只承载
/monitoring/status。5308 上监听什么,取决于所配置的后端:
- uPlot 模式(默认)——由一个 socat 转发器把 Prometheus 的 HTTP API 暴露在 5308 上,
界面直接查询
/api/v1/query_range,并自行绘制图表。 - Grafana 模式——Grafana 通过 podman 的端口映射直接监听 5308,界面用 iframe 将其嵌入。
TOWN_OS_MONITORING_PORT 可以改变面板端口;
TOWN_OS_PROMETHEUS_PORT 与 TOWN_OS_NODE_EXPORTER_PORT 对两个环回端口起同样作用。
系统服务
系统服务是由 systemd 管理的基础设施容器(与用户安装的软件包服务不同)。
它们使用 town-os-system-- 作为单元名前缀。
/system-services 公开
/
已认证 列出系统服务及其实时单元状态。从本机访问时无需认证。 每个条目包含键名、显示名称、镜像、端口和 systemd 单元状态字段。
/system-services/status 管理员
控制某个系统服务。接受 key 和 action
(start、stop 或 restart)。
/system-services/refresh 管理员 刷新系统服务的单元文件和状态。
语言区域
系统的国际化语言区域信息。
/locales 已认证 返回当前语言区域、已填充的语言区域列表、常见语言(含其母语字形名称) 以及扩展语言区域。使用 BCP 47 语言代码。
虚拟机镜像
管理虚拟机软件包所用的、已缓存的虚拟机磁盘镜像。远程镜像会被下载,
并通过 qemu-img convert 转换为 raw 格式;转换后的镜像缓存在
vm-images 子卷中。
/vm-images 已认证 列出已缓存的虚拟机磁盘镜像。返回每个镜像的名称和文件大小。
/vm-images/upload 管理员
从某个 URL 下载虚拟机镜像并转换为 raw 格式。接受一个 URL 和可选的名称。
名称默认取自 URL 中的文件名,并加上 .raw 扩展名。
下载超时为 30 分钟。
/vm-images/delete 管理员 按名称移除一个已缓存的虚拟机镜像。
对象存储管理 API(gfeh)
以上都是 Town OS 的 API,通常应用程序应当使用它。在它之下,
每个 gfehd 分区还有自己的管理接口:
以 JSON over HTTP 提供,并且只在它的 Unix 套接字上,绝不监听端口。
这个接口上既没有令牌,也没有身份验证。套接字的文件系统权限本身就是访问控制,
因此能够访问到它,就已经意味着是这台机器上的 root。套接字位于 btrfs 卷上,
因为那是 gfehd 容器与系统控制器容器都能看到的唯一一个文件系统。
| 调用 | 方法与路径 | 用途 |
|---|---|---|
Health | GET /v1/health | 存活探测,同时也用作就绪探测。 |
Names | GET /v1/names | 该分区希望发布的名称。 |
ListPrincipals | GET /v1/principals | 该分区的用户森林。 |
CreatePrincipal | POST /v1/principals | 接受 name、parent、ceiling——不含密码。 |
DeletePrincipal | DELETE /v1/principals/<name> | 移除一个主体。 |
ListGrants | GET /v1/grants?principal= | 访问控制列表,可只取某一个主体的。 |
CreateGrant | POST /v1/grants | 授予访问权限;会被收窄到主体的上限。 |
RevokeGrant | DELETE /v1/grants/<id> | 撤销一项授权。 |
ListExposures | GET /v1/exposures | 已发布的 /f/<token> 链接。 |
WithdrawExposure | DELETE /v1/exposures/<token> | 停止提供某条已发布的链接。 |
gfehd 会把内部错误映射为 HTTP 状态码——404、409、400——
而 Go 客户端再把它们映射回哨兵错误,因此 errors.Is 在跨越套接字边界之后依然可用。
对于名为 <network> 的网络,其分区的文件位置如下:
| 内容 | 位置 |
|---|---|
| 分区数据 | <btrfsBase>/gfeh/<network>,挂载于 /data/<network> |
| 配置 | <btrfsBase>/gfeh-control/<network>/gfehd.yaml |
| 管理套接字 | <btrfsBase>/gfeh-control/<network>/run/admin.sock |
| 单元 | town-os-system--gfeh-<network>.service |
DNS gRPC API(rolodex)
上面的 /dns/* 端点是 Town OS 视角下的 DNS。rolodex 本身通过
gRPC 管理,暴露在一个 Unix 套接字上(默认为
/var/run/rolodex-dns.sock),也可以选择暴露在 TCP 上。
开箱即用时,套接字是唯一的管理通道——grpc.tcp_bind 为空。
只有一个服务 rolodex_dns.RolodexDnsService,包含 74 个方法。
所有路径均为 /rolodex_dns.RolodexDnsService/<方法名>。
完整的消息定义位于 rolodex-dns 仓库的 proto/rolodex_dns.proto;
下面的分组说明这些方法各自覆盖了什么。
记录与解析
| 方法 | 用途 |
|---|---|
AddRecord | 向本地数据库添加一条 DNS 记录。 |
RemoveRecord | 从本地数据库移除记录。 |
ListRecords | 按可选过滤条件查询本地数据库。 |
SetForwarders | 配置上游转发器。 |
SetResolutionMode / GetResolutionMode | 在运行时更改并读取解析模式。 |
GetSearchDomains | 某个客户端 IP 对应的搜索域。 |
FlushCache | 清空 DNS 与屏蔽列表缓存。 |
权威区域
| 方法 | 用途 |
|---|---|
AddAuthoritativeZone | 把某个区域声明为权威区域。 |
RemoveAuthoritativeZone | 把某个区域从权威列表中移除。 |
ListAuthoritativeZones | 列出所有权威区域。 |
网络范围
范围是 rolodex 划分「谁能解析什么」的方式,也是 Town OS 网络所映射到的对象。 IP 与范围的关联带有 TTL,需要定期刷新。
| 方法 | 用途 |
|---|---|
CreateNetworkScope / DeleteNetworkScope / ListNetworkScopes | 管理范围。删除一个范围会连同它的记录与关联一起删除。 |
JoinNetwork / LeaveNetwork | 把客户端 IP 关联到某个范围,或解除该关联。 |
GetNetworkAssociations | 读取 IP 与范围的关联。 |
AddScopedRecord / RemoveScopedRecord / ListScopedRecords | 只在某个范围内存在的记录。 |
范围 TLD
按网络归属的区域,在各网络之间划分。
| 方法 | 用途 |
|---|---|
AddScopeTld / RemoveScopeTld / ListScopeTlds | 把一个全局唯一的 TLD 注册为某个范围所有。 |
SetScopeTldForwarders / ListScopeTldForwarders | 某个范围的 TLD 所对应的对端转发器。 |
ListScopeTldListeners | 绑定到某个范围各 TLD 上的 ingress DNS 监听器。 |
屏蔽列表
| 方法 | 用途 |
|---|---|
SetDnsblConfig / GetDnsblConfig | 基于订阅的域名屏蔽列表配置。 |
AddDnsblAllowlistEntry | 把某个名称及其子域从基于名称的屏蔽检查中豁免。 |
RemoveDnsblAllowlistEntry / ListDnsblAllowlistEntries | 管理放行列表。 |
AddLocalBlocklistEntry / RemoveLocalBlocklistEntry / ListLocalBlocklistEntries | 本机自有的屏蔽列表。 |
加密传输
每种传输都有对应的设置与读取方法。DoH 提供 HTTP/2,并在 enable_h3 开启时,
在同一地址、同一端口、同一证书上提供 HTTP/3。
| 方法 | 用途 |
|---|---|
SetDotConfig / GetDotConfig | DNS over TLS。 |
SetDohConfig / GetDohConfig | DNS over HTTPS,含 HTTP/3。 |
SetDoqConfig / GetDoqConfig | DNS over QUIC。 |
SetProxyConfig / GetProxyConfig | HTTP 代理配置。 |
DNSSEC、DANE 与 ACME
| 方法 | 用途 |
|---|---|
GenerateDnssecKey / ListDnssecKeys / DeleteDnssecKey | 按区域管理 DNSSEC 密钥材料。 |
GetDsRecords | 某个区域的 DS 记录。 |
SignZone | 用区域自己的 DNSSEC 密钥为其签名。 |
GenerateTlsaRecord / ListTlsaRecords | 由证书生成的 TLSA 记录。 |
GenerateDaneRootCa | 生成用于 DANE 的根 CA 证书。 |
EnsureZoneCa | 确保某个区域拥有 CA。 |
RequestAcmeCert / GetAcmeStatus | 通过 ACME DNS-01 申请证书,并查询其状态。 |
CreateEabCredential / RemoveEabCredential | 签发限定于某个区域的外部账户绑定(kid 加 HMAC),供 ACME 客户端的 newAccount 使用。 |
ListAcmeAccounts / ListAcmeCertificates | 已注册的 ACME 账户与已签发的证书。 |
DHCP
| 方法 | 用途 |
|---|---|
AddDhcpPool / RemoveDhcpPool / ListDhcpPools | 在某个范围内用于分配的地址池。 |
ListDhcpLeases / DeleteDhcpLease | 地址租约,按 MAC 地址删除。 |
SetDhcpCertOption / RemoveDhcpCertOption / ListDhcpCertOptions | 为某个范围通过 DHCP 下发给客户端的证书。 |
诊断与调优
| 方法 | 用途 |
|---|---|
GetCacheStats / FlushDnsCache | 缓存统计,以及清空响应缓存。 |
GetQueryLatencyStats | 上游查询延迟。 |
SetTtlDriftConfig / GetTtlDriftConfig | TTL 漂移配置。 |
SetTrackedTlds / ListTrackedTlds | 按 TLD 指标背后所跟踪的 TLD 列表,包括已存储的与实际生效的。 |
SetDns64Config / GetDns64Config | DNS64 配置。 |
客户端库
Town OS 自带 Go 和 JavaScript 客户端库,覆盖完整的 API。 两个客户端在遇到非 200 响应时,都会依据 RFC 9457 的问题详情抛出带类型的错误。
Go 客户端
Go 客户端位于 src/svc/systemcontroller/client.go,实现了
Client 接口。它同时支持 Unix 套接字和 HTTP 连接。
// 通过 Unix 域套接字连接(生产环境)
client := systemcontroller.InitClient("/run/town-os/systemcontroller.sock")
// 通过 HTTP 连接(开发 / 测试)
client := systemcontroller.FromClient(http.DefaultClient, "http://localhost:5309")
完成身份验证后请设置 client.Token。所有方法的第一个参数都是
context.Context。
存储
| 方法 | 说明 |
|---|---|
CreateFilesystem(ctx, fs) | 创建一个新的 btrfs 子卷。 |
ModifyFilesystem(ctx, name, fs) | 重命名文件系统或调整其大小。 |
RemoveFilesystem(ctx, name) | 按名称删除文件系统。 |
ListFilesystems(ctx, prefix, state, params) | 按名称前缀和状态("user"、"installed"、"uninstalled")过滤的分页列表。 |
仓库
| 方法 | 说明 |
|---|---|
AddRepository(ctx, name, rawURL, username, password) | 注册一个软件包仓库,凭据可选。 |
RemoveRepository(ctx, name) | 按名称移除仓库。 |
MoveRepository(ctx, name, position) | 调整优先级(0 = 最高)。 |
RefreshRepositories(ctx) | 刷新所有元数据。返回错误映射。 |
ListRepositories(ctx, params) | 仓库的分页列表。 |
软件包
| 方法 | 说明 |
|---|---|
ListPackages(ctx, params) | 可用软件包的分页列表。 |
ListPackagesByRepo(ctx, params) | 按仓库分组的软件包。 |
ListPackageVersions(ctx, name) | 某个软件包的可用版本。 |
GetPackageQuestions(ctx, name) | 按名称获取配置提问。 |
GetPackageQuestionsByIdentity(ctx, repo, name, version) | 特定版本的提问。 |
ListChildren(ctx, repo, name) | 子软件包的名称。 |
InstallPreview(ctx, repo, name, version) | 不实际安装,预览卷与端口。 |
InstallPackage(ctx, name, version, responses, reuseVolumes, importFromVersion, skipResponseReuse) | 安装一个软件包。name 采用 "repo/package" 格式。 |
UninstallPackage(ctx, repo, name, version, purgeVolumes) | 移除一个已安装的软件包。 |
DisablePackage(ctx, repo, name) | 停止服务但不卸载。 |
EnablePackage(ctx, repo, name) | 重新启用一个已禁用的软件包。 |
PurgeVolumes(ctx, repo, name) | 删除某个软件包的全部数据卷。 |
ListUninstalledVolumes(ctx, repo, name) | 检查是否有遗留的卷。 |
PurgeUninstalledVolumes(ctx, repo, name) | 删除遗留的卷。 |
ListInstalled(ctx, params) | 以 "repo/name@version" 形式列出已安装的软件包。 |
GetResponses(ctx, repo, name, version) | 已保存的配置回答。 |
GetInstalledInfo(ctx, repo, name, version) | 详细信息,含提问、回答与说明。 |
Systemd
| 方法 | 说明 |
|---|---|
ListUnits(ctx, params) | systemd 单元的分页列表。 |
SetUnitStatus(ctx, name, action) | 执行 "start"、"stop" 或 "restart"。 |
LogReplay(ctx, name) | 通过 SSE 推送 journal 条目。返回一个 channel。 |
LogTail(ctx, params) | 一页 journal 条目,支持游标分页、grep、时间范围和优先级过滤。 |
账户
| 方法 | 说明 |
|---|---|
Authenticate(ctx, username, password) | 返回会话令牌和账户。 |
CreateAccount(ctx, username, password, email, phone, realName, admin) | 创建一个用户。密码至少 8 个字符。 |
GetAccount(ctx, username) | 按用户名获取账户。 |
UpdateAccount(ctx, username, fields) | 修改账户字段(password、email、phone、real_name、admin)。 |
ListAccounts(ctx, params) | 账户的分页列表。 |
DisableAccount(ctx, username) | 阻止其进行身份验证。 |
EnableAccount(ctx, username) | 重新启用一个已禁用的账户。 |
ListSessions(ctx, token) | 该令牌所属用户的活动会话。 |
SessionUsername(ctx, token) | 会话令牌对应的用户名。 |
RevokeSession(ctx, sessionID) | 使某个会话失效。 |
审计、设置与升级
| 方法 | 说明 |
|---|---|
ListAuditLog(ctx, opts, token) | 带过滤条件的分页审计日志。 |
GetSettings(ctx) | 以键值映射返回全部设置。 |
GetSetting(ctx, key) | 按键获取单个设置项。 |
SetSetting(ctx, key, value) | 更新某个设置项。 |
ListUpgrades(ctx) | 有新版本可用的软件包。 |
DismissUpgrades(ctx) | 把待处理的升级标记为已忽略。 |
归档
| 方法 | 说明 |
|---|---|
UploadArchive(ctx, subvolume, archiveReader, filename, subpath, stopService) | 上传归档并解压到某个子卷。支持格式:tar.gz、tar.bz2、tar.xz。 |
DownloadArchive(ctx, subvolume, paths, stopService, format) | 为子卷内容创建归档。返回一个 io.ReadCloser。 |
健康检查
| 方法 | 说明 |
|---|---|
Ping(ctx) | 服务健康状况与各项计数摘要。 |
JavaScript 客户端
JavaScript 客户端位于 ui/src/api/,由 Town OS 仪表盘界面使用。
它以一组模块化的 mixin 构建在 SystemControllerClient 类之上。
非 200 响应会抛出 ApiError,其中带有解析后的 RFC 9457 问题详情。
import SystemControllerClient from './api/client.js';
const client = new SystemControllerClient('http://localhost:5309');
// 身份验证之后
const result = await client.authenticate('admin', 'password');
client.setToken(result.token); 存储
| 方法 | 说明 |
|---|---|
createFilesystem(fs) | 创建一个新的 btrfs 子卷。 |
modifyFilesystem(name, fs) | 重命名文件系统或调整其大小。 |
removeFilesystem(name) | 按名称删除文件系统。 |
listFilesystems(prefix, sortBy, sortOrder, state, limit, offset, search) | 带过滤条件的分页列表。 |
仓库
| 方法 | 说明 |
|---|---|
addRepository(name, url, username?, password?) | 注册一个仓库,凭据可选。 |
removeRepository(name) | 按名称移除仓库。 |
moveRepository(name, position) | 调整优先级(0 = 最高)。 |
refreshRepositories() | 刷新所有元数据。返回错误映射或 null。 |
listRepositories(sortBy, sortOrder, limit, offset, search) | 分页列表。 |
软件包
| 方法 | 说明 |
|---|---|
listPackages(sortBy, sortOrder, limit, offset, search) | 可用软件包的分页列表。 |
listPackagesByRepo(search) | 按仓库分组的软件包。 |
listPackageVersions(name) | 某个软件包的可用版本。 |
getPackageQuestions(name) | 按名称获取配置提问。 |
getPackageQuestionsByIdentity(repo, name, version) | 特定版本的提问。 |
installPreview(repo, name, version) | 不实际安装,预览卷与端口。 |
installPackage(repo, name, version, responses, reuseVolumes?, importFromVersion?) | 带配置回答安装一个软件包。 |
uninstallPackage(repo, name, version, purgeVolumes?) | 移除一个已安装的软件包。 |
disablePackage(repo, name) | 停止服务但不卸载。 |
enablePackage(repo, name) | 重新启用一个已禁用的软件包。 |
purgeVolumes(repo, name) | 删除某个软件包的全部数据卷。 |
listUninstalledVolumes(repo, name) | 检查是否有遗留的卷。 |
purgeUninstalledVolumes(repo, name) | 删除遗留的卷。 |
listInstalled(sortBy, sortOrder, limit, offset, search) | 以 "repo/name@version" 形式列出已安装的软件包。 |
getResponses(repo, name, version) | 已保存的配置回答。 |
getInstalledInfo(repo, name, version) | 详细信息,含提问、回答与说明。 |
Systemd
| 方法 | 说明 |
|---|---|
listUnits(sortBy, sortOrder, limit, offset, search) | systemd 单元的分页列表。 |
setUnitStatus(name, action) | 执行 "start"、"stop" 或 "restart"。 |
logReplay(unit) | 通过 SSE 推送 journal 条目。返回一个 AsyncGenerator。 |
logTail(unit, lines?, before?, after?, grep?, since?, until?, priority?) | 一页 journal 条目,支持游标分页、grep、时间范围和优先级过滤。 |
账户
| 方法 | 说明 |
|---|---|
authenticate(username, password) | 返回会话令牌和账户。 |
createAccount(username, password, email, phone, realName, admin) | 创建一个用户。密码至少 8 个字符。 |
getAccount(username) | 按用户名获取账户。 |
updateAccount(username, fields) | 修改账户字段。 |
listAccounts(sortBy, sortOrder, limit, offset, search) | 账户的分页列表。 |
disableAccount(username) | 阻止其进行身份验证。 |
enableAccount(username) | 重新启用一个已禁用的账户。 |
listSessions(token) | 该令牌所属用户的活动会话。 |
sessionUsername(token) | 会话令牌对应的用户名。 |
revokeSession(sessionID) | 使某个会话失效。 |
审计、设置与升级
| 方法 | 说明 |
|---|---|
listAuditLog(opts) | 带过滤条件的分页审计日志。 |
getSettings() | 以键值对象返回全部设置。 |
getSetting(key) | 按键获取单个设置项。 |
setSetting(key, value) | 更新某个设置项。 |
listUpgrades() | 有新版本可用的软件包。 |
dismissUpgrades() | 把待处理的升级标记为已忽略。 |
归档
| 方法 | 说明 |
|---|---|
uploadArchive(subvolume, file, subpath?, stopService?) | 通过 FormData 上传并解压归档。返回 {needs_restart, message}。 |
downloadArchive(subvolume, paths?, stopService?, format?) | 下载子卷归档。返回原始的 Response 以便流式处理。 |
健康检查
| 方法 | 说明 |
|---|---|
ping() | 服务健康状况与各项计数摘要。 |
开发参考
Town OS 后端运行在 5309 端口,Vite 开发服务器运行在 5173 端口。
用 make dev 启动完整的开发环境。
核心目标
| 目标 | 说明 |
|---|---|
make dev | 启动完整的开发环境(后端 + Vite 开发服务器)。 |
make dev-stop | 停止并删除开发后端容器。 |
make dev-logs | 在运行中的开发容器内跟踪 journalctl。 |
make dev-clean | 停止容器并拆除开发用的 btrfs 卷。 |
测试目标
| 目标 | 说明 |
|---|---|
make test | 运行代码检查、Go 单元测试和 JS 单元测试。 |
make test-integration | 在特权 Podman 容器中运行 Go 集成测试。 |
make test-ui-integration | 针对后端容器运行 Bun 界面集成测试。 |
make test-full | 依次运行全部测试套件。 |
make auto-test | 监听文件改动并自动重跑测试。 |
构建目标
| 目标 | 说明 |
|---|---|
make production-image | 构建生产环境容器镜像。 |
make test-image | 构建测试用容器镜像。 |
make pull-images | 从 Docker Hub 拉取基础容器镜像。 |
先决条件
- Go 1.25+
- Bun——JavaScript 运行时
- Podman——rootful 模式,需要
sudo - btrfs-progs——
mkfs.btrfs - golangci-lint
创建一个 .env 文件,写入仓库凭据:
TOWN_OS_REPO_USERNAME=<username>
TOWN_OS_REPO_PASSWORD=<password>
装好先决条件之后,请先运行 make pull-images,再执行其他任何目标。