Files
WorkspaceAccessHost/docs/client-server-transport.md
Nitload 0ea679932e
Verify / verify (ubuntu-latest, linux-bionic-arm64) (push) Canceled after 0s
Verify / verify (ubuntu-latest, linux-x64) (push) Canceled after 0s
Verify / verify (windows-latest, win-x64) (push) Canceled after 0s
Verify / postgres (push) Canceled after 0s
Build Charac server and client with OIDC, SSH sessions, and packages
2026-10-05 14:02:50 +08:00

14 KiB
Raw Permalink Blame History

Client 与 Server 的单域名入口

配置域名与监听地址

Server 可通过全局 --config <路径> 显式指定 INI 配置;未指定时,开发环境优先读取仓库根目录的 workspace-access.config,发布时需要提供外部配置路径,或自行将同名文件放在可执行文件旁。启动时加载,修改后需重启。配置示例:

[server]
domain=127.0.0.1
listen=http://127.0.0.1:5080

domain 是客户端使用的对外域名,只填主机名,不带协议或端口。Server 以它校验 HTTP Host;不匹配时返回 400。listen 是 Kestrel 实际绑定的 HTTP 地址,默认仅在本机监听。两者分开配置,例如通过 Caddy 提供 https://access.example.com 时,设 domain=access.example.com,保留 listen=http://127.0.0.1:5080,并让 Caddy 将原始 Host 和 WebSocket Upgrade 转发到该监听地址。配置域名不会自动创建 DNS 记录、签发 TLS 证书或开放端口;这些由部署环境和反向代理负责。目标 sshd 始终只在内网可达。

路由

CLI 只接受一个 Server origin,例如 https://access.example.com。操作类型由 HTTP 方法和固定路径表示,目标与会话定位 ID 通过 query 传递。账户、认证令牌、SSH 证书和密钥不放进 URL。

方法与路径 用途 当前状态
GET /status 公开的协议版本、OIDC public client 配置与会话可用状态 已实现;ready 表示 OIDC 和数据库连接已配置,不保证后端在线
GET /resources 当前身份可新建的目标及持有的存活会话快照 已实现,需 Bearer token
GET /identity 返回当前经过验证的 (issuer, subject),供本机管理员登记授权 已实现,需 Bearer token
POST /session?target={targetId} 验证 Bearer token 与授权后,按授权绑定的 Server 私钥或临时 SSH 密码创建工作区 已实现;返回工作区 ID,密码不入库
GET /session?id={sessionId} 再次校验身份和目标授权,升级为 WebSocket 附着 已实现;takeover=true 显式接管,默认已有控制者时返回 409
DELETE /session?id={sessionId} 再次校验身份、设备和目标授权后结束工作区 已实现;后端 SSH 连接随之关闭

两个 ID 均使用非空 GUID。query 只负责定位资源,不代表访问权;创建和附着先验证 authentik access token、目标授权和工作区归属。系统账户由 Server 根据数据库授权映射;已绑定 SSH 登录密钥时 Server 从受保护目录加载私钥,未绑定时返回 400 password_required,Client 才在 HTTPS 请求体提供一次性 SSH 密码。密钥配置见SSH Broker。正式 Client 在 GET /identity、GET /resources、POST /session、WebSocket GET /session 和 DELETE /session 请求头发送 X-RAC-Device 安装凭据。除 /identity 外,所列请求都必须带有效凭据;另一安装已有活跃连接时返回 409 device_in_use,缺少或无效凭据时返回 400 device_credential_required。独立 OIDC demo 的 /identity 调用仍可不带该头。WebSocket 使用 Authorization: Bearer 请求头,输入消息为 {"type":"input","data":"<base64>"},尺寸消息为 {"type":"resize","columns":80,"rows":24},输出消息为 {"type":"output","sequence":1,"data":"<base64>"}。输出序列号可发现丢失,但没有屏幕快照。后端 SSH 结束时,Server 会在已排队输出之后发送 {"type":"ended","reason":"ssh_closed"},再关闭 WebSocket;显式结束工作区时 reason 为 workspace_closed。接管造成的观察者连接关闭不发送后端结束事件。若连接意外中断而没有收到 ended,Client 将其视为 Server 连接中断,不能误报为 SSH 已正常结束。LAN 管理面承担目标注册、后端凭据配置与授权维护,不挂在这个 WAN 路由组上。协议兼容性由 /status 中的 protocolVersion=3 表示,当前不在 URL 中加入 /api/v1。

正式 Client 登录与交互

login --server <origin> 复用已在 demo 验证的浏览器 Authorization Code + PKCE 流程,再请求 Server /identity。只有 Server 接受令牌后,CLI 才显示登录成功及 (issuer, subject)。whoami 保留为同一入口。Client 从 Server /status 获取 [oidc] 配置,不需要复制 Server 的 INI 或数据库密码。demo 继续用于独立验证身份提供方。

在仓库根目录启动 Server,再在另一个终端操作 Client:

dotnet run --project src/server -- --config .\workspace-access.config serve
dotnet run --project src/client -- login --server http://127.0.0.1:5080
dotnet run --project src/client -- connect --server http://127.0.0.1:5080

正式 CLI 还可以在用户级 client.config 的 [client] server 设置默认入口:Windows %APPDATA%\Charac\client.config,Linux/Termux 使用 $XDG_CONFIG_HOME/charac/client.config 或 ~/.config/charac/client.config。配置后直接运行 connect、login、status;命令上的 --server 优先,--config <路径> 可在命令名前选择另一份配置。此文件只保存入口 origin,不保存认证令牌。

connect 自行登录并显示资源选择菜单,无需先执行 login;login 用于验证配置和取得身份键,不建立跨进程登录缓存。浏览器登录后的蓝色回调页、IBM Plex Mono 字体与 demo 共用同一份实现。Server 必须配置 OIDC 和数据库;登录身份仍需由管理员登记目标授权,见认证与授权。也可用 connect --server <origin> --target <UUID> 跳过菜单;--target 是数据库目标 UUID,不是主机 IP。若该授权已绑定可用的 Server SSH 登录密钥,Client 不会提示 SSH 密码;否则继续使用交互式密码登录。

正式 Client 首次运行时在当前用户的本地应用数据目录 workspace-access/device-key 生成 32 字节随机安装凭据;后续命令复用它。同一 (issuer, subject) 在 Server 上只能由一个安装凭据持有活跃 Client 连接,同一安装可同时打开多个终端。最后一条 WebSocket 断开后,另一设备可登录并连接;已有 SSH 工作区仍保留。管理员的本机 disconnect 可以立即切断此身份的所有 Client/WebSocket 并释放占用,见服务宿主。这是 Server 进程内的活跃连接仲裁,不限制 authentik 自身的浏览器登录,也不把设备归属写入数据库。安装凭据是可复制的 bearer 秘密,不能证明物理设备;复制到另一机器会被视为同一安装,应像密码一样保护。仅经 HTTPS 发送;开发回环 HTTP 例外。Windows 使用当前用户的应用数据目录权限,Unix 创建文件时限制为 0600。后续若需抵抗凭据复制,应另设计设备密钥的持有证明。

连接成功后持续转发终端输入、输出、鼠标和尺寸变化。Ctrl+] 解除附着,按键回退模式也接受 F12;工作区继续由 Server 持有。Client 在附着前打印工作区 ID,重新进入使用:

dotnet run --project src/client -- connect --server http://127.0.0.1:5080 --id <工作区UUID>

重新附着会重新登录、重新检查数据库授权,不再创建 SSH 会话或请求 SSH 密码。若仍有控制者,Server 返回 409,当前 Client 不自动接管。远端 exit、SSH 断线或 Server 停止会结束后端会话;正式 Client 的本机退出和传输错误不会发送工作区删除请求。令牌只保留在当前命令进程中;离线输出仍受 Server 的有界缓冲限制,屏幕快照和自动重连尚未实现。真实 OIDC → Server → SSH 交互式 ls 已在 .100 → .101 实测;正常退出及重新附着待复测,Android 限制见平台说明。

登录后的资源发现

GET /resources 要求经过 Server 验证的 Bearer token,不接受 query 中自报的用户身份。返回 targets 和 sessions 两个数组;无授权或未登记的身份返回空数组。未认证返回 401;Server 缺少 OIDC 或数据库配置时返回 503。响应设置 Cache-Control: no-store。

targets 的每项为 { id, name, account }。Server 使用一次 EF LINQ 查询,按 (issuer, subject)、身份/目标/授权启用状态、授权有效期和已启用主机公钥过滤;复用实际建连的授权条件。不返回内网地址、SSH 端口、主机公钥、CA 或后端凭据。可列出的目标代表有权尝试新建会话,不代表机器已经通过在线探测。

sessions 的每项为 { id, targetId, targetName, account, createdAt, state }。创建时间使用 UTC,state 为 detached(没有控制者,可附着)或 attached(已有控制者)。只列出当前身份持有、仍有目标访问权且系统账户映射一致的存活工作区;后端结束或 Server 停止后不保留在列表中。创建时间与状态来自进程内 Session Manager,不新增数据库表。

数据库授权和各工作区状态构成查询时的快照,不是连接许可。实际创建、附着、结束仍重新查权限;管理员改动账户映射后,旧账户工作区不会列出,直接附着/结束请求也被拒绝。在线撤销和旧工作区的管理员清理机制仍待完善。列表读取不持有全局锁执行数据库或网络操作。

不指定 ID 的 connect --server <origin> 会在登录后显示菜单:已有会话在前,新建目标在后;输入编号选择、r 重新请求列表、q 或输入结束退出。占用中的会话显示但不提供编号,CLI 暂不支持显式接管;列表显示后若状态变化,实际附着仍由 Server 拒绝或准许。刷新复用当前进程令牌,不重新打开浏览器。取消和刷新都不会创建或删除工作区。

CLI 探测

WorkspaceAccessClient status --server https://access.example.com 请求 /status,检查服务标识和协议版本。run --server <origin> --target <id> 从状态响应取得 OIDC issuer、client ID 和 discovery URL,执行浏览器 Authorization Code + PKCE,提示输入 SSH 密码,创建工作区并通过 WebSocket 发送 Linux ls;attach --server <origin> --id <id> 重新登录并附着已有工作区,也发送 ls。这两个命令目前用于一次性验收,不提供持续交互终端。CLI 不持久化令牌和密码。只允许 HTTPS origin;开发时允许 loopback HTTP,例如 http://127.0.0.1:5080。不接受 Server origin 中的路径、查询串或用户信息。

无需 OIDC 的本机端到端诊断使用显式 serve --local-probe 开关。Server 此时额外注册 /local/session,从 [ssh_probe] 读取 .101 等诊断目标,不创建 PostgreSQL 记录,也不改变正式 /session 的认证要求。开关只在 [server] listen 和 domain 都为数值回环地址时生效;请求来源还必须是回环地址。只用于同一台机器上的两个终端,不要通过 Caddy 转发或用于 LAN/WAN。SSH 后端仍以运行时输入的密码登录,并固定校验配置的目标主机公钥。

终端一保持 Server 运行,终端二运行 CLI:

dotnet run --no-restore --no-launch-profile --project src/server -- --config .\workspace-access.config serve --local-probe
dotnet run --no-restore --no-launch-profile --project src/client -- probe-session --server http://127.0.0.1:5080

第二条命令会提示输入 SSH 密码,经 HTTP 创建托管工作区、WebSocket 发送一次 ls、检查输出和退出码,再关闭该诊断工作区。要在同一个工作区持续输入命令,把第二条命令改为 dotnet run --no-restore --no-launch-profile --project src/client -- shell --server http://127.0.0.1:5080。在 Windows VT 终端中,shell 开启 VT 输出和延迟行尾换行,读取并转发原始输入字节,包括 nvim 请求的鼠标报告、方向键和 Ctrl+C;只有远端 TUI 开启鼠标跟踪时才转发鼠标报告,退出后会过滤滞留报告。连接后会同步终端尺寸,随后持续转发尺寸变化。按 Ctrl+] 退出并关闭本次诊断工作区,Server 仍运行。若终端不支持 Windows VT 输入,CLI 回退到按键读取,鼠标操作不可用,此时可用 F12 退出。退出时恢复本机控制台模式、关闭终端鼠标报告并清空待处理的本机输入;只有远端仍处于备用屏幕时才退出备用屏幕,避免把光标恢复到旧位置。本地密码提示和断开状态会接在当前终端行后,不额外插入空行;收到 ended 后显示工作区关闭状态。远端 shell 自行结束后 Server 可能已经删除工作区,此时 Client 的清理请求收到 404 不会误报为连接失败。附着期间输出队列采用背压并检查序号,若检测到序号缺口,CLI 会结束该诊断工作区并报告错误。nvim/Mason 的 Up/Down 滚动残影已在 2026-10-03 的本机 Debug Client 测试中未复现;断开提示的位置尚待实机复测。Android/Termux 的原始输入模式、断线恢复及正式认证路径的交互验收仍待完成。ssh probe 与 ssh broker-probe 则是执行完毕即退出的单次诊断命令;它们不会启动常驻 HTTP Server。

本机可分别运行:

dotnet run --project src/server
dotnet run --project src/client -- status --server http://127.0.0.1:5080
dotnet run --project src/client -- run --server http://127.0.0.1:5080 --target <已授权目标的 UUID>

健康探针 /health/live 与 /health/ready 保持独立于客户端协议,也遵守相同的 Host 校验。若部署需要转发头,必须在 [server] trusted_proxy 中只信任直接相连的代理地址;Server 仅在此前提下读取一跳转发来源。