跳转至

MITM 独立 worker 协议

本文档定义了 sing-box 为外部 MITM worker 使用的 worker 侧协议。

该协议与具体运行时无关。sing-box 只要求一个已连接的 Unix 域套接字字节流,不假设特定语言或嵌入式运行环境。

生命周期

  1. sing-box 与 worker 主进程建立一条 Unix 域套接字流。
  2. StreamID = 0 专用于启动协商。
  3. sing-box 发送 HELLO
  4. worker 回复 HELLO
  5. sing-box 发送 CAPS,声明选中的协议版本和协商后的能力位集。
  6. worker 回复完全匹配的 CAPS
  7. 只有双方 CAPS 完全一致后,才允许开始真实流量。
  8. 每个被拦截的请求或 HTTP/2 流都在同一条 socket 上使用一个逻辑 StreamID

如果协商失败,sing-box 会在服务真实流量前拒绝该 worker。

帧格式

所有帧都使用相同的 9 字节头:

StreamID[4] + Type[1] + Length[4]
  • StreamID,大端序 32 位无符号整数。
  • Type,8 位无符号帧类型。
  • Length,大端序 32 位无符号负载长度。

StreamID = 0 仅用于会话协商。

帧类型

名称
0x01 control
0x02 request_headers
0x03 request_body
0x04 request_trailers
0x05 response_headers
0x06 response_body
0x07 response_trailers

request_bodyresponse_body 的负载是原始字节。其余负载都是 UTF-8 JSON 对象,并且严格拒绝未知字段。

控制帧

control 负载以一个额外的子类型字节开头,后面跟一个 JSON 对象。

子类型 作用域
0x01 HELLO StreamID = 0 的启动协商
0x02 OPEN 每个逻辑流的打开与 worker 决策
0x03 END 半关闭或完整关闭一个逻辑流
0x04 ABORT 中止或重置一个逻辑流
0x05 ERROR 结构化协议或运行时错误
0x06 WINDOW 可选的流控额度更新
0x07 CAPS 会话启动阶段的能力确认

未知的关键控制子类型属于致命协议错误。

版本与能力协商

HELLO

{
  "min_version": 1,
  "max_version": 1,
  "required_capabilities": 217,
  "supported_capabilities": 511
}
  • min_version / max_version,支持的闭区间版本范围。
  • required_capabilities,最终协商结果中必须存在的能力位。
  • supported_capabilities,该端理解的完整能力位集合。

sing-box 会拒绝以下情况:

  • 双方版本范围没有交集。
  • 必需能力位不是支持能力位的子集。
  • 出现未知能力位。

CAPS

{
  "version": 1,
  "capabilities": 255
}

双方必须发送完全一致的选定版本与协商后能力位集。

任何不匹配都会导致启动失败。

能力位

名称 含义
1 << 0 request_headers 可交换请求头
1 << 1 request_body 允许请求体帧
1 << 2 request_trailers 允许请求尾部帧
1 << 3 response_headers 可交换响应头
1 << 4 response_body 允许响应体帧
1 << 5 response_trailers 允许响应尾部帧
1 << 6 direct_response worker 可以直接短路返回响应
1 << 7 pass_through worker 可以显式放弃拦截
1 << 8 window_updates 允许 WINDOW

如果某个帧依赖的能力没有被协商成功,worker 不得发送该帧。

打开逻辑流

sing-box 会在非零 StreamID 上用 OPEN 打开每个逻辑流:

{
  "protocol": "http/1.1",
  "module": "capture",
  "request_body": true,
  "request_trailers": true,
  "response_body": true,
  "response_trailers": true,
  "is_connect": false,
  "is_extended_connect": false
}

worker 必须返回自己的 OPEN 决策:

{"decision":"intercept"}

允许值:

  • intercept
  • direct_response
  • pass_through

direct_response 需要协商到 direct_response 能力。

pass_through 需要协商到 pass_through 能力。

头部负载

sing-box → worker 请求头

{
  "method": "GET",
  "scheme": "https",
  "authority": "example.com",
  "path": "/v1/data?ok=1",
  "headers": {
    "accept": ["application/json"]
  }
}

worker → sing-box 请求变更

{
  "authority": "rewritten.example",
  "path": "/internal/data?ok=1",
  "headers": {
    "x-policy": ["capture"]
  }
}

请求变更帧中不包含 method。任何尝试发送 methodoutboundtlsfsm 或其他未知字段的行为都会被拒绝。

响应头

{
  "status_code": 200,
  "headers": {
    "content-type": ["application/json"]
  }
}

对于 direct_response,第一帧 response_headers 必须携带 status_code

Trailers

{
  "headers": {
    "grpc-status": ["0"]
  }
}

允许的 worker 输出

worker 只能执行以下操作:

  • 修改请求头、请求体和请求尾部。
  • 修改响应头、响应体和响应尾部。
  • 返回直接响应。
  • 中止或重置逻辑流。
  • 显式返回 pass_through
  • 仅在协商到 window_updates 后发送 WINDOW

禁止的 worker 输出

worker 不得尝试在显式合同之外控制 sing-box。

会被拒绝的示例:

  • 修改请求方法。
  • 选择出站或修改出站标签。
  • 修改 TLS 引擎或证书。
  • 发送泛化的 FSM、调度器或传输层控制。
  • 在普通头映射中注入 HTTP 伪头。
  • 通过普通头映射修改 Host,而不是使用 authority
  • 发送未知能力位。
  • 在 JSON 负载中携带未知字段。
  • 出现错误流转,例如在 response_headers 之前发送 response_body

流关闭与错误

END

{"target":"request"}

有效 target

  • request
  • response
  • stream

ABORT

{
  "target": "stream",
  "code": "policy_denied",
  "message": "blocked by worker policy"
}

ERROR

{
  "code": "bad_frame",
  "message": "unknown critical control subtype"
}

WINDOW

{
  "target": "response",
  "credit": 32768
}

只有在协商到 window_updates 后,WINDOW 才合法。

失败语义

  • 协商失败属于会话级致命错误。sing-box 会关闭 worker 连接,不接收流量。
  • 解析失败、未知关键子类型、禁止变更或不可能的状态流转,都属于确定性协议失败。
  • 对于已支持的 MITM 流量,sing-box 遇到 worker 协议失败时会 fail-closed,而不是静默绕过策略。
  • pass_through 必须显式返回。缺少有效 worker 决策不等于隐式放行。

脱敏要求

worker 应当把所有解密后的 HTTP 内容视为敏感数据。

  • sing-box 的常规观测输出不会记录解密后的消息体字节。
  • sing-box 的常规观测输出不会记录 AuthorizationProxy-AuthorizationCookieSet-Cookie、各类 token 头以及 API key 等敏感头。
  • 除非运维方单独构建了安全审计链路,否则 worker 自身日志也应遵循相同原则。

结构化 MITM 事件中可见的请求元数据仅限于模块标签、协议、决策、authority/path/query、安全头以及消息体是否存在或大小等脱敏字段。

示例收发流程,带请求改写的拦截

S→W  stream=0  control/HELLO   {"min_version":1,"max_version":1,...}
W→S  stream=0  control/HELLO   {"min_version":1,"max_version":1,...}
S→W  stream=0  control/CAPS    {"version":1,"capabilities":255}
W→S  stream=0  control/CAPS    {"version":1,"capabilities":255}

S→W  stream=1  control/OPEN    {"protocol":"http/1.1","module":"capture","request_body":true}
S→W  stream=1  request_headers {"method":"GET","scheme":"https","authority":"example.com","path":"/v1/data"}
W→S  stream=1  control/OPEN    {"decision":"intercept"}
W→S  stream=1  request_headers {"authority":"rewritten.example","path":"/internal/data"}
S→W  stream=1  request_body    <bytes>
S→W  stream=1  control/END     {"target":"request"}
W→S  stream=1  response_headers {"status_code":200,"headers":{"x-worker":["1"]}}
W→S  stream=1  response_body    <bytes>
W→S  stream=1  control/END      {"target":"response"}

示例收发流程,直接响应

S→W  stream=7  control/OPEN      {"protocol":"h2","module":"capture"}
S→W  stream=7  request_headers   {"method":"GET","scheme":"https","authority":"api.example","path":"/healthz"}
W→S  stream=7  control/OPEN      {"decision":"direct_response"}
W→S  stream=7  response_headers  {"status_code":503,"headers":{"content-type":["text/plain"]}}
W→S  stream=7  response_body     <"worker unavailable">
W→S  stream=7  control/END       {"target":"response"}

示例收发流程,显式 pass-through

S→W  stream=11 control/OPEN      {"protocol":"http/1.1","module":"capture"}
S→W  stream=11 request_headers   {"method":"CONNECT","authority":"db.example:443"}
W→S  stream=11 control/OPEN      {"decision":"pass_through"}
W→S  stream=11 control/END       {"target":"stream"}

独立 worker 的实现说明

  • 把 UDS 连接当成有序字节流处理,不要依赖 socket 层的消息边界。
  • StreamIDLength 都使用大端序解析。
  • StreamID = 0 只用于协商。
  • 你的解析器也应拒绝或显式暴露未知字段,不要静默忽略合同变化。
  • 不要尝试逆向控制 sing-box 的内部实现。如果合同没有暴露该能力,worker 就不能控制它。