协议版本:1(
kProtocolVersion) 对应实现:backend/src/protocol/frame_packet.h、frontend/src/lib/protocol.ts状态:一阶段。真实抓屏、输入注入、UDP 直连与 WebRTC 尚未接入,但字节布局已经冻结。
本文是唯一权威的线上格式说明。C++ 与 TypeScript 两份实现都以本文件为准;两者不一致时是缺陷,不是选择。
- Magic
"MOLK"不可更改。 它在包头前 4 个字节,任何传输、任何版本都必须逐字节一致。 - 所有整数小端序。 与主机字节序无关。
- 一条消息 = 一个完整包。 WebSocket 二进制消息、未来 UDP 数据报、RTCDataChannel 消息都承载恰好一条 MOLK 包;不做跨消息分片,也不在一条消息里放两条包。
- 包头 + 载荷长度必须严格相等。 接收方拒绝长度不匹配、尾部多余字节、瓦片越界的包。
- 载荷上限
64 MiB(kMaxPayloadBytes),单帧瓦片数上限4096。4K BGRA 关键帧约 33 MiB,上限必须高于它。
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 4 | magic |
4D 4F 4C 4B("MOLK") |
| 4 | 1 | version |
当前为 1 |
| 5 | 1 | type |
包类型,见下表 |
| 6 | 2 | flags |
位标志,见 §2.1 |
| 8 | 4 | session |
后台分配的会话 id(自增 u32) |
| 12 | 4 | seq |
会话内包序号 |
| 16 | 8 | ts_us |
捕获时间戳,Unix 纪元微秒 |
| 24 | 4 | payload_len |
后续载荷字节数 |
| 28 | 4 | reserved |
必须为 0 |
| 位 | 名称 | 含义 |
|---|---|---|
| 0 | KEYFRAME |
本包含关键帧(整帧可独立解码) |
| 1 | COMPRESSED |
载荷经过压缩(当前未使用) |
| 2 | LAST |
控制流中的最后一条 |
| 值 | 名称 | 载荷 |
|---|---|---|
0x01 |
HELLO |
JSON 控制载荷(传输层握手) |
0x02 |
FRAME |
帧体,见 §3 |
0x03 |
ACK |
JSON |
0x10 |
INPUT |
JSON 输入事件批(当前回 not_implemented) |
0x11 |
CLIPBOARD |
JSON |
0x12 |
FILE_DATA |
u32 id + u32 保留 + u64 offset + 文件字节 |
0x13 |
AUDIO |
交错 little-endian 16-bit PCM(格式见 welcome) |
0x14 |
VOICE |
语音通话 PCM:48 kHz 单声道 little-endian 16-bit |
0x7F |
CONTROL |
会话级 JSON 控制 |
0x80 |
PING |
空 |
0x81 |
PONG |
空 |
未知类型必须被忽略或回错,不得按已知类型解析。
帧体 = 16 字节帧头 + N × 16 字节瓦片表项 + 按表项顺序拼接的瓦片字节。
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 4 | frame_seq |
帧序号 |
| 4 | 2 | tile_count |
瓦片数(≤ 4096) |
| 6 | 1 | codec |
见 §3.3 |
| 7 | 1 | flags |
帧标志 |
| 8 | 2 | screen_w |
屏幕宽(像素) |
| 10 | 2 | screen_h |
屏幕高(像素) |
| 12 | 2 | tile_w |
标称瓦片宽 |
| 14 | 2 | tile_h |
标称瓦片高 |
帧 flags:
| 位 | 名称 | 含义 |
|---|---|---|
| 0 | KEYFRAME |
整帧可独立解码 |
| 1 | PARTIAL |
只包含变化瓦片;未列出的区域保持上一帧内容 |
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 2 | x |
左边界(像素) |
| 2 | 2 | y |
上边界(像素) |
| 4 | 2 | w |
宽 |
| 6 | 2 | h |
高 |
| 8 | 1 | flags |
瓦片标志 |
| 9 | 3 | reserved |
0 |
| 12 | 4 | byte_length |
紧随其后的瓦片字节数 |
瓦片 flags:
| 位 | 名称 | 含义 |
|---|---|---|
| 0 | CHANGED |
本帧有变化 |
| 1 | KEYFRAME |
该瓦片可独立解码 |
约束:
- 瓦片矩形必须落在
screen_w × screen_h内;接收方应做边界检查。 - 瓦片尺寸不要求等于
tile_w × tile_h:服务端会把同一行里连续变化的瓦片合并成一条完整的水平条带(关键帧因此是每行一条),接收方必须按表项中的w/h处理任意矩形。 - 瓦片字节不跨包。一个瓦片必须完整落在同一帧包内。
- 表项顺序即数据顺序,无空洞、无尾部多余字节(解析器会拒绝)。
| 值 | 名称 | 说明 |
|---|---|---|
| 1 | bgra |
原始 32 位 BGRA,自上而下逐行;byte_length 必须 ≥ w*h*4 |
| 2 | png |
单瓦片 PNG |
| 3 | h264 |
Annex-B 码流(规划中) |
| 4 | av1 |
AV1 OBU:每个瓦片是一条可独立解码的 intra 流;解码后按 x/y/w/h 贴回画面 |
| 5 | deflate_bgra |
zlib(deflate) 压缩的 BGRA 瓦片 |
| 6 | rgb24 |
原始 24 位 RGB(R、G、B),丢弃 alpha |
| 7 | rgb565 |
原始 16 位 RGB565,小端 |
| 8 | deflate_rgb24 |
zlib(deflate) 压缩的 RGB24 瓦片 |
| 9 | deflate_rgb565 |
zlib(deflate) 压缩的 RGB565 瓦片 |
bgra 的通道顺序是 B、G、R、A。Canvas ImageData 需要 R、G、B、A,渲染时必须换位。
rgb24 的顺序是 R、G、B;rgb565 每像素 2 字节、小端,位布局为
RRRRRGGG GGGBBBBB(5/6/5),解码时用位复制扩展回 8 位。
codec = 5/8/9时,每个瓦片的byte_length是 zlib 流(带 zlib 头,浏览器DecompressionStream("deflate")可直接解压)的长度。- 解压后的长度必须按 codec 精确匹配:
bgra为w*h*4,rgb24为w*h*3,rgb565为w*h*2。 - 服务端按画质预设选择 zlib 级别:关键帧/大突发在“流畅”档固定级别 1, “平衡”档沿用自适应(1/3/6),“清晰”档提高到 3/6/9。
- 如果整帧压缩后不小于该格式的原始大小,服务端回退为对应的原始格式
(
1/6/7)。有损格式不会因为有损而回退到 BGRA:打包后的 2/3 字节 仍然比 4 字节小。 - 浏览器原生只支持
DecompressionStream("deflate"),所以服务端不直接发 Brotli;桌面客户端如需要 Brotli,应在传输协商中单独约定 codec,而不是复用 codec 5。 - 帧内所有瓦片使用同一种 codec,不混合。
服务端只有在 hello.av1 = true(本地核心代理确认 dav1d 可用)且设置里启用 AV1
时才会生成 AV1。默认的全帧模式(av1_full_frame=1)里,每个 FRAME 包只包含
AV1:本帧所有变化矩形都编码进 AV1 瓦片,基础瓦片数组为空,不会再同时发送同一画面
的瓦片。关键帧是覆盖整屏的单条 AV1 intra 流,并带 KEYFRAME 包标志与帧头标志,
接收端可据此重新建立画面。旧的分块模式(av1_full_frame=0)保留兼容:先发基础
瓦片帧,再发一个带 tileRefresh 包标志的 AV1 补丁,补丁覆盖从基础瓦片中移除的
大块区域。
AV1 不可用、观看端没有解码能力或单帧编码失败时,服务端自动回到普通瓦片帧;
hello.av1 为假的会话永远只收到瓦片。
quality 控制消息(见 §4.2)把主机编码切换到以下预设之一:
| 预设 | 格式 | zlib | 变化阈值 | 用途 |
|---|---|---|---|---|
smooth(流畅) |
rgb565 |
固定 1 | 至少 16 | 优先包体积;16 位色,丢弃细小的颜色差异 |
balanced(平衡,默认) |
bgra |
自适应 | 使用配置值(默认 4) | 无损,和旧版本行为一致 |
quality(清晰) |
bgra |
3/6/9 | 最多 1 | 优先画质,加大压缩力度 |
raw(原画不压缩) |
bgra |
关闭 | 最多 1 | 预留:客户端可显式请求;1080p 的裸 BGRA 流量远超普通局域网,UI 不暴露 |
game(游戏) |
rgb565 |
固定 1 | 至少 16 | 允许有损;等价于 smooth |
画质预设只管画质,不再隐含帧率。帧率由消息里的 fps(1..60)单独控制,
与 capture-fps 等价;客户端默认按用户选择的“平均帧率”发送,只有检测到编码
跟不上(主机经 capture-stats 上报的 build_ms 超出帧预算)或网络跟不上
(RTT 升高、丢帧、拉帧完成率不足)时才自动降帧,恢复后再逐步回到设定值。
ultra(超清)和 lossless(无损)映射到 quality:BGRA 原画 + zlib(无损,
不是不压缩);raw 只保留给显式请求不压缩的客户端。预设和采集帧率都是主机全局的:
一个会话切换会影响同一主机上的所有会话。auto 不是 wire 协议的一部分,由客户端
在画质档位之间自动切换(RTT、拉帧完成率、吞吐量,带迟滞);“平均帧率 = 自适应”
时按档位给出窗口:极速 5–25、流畅/平衡 10–25、清晰/超清/无损 15、游戏 30–60,
负载不足时一律可降到 5。
- 端点:
ws://{host}:{port}/ws/session - 二进制消息:一条 MOLK 包。
- 文本消息:UTF-8 JSON 控制消息。
- 单条消息上限:接收方向 1 MiB(控制消息足够)。
鉴权在应用层完成,不在握手层:
- 连接建立后,服务端发
hello_required(或直接发welcome,若 URL 带有效?token=)。 - 客户端发送:
{"type":"hello","token":"<token>","session":"<session_token>","client":"molink-web","protocol":1}- 令牌正确 →
welcome;错误 →error后以 close code1008关闭。 - 连接后 5 秒内未完成鉴权 → 服务端以
1008 "hello timeout"关闭。 - 错误令牌按来源 IP 计数,与
/api/login共用阶梯封禁额度(见 §5.1);封禁中的来源在握手后立即以1008关闭。
令牌来自 molink-core 启动时的随机生成(32 位十六进制)或 MOLINK_TOKEN / --token。
welcome 会带一个 session_token。断线重连时客户端把它放在 session 字段(token
仍带原验证码),服务端先用会话令牌鉴权,成功则不消耗验证码次数;会话令牌失效
(主机重启)或主机处于锁定状态时,退回用 token 里的验证码鉴权。对已鉴权的连接
重复发 hello 只会重发 welcome,不会再次扣减多次验证码的次数。
没有验证码的客户端可以请求被控端手动同意:
{"type":"request-access","name":"Alice 的浏览器","client":"molink-web"}服务端回 {"type":"access-requested","id":N},连接保持打开(不受 5 秒
hello timeout 限制,审批等待上限 120 秒)。被控端在本机 UI 上允许后,
服务端直接发 welcome,客户端无需再发 hello;拒绝则回 error
(code=access_denied)并关闭。开关关闭时回 approval_disabled。
本机 UI 通过 /api/state 的 access_requests 字段(仅回环地址返回)轮询
待审批列表,通过 POST /api/access/approve?id=N 或 /deny?id=N 决定。
「设置 → 安全 → 允许免密远程请求」对应 /api/settings?access_requests=0|1,
默认开启。锁定状态下请求会被直接拒绝。
服务端 → 客户端
| type | 字段 | 说明 |
|---|---|---|
hello_required |
protocol, service, version |
等待鉴权 |
welcome |
session, session_id, session_token, protocol, service, version, transport, transport_path, transports, transport_ports, tiles, displays, display_index |
鉴权成功;transport 是线路协议(websocket/quic/udp),transport_path 是数据通路(direct/p2p/relay),两者独立;displays 为可切换显示器列表;session_token 用于重连免扣验证码次数 |
ping |
t |
应用层心跳,每 15 秒 |
pong |
t, server_ms |
对客户端 ping 的回应 |
access-requested |
id |
已登记免密请求,等待本机用户审批 |
quality |
mode, format, fps |
画质/帧率切换的确认 |
capture-stats |
fps, target_fps, build_ms |
每秒一次:主机实际发布帧率、目标帧率、单帧构建+编码耗时(EMA,毫秒)。客户端据此区分“屏幕没变化”和“主机编码不过来” |
codec |
av1, wire, decoded, full(可选) |
本地核心代理每秒最多一次的解码统计;full=true 表示线路只传 AV1 全帧 |
error |
code, message |
错误 |
welcome 示例(真实屏幕):
{
"type": "welcome",
"session": "A3F-9K2-X7Q",
"session_id": 1,
"protocol": 1,
"service": "moLink Core",
"version": "0.8.0",
"transport": "websocket",
"transport_path": "direct",
"tiles": {
"codec": "deflate-bgra",
"screen_w": 2880,
"screen_h": 1920,
"tile_w": 256,
"tile_h": 256,
"backend": "x11-shm",
"synthetic": false
},
"input": true,
"input_backend": "xtest",
"clipboard": true,
"clipboard_backend": "x11-selection"
}backend 取值:x11-shm、x11、dxgi-duplication、gdi、secure-desktop、
synthetic。synthetic: true 表示当前是合成测试帧,
不是真实屏幕。codec 反映当前帧实际使用的编码(bgra、deflate-bgra、rgb24、
rgb565、deflate-rgb24 或 deflate-rgb565);quality 是当前画质预设
(smooth/balanced/quality/raw);input 表示本机是否支持输入注入;
clipboard 表示本机是否支持剪贴板同步;can_set_display_mode 表示该采集后端
能否执行 display-mode(secure-desktop、gdi、X11 与合成帧均为 false)。
transport 与 transport_path 是两个正交的轴:前者是线路协议(websocket、
quic、udp、tcp),后者是数据通路(direct = 对端直接连到本 core,
p2p = ICE 打洞后的 UDP,relay = 目录中转)。因此一条“中继上的 QUIC 会话”
应当是 transport_path=relay + transport=quic,而不是一个新的混合取值。
UI 用两个独立标签分别显示。旧 core 没有 transport_path,其 transport=relay
表示通路、transport=udp 表示 P2P,客户端需要兼容这个旧含义。
QUIC 会话(transport=quic)在一条双向 QUIC 流上复用完全相同的 MOLK 包;
文本控制消息包在 packet_type::control(0x7f)里,鉴权仍是第一条 hello,
因为 QUIC 没有 URL 可以带 ?token=。主机在 --quic-port(默认 28420)监听,
/api/transports 中 quic.available=true 时才对外宣告。
客户端 → 服务端
| type | 字段 | 说明 |
|---|---|---|
hello |
token, session(可选) |
鉴权;session 是上次 welcome 给的会话令牌,优先于验证码且不消耗次数 |
request-access |
name, client |
无验证码请求远程访问;需被控端开关开启 |
ping |
t |
客户端主动测延迟;服务端回 pong 并原样带回 t |
pong |
t |
回应服务端心跳 |
request-frame |
— | 请求一帧。服务端返回自上次以来所有未送达的帧;无变化时返回 0 瓦片帧 |
request-keyframe |
— | 请求下一个捕获帧标记为关键帧(客户端丢失基准时使用) |
capture-fps |
fps (1..60) |
调整主机的采集帧率(全局);服务端回 capture-fps |
quality |
mode(smooth/balanced/quality/ultra/lossless/raw/game),可选 fps |
切换主机的编码预设(全局);fps 独立调整采集帧率(与画质无关)。服务端回 quality,带实际生效的 mode、format 与 fps |
display |
index |
切换被控端显示器(全局);服务端回 display 并发送新关键帧 |
display-mode |
width, height |
请求主机切换显示模式;仅当 welcome.tiles.can_set_display_mode 不为 false 时可用(目前仅 DXGI 采集)。成功回 display-mode 并发送关键帧;后端不支持回 display_mode_unsupported,驱动拒绝回 display_mode_failed,两秒内重复请求回 display_mode_busy |
input |
events[] |
指针/键盘事件;需要已鉴权,见 §4.3 |
bye |
— | 正常关闭 |
welcome.displays 的每一项包含 index、name、x、y、width、height、primary;display_index 是当前采集的显示器,默认 0(主显示器)。客户端发送 {"type":"display","index":N} 后,服务端切换采集、按新显示器重算输入坐标,并发布关键帧。显示器列表是主机级共享的:一个会话切换会影响同一主机上的其他会话。
{
"type": "input",
"events": [
{"k": "move", "x": 0.42, "y": 0.31},
{"k": "button", "button": 1, "down": true},
{"k": "button", "button": 1, "down": false},
{"k": "wheel", "dx": 0, "dy": 1},
{"k": "key", "code": "KeyA", "down": true},
{"k": "key", "code": "KeyA", "down": false}
]
}| 字段 | 说明 |
|---|---|
k |
move / button / wheel / key |
x, y |
归一化到 [0,1] 的屏幕坐标;越界事件被拒绝 |
button |
X11 按钮号:1 左、2 中、3 右(4–7 为滚轮,由服务端使用) |
dx, dy |
滚轮格数;dy > 0 向下滚动 |
code |
浏览器 KeyboardEvent.code,如 KeyA、Digit1、ArrowLeft、ShiftLeft |
down |
按下/抬起,默认 true |
- 单条消息最多 64 个事件。
events缺失时,消息顶层对象本身按一个事件解析。- 组合键由客户端分别发送修饰键与主键的按下/抬起,X 服务器负责产生大写与符号。
- 会话断开且没有其它已鉴权会话时,服务端释放所有按下的键与按钮。
- 输入不可用时返回
not_implemented。
X11 的屏幕抓取不包含鼠标光标,服务端单独发送形状与位置:
{"type":"cursor","serial":300,"w":48,"h":48,"hotspotX":7,"hotspotY":7,
"rgba":"<base64>"}
{"type":"cursor-position","x":1617,"y":1006,"visible":true}| 字段 | 说明 |
|---|---|
serial |
光标形状序号;序号变化时才重发 cursor |
w, h |
光标像素尺寸 |
hotspotX, hotspotY |
热点(光标坐标对应的像素) |
rgba |
w*h*4 字节的 RGBA(非预乘),base64 |
x, y |
指针在屏幕坐标系中的位置 |
visible |
为 false 时客户端应隐藏光标 |
- 两条消息随
request-frame的响应发送;形状只在变化时发送。 - 客户端在控制模式开启时通常隐藏该覆盖层,改由本地光标指示位置。
客户端 → 服务端
{"type":"clipboard","action":"set","text":"hello"}
{"type":"clipboard","action":"get"}服务端 → 客户端
{"type":"clipboard","text":"hello from the remote machine"}- 仅支持文本(UTF8_STRING),单条上限 192 KiB。
set让服务端取得 X11 CLIPBOARD 所有权,本地应用随后粘贴即可得到该文本。get异步请求当前所有者;没有所有者或超时返回空字符串。- 剪贴板不可用时返回
not_implemented。
- 帧包按会话独立编号(包头的
seq),每个响应至少包含一个帧包。 - 0 瓦片帧表示"当前没有变化",仍推进包序号,客户端不应重绘。
- 服务端保证:只要客户端收到过关键帧,之后收到的增量帧都是按序的;抓屏侧 保留最近 120 帧 / 128 MiB 的历史,落后时逐帧补发。
- 历史被挤出或落后超过 60 帧时,服务端请求关键帧并发送 0 瓦片帧占位,直到关键帧 到达。抓屏侧每 30 秒也会主动产生一次关键帧。
- 客户端在收到首个关键帧之前不应绘制任何增量帧。
| code | 含义 |
|---|---|
unauthorized |
令牌缺失或错误 |
bad_json |
控制消息不是 JSON 对象 |
unknown_type |
不支持的控制消息类型 |
not_implemented |
功能不可用(本机没有输入注入能力) |
bad_input |
输入事件字段缺失或越界 |
bad_clipboard |
action 不是 set/get |
clipboard_too_large |
剪贴板文本超过 192 KiB |
bad_packet |
二进制消息不是合法 MOLK 包 |
frame_build_failed |
服务端构造帧失败 |
send_failed |
帧入队失败 |
unexpected_packet |
该方向不接受此包类型 |
| 参数 | 值 | 行为 |
|---|---|---|
| 心跳间隔 | 15 s | 服务端发 ping;客户端应回 pong |
| 鉴权超时 | 5 s | 未 hello 的连接以 1008 关闭 |
| 空闲回收 | 5 min | 无任何消息的连接以 1001 "idle timeout" 关闭 |
以下消息都要求会话已鉴权(hello 成功或初始 token 有效)。
聊天
- 客户端 → 核心:
{"type":"chat","action":"send","text":"..."}(1–4096 字节) - 核心 → 所有会话与主机 UI:
{"type":"chat","action":"message","id":N,"from":"remote|host","sender_name":"机器名","sender_id":"设备ID","sender_session":N,"text":"...","ts":epoch_ms} - 客户端请求历史:
{"type":"chat","action":"history"}→{"type":"chat","action":"history","messages":[...]} - 主机 UI 通过
GET /api/chat?since=N轮询、POST /api/chat?text=...回复(仅回环)。 - 发送者身份来自
hello的可选name/device_id;没有设备 ID 的浏览器按来源 IP 标注。sender_session是主机侧会话号,客户端用它把“自己发的”消息右对齐。
文件传输
- 上传:
{"type":"file","action":"offer","name":"a.txt","size":123}→{"type":"file","action":"accept","id":N,"chunk":262144,"dir":"..."};随后用包类型0x12按 offset 顺序发送数据,最后{"action":"complete","id":N}→{"action":"done","id":N,"path":"..."}。文件保存在远程主机~/Downloads/moLink。 - 浏览:
{"action":"browse","path":""}→{"action":"listing","path":"...","root":"...","entries":[{"name":"a","dir":false,"size":1,"mtime":0}]}; 空路径表示主目录,路径被限制在主目录内(含符号链接解析)。 - 下载:
{"action":"request","path":"..."}→{"action":"sending","id":N,"name":"a","size":123}; 每收到一块后发送{"action":"pull","id":N}拉取下一块,核心用0x12回传,最后{"action":"eof","id":N}。分块 256 KiB。 - 远程剪贴板:
{"type":"clipboard","action":"files"}查询当前文件列表;主机剪贴板变化时核心 主动推送{"type":"clipboard","action":"files","files":[{"name","path","size"}]}。{"type":"file","action":"clipboard-set","paths":[...]}把远程文件放进主机剪贴板 (WindowsCF_HDROP,X11 为text/uri-list/x-special/gnome-copied-files), 回复{"type":"file","action":"clipboard","ok":true,"count":N}。 - 本机 UI 的自动文件剪贴板:
GET /api/clipboard-files返回本机剪贴板文件快照 ({"version":N,"files":[{"id","name","size"}]},仅回环);GET /api/clipboard-file?id=返回内容。id 是不透明哈希,文件不在当前剪贴板时返回 404。页面把文件按上面的上传 流程传到远端后,再发clipboard-set,因此远端(包括启用剪贴板重定向的 RDP 客户端) 可以直接粘贴。端到端同步只在本机壳层/本机浏览器可用。
音频
- 客户端 → 核心:
{"type":"audio","action":"start"}/{"type":"audio","action":"stop"}; start 回复{"type":"audio","action":"started","sample_rate":48000,"channels":2,"bits":16}。 - welcome 中
audio表示能力,audio_format给出sample_rate/channels/bits。 核心用包类型0x13发送交错 16-bit PCM。当前实现为 Windows WASAPI 回环采集;其他平台audio=false。
语音通话(不含视频)
- 客户端发起:
{"type":"call","action":"invite"};核心回推事件{"type":"call","action":"ringing|accepted|declined|ended|audio_error","host_microphone":bool,"started_ms":epoch_ms}。 - 主机端通过
GET /api/call轮询状态(idle|ringing|active),用POST /api/call?action=accept|decline|end接听/拒绝/挂断(仅回环)。 - 客户端挂断:
{"type":"call","action":"hangup"}。 - 通话音频使用包类型
0x14,载荷为 48 kHz 单声道 little-endian 16-bit PCM; 客户端发麦克风,主机发本机麦克风。welcome 的call表示该主机是否支持通话。 当前实现为 Windows WASAPI;其他平台call=false。
核心监听 0.0.0.0:28418,但 allow_remote_access 默认关闭:非回环 TCP 连接在 HTTP 解析前立即关闭,已有远程 WebSocket 也会断开。打开该设置后立即生效,无需重启;显式 --host 127.0.0.1 则真正只绑定本机。非回环浏览器还需要 allow_web_client,且必须通过授权码登录。
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| GET | /healthz |
否 | 健康检查,含 service: "moLink Core" |
| GET | /api/health |
否 | 同 /healthz |
| GET | /api/state |
否 | 本机/远程、锁定状态、设备 ID、锁定超时、全部端口 |
| GET | /api/transports |
否 | 传输协商表:可用性、端口、推荐顺序 |
| POST | /api/login |
否 | 用长期/临时授权码登录/解锁;返回 session_token;失败限速 |
| GET | /api/device |
仅回环 | 设备 ID、本机会话 token、临时码状态;锁定时 423 |
| GET | /api/discovery |
是 + 仅回环 | 局域网发现的设备列表 |
| GET | /api/settings |
是 + 仅回环 | 安全设置与长期授权码 |
| POST | /api/settings?allow_remote=&allow_web=&discovery=&name= |
是 + 仅回环 | 修改安全设置 |
| POST | /api/auth/code?mode=&uses=&value= |
是 + 仅回环 | 切换/刷新单次或多次临时码 |
| POST | /api/auth/long-term?value= |
是 + 仅回环 | 设置/生成长期授权码 |
| GET | /api/info |
是 | 版本、绑定地址、能力开关、会话数 |
| GET | /api/sessions |
是 | 当前会话快照 |
| POST | /api/activity |
是 | 记录一次用户活动 |
| POST | /api/ui-state?client=&sessions= |
是 | 上报某窗口打开的会话数 |
| POST | /api/lock-timeout?seconds= |
是 | 修改并持久化空闲锁定时间(0 = 从不) |
| POST | /api/network?tcp=&udp= |
是 | 修改并持久化 TCP/UDP 端口;重启后生效 |
| GET | / |
否 | 配置了 --web-root 时返回前端 index.html,否则返回服务自述 JSON |
鉴权头(二者其一)使用 /api/login 返回的会话 token(不是授权码):
Authorization: Bearer <session_token>
X-MoLink-Token: <session_token>
桌面壳通过回环 /api/device 获取自己的会话 token。WebSocket 的 hello / ?token= 可以携带会话 token 或授权码;代码成功鉴权后会按单次/多次规则消耗。
健康检查响应:
{
"status": "ok",
"service": "moLink Core",
"version": "0.8.0",
"protocol": 1,
"pid": 12345,
"uptime_ms": 2032,
"sessions": 0
}CORS 只对本地开发来源 http://localhost:1420、http://127.0.0.1:1420 开放;核心托管的 UI 与接口同源,不需要 CORS。
peer为回环地址(127.0.0.1、::1、::ffff:127.0.0.1)时视为本机访问,可免验证读取/api/device。- 默认只绑定回环;
allow_remote_access打开后才接受非回环连接,allow_web_client控制非回环浏览器能否加载 UI/登录页。 - 其他来源必须用授权码登录(
POST /api/login);成功后返回session_token,后续 API 使用该 token。授权码分长期码、单次码、多次码:长期码始终有效;单次码用一次后自动换新;多次码按次数扣减,用完后自动换新。 - 没有已鉴权 WebSocket 会话、且所有 UI 窗口上报的
sessions为 0、且空闲超过lock_timeout秒时,核心锁定。 - 锁定期间:除
/api/state、/api/login、/api/health外的/api/*返回423 locked;WShello以1008 locked关闭。 - 验证失败按来源 IP 计数,
/api/login、其他/api/*的错误会话 token 与 WebSockethello共用额度:每轮 5 次失败后锁定,时长依次为 30 秒、2 分钟、10 分钟、1 小时、6 小时;第 6 轮永久封禁。24 小时无新失败重置阶梯;回环地址不计数。 - 临时锁定返回
429与Retry-After(JSON 中同时带retry_after),永久封禁返回403 ip_banned。封禁列表保存在配置目录的banned_ips,删除后重启核心即可解除。
GET /api/transports(无需鉴权,锁定期间也可用)返回当前 build 的传输能力和端口表:
{
"current": "websocket",
"preferred": ["quic","http3","tcp","http2","websocket","udp"],
"ports": {"web":28418,"http3":28419,"quic":28420,"tcp_data":28421,"udp_data":28422},
"transports": [
{"name":"websocket","role":"web","available":true,"secure":false,
"port":28418,"note":"browser baseline; /ws/session"},
{"name":"http2","role":"data","available":true,"secure":false,
"port":28418,"note":"WebSocket over HTTP/2 (RFC 8441) on the web port"},
{"name":"http3","role":"data","available":true,"secure":true,
"port":28419,"note":"WebSocket over HTTP/3 (RFC 9220)"},
{"name":"quic","role":"data","available":true,"secure":true,
"port":28420,"note":"MOLK session stream over QUIC"},
{"name":"tcp","role":"data","available":true,"secure":false,
"port":28421,"note":"MOLK session stream over TCP"},
{"name":"udp","role":"data","available":false,"secure":false,
"port":28422,"note":"direct data channel (planned)"}
]
}- 选择规则:客户端按
preferred顺序取第一个自己支持且available: true的项。浏览器的 WebSocket API 通常只走 h1;桌面对桌面的/ws/direct代理先探测本接口,按 QUIC → TCP → HTTP/2 → WebSocket 的阶梯连接,前一级握手失败才降级。MOLINK_QUIC=0可跳过 QUIC。HTTP/2 会话是 web 端口上的 RFC 8441 extended CONNECT(h2c 先验知识;有证书时 ALPNh2),与 h1 的/ws/session共用同一路径。 http3条目是一项 WebSocket-over-HTTP/3 服务(RFC 9220,ALPNh3,端口 28419):支持它的浏览器或 peer core 可以直接连;普通浏览器仍会落到websocket。/api/transports不需要鉴权,peer core 在建立数据通道前就靠它协商。远程访问关闭时 QUIC 监听只绑回环地址,非回环调用方看到quic.available=false。- WebSocket
welcome同时带transport、transport_path、transports与transport_ports,已连接的客户端不需要再走一次 HTTP。 - 中继端点由目录的
POST /v1/connect/relay返回(relay.endpoints:协议、端口、是否 TLS),客户端按quic → http3 → tcp → http2 → udp → websocket选择;TCP/QUIC 中继握手是一行{"connect_id","token"}后接原始 MOLK 流,HTTP/2 中继是设备端口上的 RFC 8441 WebSocket(TLS/ALPNh2或 h2c)承载同一行握手与消息,HTTP/3 中继用 WebSocket-over-H3 承载同一行握手与消息,UDP 中继用同一行 JSON 握手,之后是按 ICE 相同格式分片的 1200 字节数据报。 - 内网直连:
GET /v1/devices?id=在请求方与目标同一公网 IP(或回环)时返回address、quic_port、tcp_port、tls_fingerprint、tls_cert;客户端先直连 QUIC、再试 TCP,成功后关闭 ICE/中继。端口为 0 表示该监听未启动;心跳会用 0 清除已失效的端口。 - 证书固定:
GET /api/transports带tls_fingerprint/tls_cert,POST /v1/connect/relay也带tls_cert;客户端把证书写入临时 CA 文件并校验指纹后,用PEER校验 QUIC/TCP/HTTP3 数据通道。TCP 数据端口是 TLS-only。 - 浏览器 WebSocket API 基于 HTTP/1.1,h2/h3 不会替代它;h2c 只对桌面/命令行客户端有意义,HTTP/3 需要 TLS 证书与独立 QUIC 前端。
- 中继目前用 WebSocket 到目录服务;通路(
relay)与协议(websocket/quic)是两回事,目录端支持 QUIC 中转后只需把protocol换成quic,会话层不变。
molink-core [--host <addr>] [--port <n>] [--http3-port <n>] [--quic-port <n>]
[--tcp-data-port <n>] [--udp-port <n>] [--token <secret>]
[--allow-remote] [--parent-pid <pid>] [--print-ready]
--host 默认 0.0.0.0。端口段:28418 网页/API/WS、28419 HTTP/3、
28420 QUIC 数据、28421 TCP 数据、28422 UDP 数据。命令行参数优先于
/api/network 与 /api/lock-timeout 写入配置目录的持久化值。
--print-ready 在开始监听后向 stdout 输出一行 JSON,壳层据此得到端口与令牌:
{"event":"ready","service":"moLink Core","version":"0.8.0","protocol":1,
"pid":12345,"host":"127.0.0.1","port":28418,"token":"<32 hex>",
"client_token":"<32 hex>","session_endpoint":"/ws/session",
"transport":"websocket",
"transports":["websocket"],
"ports":{"web":28418,"http3":28419,"quic":28420,
"tcp_data":28421,"udp_data":28422},
"synthetic_frames":true}token 是只给本机壳层的会话 token,不是授权码;授权码通过 /api/device / 设置页展示。
client_token 只通过 ready 行交给壳层。allow_web_client=0 时,回环 Web UI、/api/state、/api/login、/api/device、/api/transports 需要请求头 X-MoLink-Client 或初始 URL 的 client=;WebSocket 不受这道门限制(浏览器 WS 不能加自定义头),仍必须用授权码或会话 token 鉴权。
--parent-pid 让核心每秒检查一次父进程;父进程消失则自行退出,避免壳层被杀后留下孤儿进程。
kProtocolVersion只在字节布局变化时递增。- 新增可选控制消息类型不需要递增协议版本;旧客户端应把未知
type当作可忽略/报错处理。 - 新增包类型必须使用新值,不得复用被弃用的值。
- 修改
codec编号、包头/帧头/瓦片表项布局,必须递增协议版本并同步更新 C++ 与 TS 两份实现和本文件。
桌面核心在设置中配置 server_url / server_invite 后,会向目录服务注册并每 30 秒发送心跳;
服务端 API 见独立仓库
Antruly/molink-server。核心侧行为:
- 配置保存在
~/.config/molink/settings的server_url、server_invite、server_enabled,核心线程每秒轮询,修改后即时生效。server_url为空且总开关开启时 使用内置目录https://molink.ssvr.top:28430(MOLINK_DEFAULT_SERVER_URL)。 - 注册得到的凭据保存在
~/.config/molink/server.json(url、device_id、display_id、token、domain、domain_valid,POSIX 下权限 0600);重启后复用同一 ID,心跳失败 (401/404)才重新注册。 GET /api/server(仅回环):url、custom_url、default_url、using_default、invite、enabled、registered、online、device_id、display_id、domain、domain_valid、last_error。POST /api/server?url=&invite=&enabled=(仅回环):保存配置并触发重新注册;enabled独立于自定义 URL,可只关远程目录而保留地址。POST /api/domain(仅回环,JSON{action, code?}):join加入他设备的域组码,rotate让目录为本机签发新码(本机是旧码所有者时旧码立即失效)。GET /api/domain/devices(仅回环):返回同域组设备列表;码已失效时返回unknown_domain。GET /api/state增加device_name、avatar、platform与server对象(字段同上, 但不含 token/invite)。- 通过设备 ID 连接:客户端使用已配置的 server URL 请求
GET <server>/v1/devices?id=<ID>,拿到address/peer_ip/port/name后按正常的直连 会话流程鉴权;目录只做寻址,不转发媒体。 - 目录客户端支持
https://:使用系统信任库,或在设置中提供server_ca(PEM 路径) 以信任私有 CA。服务端用--tls-cert/--tls-key开启 TLS,TLS 上只宣告 HTTP/1.1。