Skip to content

Commit 4a3ff64

Browse files
committed
add revm dockerd docker api proxy doc
Signed-off-by: ihexon <14349453+ihexon@users.noreply.github.com>
1 parent 56722ec commit 4a3ff64

6 files changed

Lines changed: 362 additions & 0 deletions

File tree

.idea/.gitignore

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/go.imports.xml

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/ihexon.github.io.iml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
---
2+
title: "revm 中的 TunnelHostUnixToGuest 设计逻辑"
3+
summary: "从设计角度记录 revm container mode 中 TunnelHostUnixToGuest 的位置:为什么需要 host Unix socket 到 guest TCP 的代理、为什么通过 gvproxy 建 tunnel,以及为什么 CloseWrite 是流式 Docker/Podman API 的关键语义。"
4+
description: "从设计角度记录 revm container mode 中 TunnelHostUnixToGuest 的位置:为什么需要 host Unix socket 到 guest TCP 的代理、为什么通过 gvproxy 建 tunnel,以及为什么 CloseWrite 是流式 Docker/Podman API 的关键语义。"
5+
date: 2026-05-13
6+
draft: false
7+
categories:
8+
- "linuxvm"
9+
tags:
10+
- "revm"
11+
- "dockerd"
12+
- "gvproxy"
13+
- "podman"
14+
- "docker"
15+
- "network"
16+
---
17+
18+
`TunnelHostUnixToGuest` 是一个Podman API 代理函数,它只做一件事:
19+
20+
把 host 上一个 Unix socket 收到的连接,通过 gvproxy tunnel 转发到 guest VM 里的 Podman API TCP 端口。
21+
22+
这篇文章记录几个关键的设计点:host/guest 边界、CLI 兼容性、网络后端隔离、连接生命周期,以及 Docker/Podman hijack stream 里的 half-close 语义。
23+
24+
<!--more-->
25+
26+
## 问题背景
27+
28+
`revm` container mode 想提供一个轻量的容器 VM:host 上运行 `docker` / `podman` CLI,真正的容器环境跑在 libkrun 启动的 Linux guest 里。
29+
30+
从用户视角看,它应该像这样工作:
31+
32+
```sh
33+
./dockerd --id dev
34+
35+
export DOCKER_HOST=unix://$HOME/.cache/revm/dev/socks/podman-api.sock
36+
docker ps
37+
docker run --rm alpine uname -a
38+
```
39+
40+
这里的 `dockerd``revm` 的 CLI 入口,不是 Docker 官方的 daemon。VM 里真正响应容器 API 的是 Podman API service。Podman 提供 Docker-compatible API,因此 Docker CLI 可以通过这个 socket 工作。
41+
42+
这里有一个接口形态不匹配的问题:
43+
44+
```text
45+
Docker/Podman CLI 习惯连接 host 上的 Unix socket
46+
Podman API 实际运行在 guest VM 里的 TCP 端口
47+
```
48+
49+
`TunnelHostUnixToGuest` 就是在这个不匹配处做边界适配。
50+
51+
## 核心链路
52+
53+
container mode 下的代理链路可以概括为:
54+
55+
{{< mermaid >}}
56+
flowchart LR
57+
cli["docker / podman CLI"]
58+
hostSock["host podman-api.sock"]
59+
tunnelFn["TunnelHostUnixToGuest"]
60+
gvproxySock["host gvproxy socket"]
61+
gvproxy["gvproxy"]
62+
guestAPI["guest Podman API<br/>192.168.127.2:port"]
63+
64+
cli --> hostSock
65+
hostSock --> tunnelFn
66+
tunnelFn --> gvproxySock
67+
gvproxySock --> gvproxy
68+
gvproxy --> guestAPI
69+
{{< /mermaid >}}
70+
71+
这条链路里有三个重要边界:
72+
73+
```text
74+
CLI 边界:host CLI 只看到 Unix socket
75+
VM 边界:容器 API 实际在 guest 内
76+
网络边界:host 进程不能直接假设 guest 网络如何实现
77+
```
78+
79+
`TunnelHostUnixToGuest` 的价值在于把这三个边界收在一个很窄的地方处理。外部 CLI 不需要知道 VM,guest Podman API 不需要知道 host Unix socket,gvproxy 负责具体的 host/guest 网络穿透。
80+
81+
## 为什么 host 上要暴露 Unix socket
82+
83+
Docker CLI 和 Podman CLI 都天然支持 Unix socket:
84+
85+
```sh
86+
export DOCKER_HOST=unix:///path/to/socket
87+
export CONTAINER_HOST=unix:///path/to/socket
88+
```
89+
90+
因此 revm 选择在 host 上暴露一个 `podman-api.sock`,而不是要求用户连接某个随机 TCP 端口。
91+
92+
这个选择有几个好处:
93+
94+
```text
95+
符合 Docker/Podman CLI 的默认使用模型
96+
避免在 host 上开放额外 TCP 监听
97+
socket 文件可以自然跟随 session 目录管理
98+
权限可以用文件系统权限表达
99+
```
100+
101+
也就是说,host Unix socket 是对用户友好的 API surface。它是 revm 对外承诺的接口,而不是内部实现细节。
102+
103+
## 为什么 guest 侧是 TCP
104+
105+
在 gvisor 网络模式下,guest 里的 Podman API 监听在一个 TCP 地址上。revm 里通常是:
106+
107+
```text
108+
192.168.127.2:<podman-api-port>
109+
```
110+
111+
这不是偶然的。gvproxy/gvisor-tap-vsock 本来就是在 host 和 guest 之间提供虚拟网络能力。通过它转发到 guest TCP 端口,比尝试直接暴露 guest Unix socket 更符合这套网络模型。
112+
113+
所以最终形成了一个有意识的接口转换:
114+
115+
```text
116+
host Unix socket -> gvproxy tunnel -> guest TCP port
117+
```
118+
119+
`TunnelHostUnixToGuest` 的名字也正好描述了这个方向:从 host 侧 Unix socket,到 guest 里的服务。
120+
121+
## 为什么中间需要 gvproxy tunnel
122+
123+
host 进程不能直接 `Dial("tcp", "192.168.127.2:port")` 并假设它一定能工作。guest 的地址存在于 gvisor-tap-vsock 管理的虚拟网络里,host/guest 通信需要通过 gvproxy 的控制 socket 建立 tunnel。
124+
125+
所以 `TunnelHostUnixToGuest` 的每条连接大致经历这些步骤:
126+
127+
{{< mermaid >}}
128+
flowchart TD
129+
accept["accept client connection"]
130+
dial["dial gvproxy socket"]
131+
request["request tunnel"]
132+
note["POST /tunnel?ip=guest&port=podman"]
133+
bridge["bridge two streams"]
134+
finish["finish on EOF or cancellation"]
135+
136+
accept --> dial
137+
dial --> request
138+
request -.-> note
139+
request --> bridge
140+
bridge --> finish
141+
{{< /mermaid >}}
142+
143+
这里的 `request tunnel` 对应的是 gvproxy 的 `/tunnel` 能力。revm 不需要理解 gvisor 网络栈细节,只需要告诉 gvproxy:
144+
145+
```text
146+
请把这条连接转发到 guest 的 192.168.127.2:<podman-api-port>
147+
```
148+
149+
这体现了一个很重要的设计原则:revm 不越过 gvproxy 直接操作 guest 网络,而是把 guest 网络穿透交给网络后端。
150+
151+
## TunnelHostUnixToGuest 的职责边界
152+
153+
我觉得这段代码最值得保留的设计点,是它的职责足够窄。
154+
155+
它做:
156+
157+
```text
158+
创建 host Unix listener
159+
accept CLI 连接
160+
为每条连接 dial gvproxy
161+
通过 gvproxy 建立到 guest IP:port 的 tunnel
162+
双向复制字节
163+
处理 context cancellation
164+
传播 half-close
165+
```
166+
167+
它不做:
168+
169+
```text
170+
不解析 Docker API
171+
不理解 Podman API
172+
不关心容器命令是什么
173+
不管理 guest Podman 生命周期
174+
不决定网络模式如何实现
175+
```
176+
177+
这很重要。因为 Docker API 里既有普通 HTTP 请求,也有 attach/exec 这种 hijacked stream。如果 proxy 层开始理解上层协议,很容易把一个简单的字节隧道写成半个 Docker daemon。
178+
179+
`TunnelHostUnixToGuest` 的设计哲学是:只做传输层适配,不进入应用层语义。
180+
181+
## 为什么不是一个通用 TCP proxy
182+
183+
它确实很像通用 proxy,但它不是完全通用的 TCP proxy。
184+
185+
它服务的是一个明确场景:
186+
187+
```text
188+
host Docker/Podman CLI
189+
通过 Unix socket
190+
访问 guest Podman API
191+
并且需要支持 Docker hijack stream
192+
```
193+
194+
因此它的设计不是“抽象到可以代理世界上一切 TCP 连接”,而是“足够通用地转发字节,同时准确保留 Docker/Podman CLI 需要的连接语义”。
195+
196+
这也是 `CloseWrite` 变得关键的原因。
197+
198+
## CloseWrite 不是细节
199+
200+
在普通 request/response HTTP 里,很多人会把连接生命周期想得很简单:
201+
202+
```text
203+
请求发完
204+
响应读完
205+
连接关闭
206+
```
207+
208+
但 Docker/Podman API 里有 hijacked stream。典型场景是:
209+
210+
```sh
211+
docker attach
212+
docker exec -i
213+
docker run -it
214+
```
215+
216+
这些场景里,连接是双向流:
217+
218+
```text
219+
stdin 方向:CLI -> guest process
220+
stdout/stderr 方向:guest process -> CLI
221+
```
222+
223+
这两个方向的生命周期不一定同时结束。一个非常典型的状态是:
224+
225+
```text
226+
stdin 已经 EOF
227+
stdout/stderr 还要继续返回
228+
```
229+
230+
如果 proxy 在 stdin EOF 时直接 `Close()` 整条连接,就会把 stdout/stderr 也切断。这会造成输出截断。
231+
232+
如果 proxy 完全不传播 stdin EOF,guest 里的进程可能永远不知道输入结束。例如:
233+
234+
```sh
235+
echo hello | docker exec -i container cat
236+
```
237+
238+
`cat` 需要看到 stdin EOF 才能退出。如果 EOF 停在 proxy 这里,guest 进程可能继续等输入。
239+
240+
正确语义是 half-close:
241+
242+
```text
243+
stdin EOF -> CloseWrite(tunnelConn)
244+
stdout/stderr 方向继续保留
245+
```
246+
247+
所以 `CloseWrite` 不是一个“小优化”,而是 Docker/Podman stream 能否正确结束的协议语义。
248+
249+
## Docker CLI 也是这么想的
250+
251+
Docker CLI 的 hijack stream 逻辑也是这个模型。
252+
253+
它会同时启动 input 和 output copy。input copy 结束后,Docker CLI 会调用 hijacked response 的 `CloseWrite()`,向 daemon 表达:
254+
255+
```text
256+
我不会再写 stdin 了
257+
但我还要继续读 stdout/stderr
258+
```
259+
260+
之后,如果还有 output stream,Docker CLI 会继续等待 output 结束,而不是因为 input 结束就退出。
261+
262+
也就是说,Docker CLI 本身就依赖 half-close:
263+
264+
{{< mermaid >}}
265+
sequenceDiagram
266+
participant CLI as Docker CLI
267+
participant Proxy as revm proxy
268+
participant Guest as guest Podman API
269+
270+
CLI->>Proxy: stdin bytes
271+
Proxy->>Guest: forward stdin bytes
272+
CLI-->>Proxy: stdin EOF / CloseWrite
273+
Proxy-->>Guest: CloseWrite tunnel side
274+
Guest->>Proxy: stdout / stderr continues
275+
Proxy->>CLI: forward output
276+
Guest-->>Proxy: output EOF
277+
Proxy-->>CLI: CloseWrite client side
278+
{{< /mermaid >}}
279+
280+
因此 revm proxy 的责任不是隐藏 half-close,而是把 half-close 正确传过去。
281+
282+
## 设计总结
283+
284+
`TunnelHostUnixToGuest` 的设计可以压缩成一句话:
285+
286+
```text
287+
在不理解 Docker/Podman API 的前提下,把 host CLI 期望的 Unix socket 连接,可靠地映射成 guest Podman API 的 TCP stream。
288+
```
289+
290+
它的几个核心取舍是:
291+
292+
```text
293+
对外暴露 Unix socket,因为 CLI 生态天然支持
294+
对内使用 guest TCP,因为 gvisor/gvproxy 的网络模型适合这样转发
295+
通过 gvproxy tunnel 穿过 host/guest 网络边界
296+
只做字节转发,不解析 Docker API
297+
保留 half-close,因为 Docker hijack stream 依赖它
298+
用 context cancellation 统一回收连接资源
299+
把不支持的网络组合挡在更早的配置阶段
300+
```
301+
302+
所以这段代码的重点不是“怎么 copy 两个 conn”,而是边界设计:
303+
304+
{{< mermaid >}}
305+
flowchart TD
306+
api["host-facing API<br/>Unix socket"]
307+
adapter["TunnelHostUnixToGuest<br/>boundary adapter"]
308+
backend["network backend<br/>gvproxy tunnel"]
309+
guest["guest service<br/>Podman API TCP"]
310+
stream["stream semantics<br/>half-close"]
311+
312+
api --> adapter
313+
adapter --> backend
314+
backend --> guest
315+
stream -.-> adapter
316+
{{< /mermaid >}}
317+
318+
只要这个边界保持干净,revm 的 container mode 就可以继续把复杂性压在内部,同时让外部 CLI 看到一个简单、熟悉的本地 socket。

0 commit comments

Comments
 (0)