- 云原生
【免费下载链接】kubevirt
Kubernetes Virtualization API and runtime in order to define and manage virtual machines.
本指南以 KubeVirt 仓库中 vendored 的github.com/mdlayher/vsock库为主体,系统讲解如何用 Go 编写基于 Linux VM sockets(AF_VSOCK)的宿主机(hypervisor)与虚拟机之间的通信程序。读完本文,你将掌握vsock.Dial、vsock.Listen、vsock.ContextID等核心 API 的用法、地址模型(CID + Port)、底层系统调用实现,并了解 KubeVirt 如何在 virt-handler 中通过该库承载 gRPC over vsock 服务。
一、什么是 VM Sockets:宿主机与虚拟机之间的原生通信通道
vsock库(vendor/github.com/mdlayher/vsock)为 Go 提供对 Linux VM sockets(AF_VSOCK)的访问能力,用于宿主机与其虚拟机之间的通信,采用 MIT 许可证。VM sockets 是 Linux 内核提供的一类地址族,其通信端点通过CID(Context ID)+ Port二元组寻址,而不是传统的 IP 地址 + 端口。与走虚拟网络的 TCP/UDP 相比,vsock 直连 virtio-vsock 设备,路径更短、语义更贴近"宿主机 ↔ 虚拟机"的一对一关系。
在 KubeVirt 中,该库是 virt-handler 与虚拟机内 agent 建立安全通信通道的基础设施之一:virt-handler 通过vsock在宿主机命名空间内监听固定端口,虚拟机内进程则以固定的本地 CID 主动拨号连接,进而承载 gRPC/TLS 流量。
从源码结构看(vsock.go),库当前预留了Config结构体作为未来扩展点,目前为空、传nil或空结构体均可:
// Config contains options for a Conn or Listener. type Config struct{}二、核心 API 全景:与 net 包完全对齐的接口设计
vsock包的设计理念是"复用 Go 标准库net包的全部接口约定"(见 doc.go):
*Addr实现net.Addr*Conn实现net.Conn*Listener实现net.Listener
这意味着任何接受net.Conn/net.Listener的 Go 代码(如 gRPC 的server.Serve(listener))都可以直接使用 vsock 连接,KubeVirt 正是这样把vsock.Listener直接喂给了grpc.Server。
2.1 服务端:Listen 系列
// Listen 监听一个面向连接的 VM sockets 端口 func Listen(port uint32, cfg *Config) (*Listener, error) // ListenContextID 显式指定 context ID 监听(高级用法) func ListenContextID(contextID, port uint32, cfg *Config) (*Listener, error) // FileListener 从已有 os.File(如 systemd socket 激活)创建 Listener func FileListener(f *os.File) (*Listener, error)要点(依据 vsock.go):
Listen(port, cfg)会自动探测本机 context ID(内部调用ContextID()后转调ListenContextID);需要绑定Local等特殊 context ID 的高级场景请直接用ListenContextID。- 传入
port = 0时由内核自动分配端口,随后通过Listener.Addr()获取实际地址。 FileListener面向 systemd socket 激活等外部机制注入的文件描述符;在 Linux 实现中(listener_linux.go),newListener会通过Getsockname校验地址族确实是SockaddrVM,避免误用 TCP 等其他 socket 类型。- 不再使用时必须调用
Close()释放资源;Close()不会关闭已经Accept出来的连接。
2.2 客户端:Dial
// Dial 向指定 context ID 的端口发起面向连接的拨号 func Dial(contextID, port uint32, cfg *Config) (*Conn, error)拨号方向语义(见 vsock.go):
- 宿主机 → 虚拟机:指定目标 VM 的 context ID;
- 虚拟机 → 宿主机进程:使用
Host(0x2)与宿主机上普通进程通信; - 虚拟机 → 宿主机 hypervisor 进程:使用
Hypervisor(0x0),注意它只指向 hypervisor 进程本身,绝大多数场景应使用Host。
2.3 能力探测:ContextID
func ContextID() (uint32, error)ContextID返回本机在 vsock 网络中的 context ID。Linux 实现(fd_linux.go)打开/dev/vsock设备后,通过ioctl(IOCTL_VM_SOCKETS_GET_LOCAL_CID)取得本机 CID:
func contextID() (uint32, error) { f, err := os.Open(devVsock) if err != nil { return 0, err } defer f.Close() return unix.IoctlGetUint32(int(f.Fd()), unix.IOCTL_VM_SOCKETS_GET_LOCAL_CID) }如果内核模块不可用、无访问权限或系统不支持 VM sockets,该函数会返回错误——因此它也是"当前系统能否使用 vsock"的直接探测手段。
三、地址模型与内置常量
Addr是 vsock 端点地址(vsock.go):
type Addr struct { ContextID, Port uint32 }Addr.String()会根据 CID 语义输出可读地址:hypervisor(0):1、local(1):1、host(2):1,其余 CID 一律显示为vm(cid):port。Network()固定返回"vsock"。
包内预定义的关键 CID 常量(vsock.go):
| 常量 | 值 | 含义 |
|---|---|---|
vsock.Hypervisor | 0x0 | 指向宿主机上的 hypervisor 进程(注意并非宿主机上的任意进程) |
vsock.Local | 0x1 | 同一台机器上的对端 socket,可替代 UNIX socket,常用于测试 |
vsock.Host | 0x2 | 宿主机上的普通进程,是虚拟机内拨号的首选目标 |
此外,/dev/vsock是宿主机与虚拟机两侧都会暴露的设备节点(devVsock = "/dev/vsock"),它是 VM sockets 操作(如 ioctl 获取 CID)的入口。
四、Linux 底层实现:从 API 到系统调用的完整链路
vsock的 Linux 实现位于 conn_linux.go 与 listener_linux.go(均有//go:build linux约束),底层依赖github.com/mdlayher/socket与golang.org/x/sys/unix。
4.1 拨号链路(Dial)
c, err := socket.Socket(unix.AF_VSOCK, unix.SOCK_STREAM, 0, "vsock", nil) sa := &unix.SockaddrVM{CID: cid, Port: port} rsa, err := c.Connect(context.Background(), sa)关键点:
- 以
SOCK_STREAM创建面向连接的流式 socket,地址族为AF_VSOCK; - 对端地址使用内核结构
unix.SockaddrVM{CID, Port}; - 连接成功后通过
Getsockname/getpeername还原本地与对端Addr组装成Conn; - 由于
github.com/mdlayher/socket处理了非阻塞connect(2)的SO_ERROR检查(见 CHANGELOG v1.0.1),拨号错误能被可靠识别。
4.2 监听链路(Listen)
if port == 0 { port = unix.VMADDR_PORT_ANY } if err := c.Bind(&unix.SockaddrVM{CID: cid, Port: port}); err != nil { ... } if err := c.Listen(unix.SOMAXCONN); err != nil { ... }关键点:
- 传
0时映射为内核常量VMADDR_PORT_ANY,由内核分配空闲端口; - 监听队列深度直接使用
unix.SOMAXCONN; - 任何系统调用失败都会先关闭 socket 再返回,避免资源泄漏(源码中的
defer式关闭模式:if err := ...; err != nil { _ = c.Close(); return nil, err })。
4.3 错误处理约定
Conn/Listener的全部操作错误都会被包装为net.OpError(带Op、Net: "vsock"、Source/Addr字段),并统一语义:io.EOF与ENOTCONN归一为io.EOF;EBADF与"use of closed"归一为use of closed network connection(vsock.go)。特别地,对/dev/vsock的权限错误不做解包,保留路径上下文以便定位。因此上层代码可以像处理 TCP 错误一样处理 vsock 错误。
4.4 非 Linux 平台的行为
非 Linux 构建使用 vsock_others.go(//go:build !linux):所有函数返回统一的errUnimplemented(vsock: not implemented on <GOOS>),保证程序在 Windows 等平台仍可编译、只是运行时会得到明确的"不支持"错误,而非编译失败。
五、稳定性与版本策略:可直接放心依赖的 v1 API
按原 README 的 Stability 章节(README.md):
- 该包拥有稳定的 v1 API,任何未来的破坏性变更都会以新的主版本号发布;特性与 bug 修复会持续发生在 v1.x.x 系列中;
- 只支持 Go 最近的两个大版本(与 Go 官方发布策略对齐),更老的 Go 版本可能缺少该包正常工作所需的关键特性与修复;
- 版本间变更详见 vendor/github.com/mdlayher/vsock/CHANGELOG.md,其中几个关键里程碑:
- v1.0.0:首次稳定发布;
Dial/Listen引入可选的*Config参数(当时无选项,传nil即可);新增ListenContextID; - v1.1.0:新增
FileListener,支持 systemd socket 激活等外部机制注入监听; - v1.1.1:修复 Windows 等非 UNIX 平台的构建问题;
- v1.2.0:要求 Go 1.18+(更早版本请用 v1.1.1);
- v1.2.1:依赖更新并以 Go 1.20 测试。
- v1.0.0:首次稳定发布;
仓库内对应的 Bazel 依赖声明位于 vendor/github.com/mdlayher/vsock/BUILD.bazel。
六、KubeVirt 中的实战:gRPC over vsock 的安全通道
该库在 KubeVirt 中承担"宿主机 ↔ 虚拟机"控制通道的角色,以下实现细节均可在仓库源码中核对。
6.1 宿主机侧:在 Host CID 上监听 gRPC 服务
pkg/virt-handler/vsock/servers.go 展示了标准服务端用法:在每个 POD 的独立网络命名空间内,以vsock.Host为 CID、固定端口1创建监听器,并直接交给 gRPC:
const vsockPort = 1 listener, lisErr := vsock.ListenContextID(vsock.Host, vsockPort, &vsock.Config{}) if lisErr != nil { return nil, nil, fmt.Errorf("failed to listen on VSOCK CID %d port %d ...", vsock.Host, vsockPort, lisErr) } server := grpc.NewServer() systemv1.RegisterSystemServer(server, system.NewSystemService(s.caManager)) go func() { serveErr := server.Serve(listener) ... }()这正是"vsock.Listener 可直接充当 net.Listener"设计价值的直接体现。同构的实现还出现在 pkg/vsock/server/server.go。
6.2 客户端侧:拨号 + TLS + 网络命名空间切换
pkg/virt-handler/vsock/dial.go 封装了Dialer,把vsock.Dial作为底层拨号函数注入(vsockDialFunc):
- 目标 CID 取自
vmi.Status.VSOCKCID,端口由调用方指定; - 拨号在目标 POD 的网络命名空间内执行(
netns.New(pid).Do(fn)),确保 VSOCK 路由落在正确的命名空间; - 在local namespaced 模式下,改用硬编码 CID
3(pkg/vsock/constants.go 中的LocalCID),以避免与其它 VM 的 CID 冲突,并可在命名空间内临时拉起对端 gRPC CA 服务; - 连接建立后叠加TLS 13(
tls.Client+ 客户端证书回调,InsecureSkipVerify配合证书双向认证机制),握手过程刻意放到netns.Do之外执行,避免阻塞 OS 线程(源码注释明确说明了这一取舍)。
6.3 调用入口
Dialer.Dial 是完整链路入口,被 pkg/virt-handler/rest/console.go 等控制面组件使用,用于向虚拟机内进程(如 guest agent)发起 gRPC over vsock 请求。
七、快速上手:一份可运行的端到端示例
结合前文 API,以下示例展示"宿主机监听 vsock 端口 → 客户端(另一侧)拨号"的最小闭环。服务端与客户端各取一段代码,在支持AF_VSOCK的 Linux 上运行(需要/dev/vsock存在且内核加载vsock/virtio_vsock模块)。
服务端(宿主机侧,监听 Host CID 的 1234 端口):
package main import ( "fmt" "io" "net" "github.com/mdlayher/vsock" ) func main() { // 监听 vsock.Host (2):1234;传 0 可让内核自动分配端口 l, err := vsock.ListenContextID(vsock.Host, 1234, &vsock.Config{}) if err != nil { panic(fmt.Sprintf("listen vsock: %v", err)) } fmt.Printf("listening on %s\n", l.Addr()) // host(2):1234 defer l.Close() for { conn, err := l.Accept() if err != nil { panic(fmt.Sprintf("accept: %v", err)) } go handle(conn) } } func handle(c net.Conn) { defer c.Close() _, _ = io.Copy(c, c) // 回显 }客户端(虚拟机侧,拨号到宿主机 Host CID 的 1234 端口):
package main import ( "fmt" "io" "github.com/mdlayher/vsock" ) func main() { // 从虚拟机内拨号到宿主机普通进程 conn, err := vsock.Dial(vsock.Host, 1234, &vsock.Config{}) if err != nil { panic(fmt.Sprintf("dial vsock: %v", err)) } defer conn.Close() fmt.Printf("connected %s -> %s\n", conn.LocalAddr(), conn.RemoteAddr()) _, _ = conn.Write([]byte("hello from VM")) _, _ = io.Copy(io.Discard, conn) }值得注意的工程细节:
LocalAddr()/RemoteAddr()返回的是共享指针,不要修改其字段(vsock.go);- 连接与监听器用完必须显式
Close(); - 若内核未加载 vsock 模块,
Listen/Dial/ContextID会返回与/dev/vsock或 ioctl 相关的错误,请先用vsock.ContextID()探测环境是否可用。
八、适用前提与限制小结
- 平台:完整功能仅限 Linux(
AF_VSOCK内核支持 +/dev/vsock设备);其他平台编译可通过,但运行时全部返回vsock: not implemented on <GOOS>; - Go 版本:当前 vendored 版本要求 Go 1.18+(CHANGELOG v1.2.0 起),并随 Go 官方策略仅支持最近两个大版本;
- API 稳定性:v1 语义稳定,
Config为未来扩展预留,目前为空结构体,传nil与&vsock.Config{}等效; - KubeVirt 集成前提:VSOCK 通道需要 VMI 开启并上报
vmi.Status.VSOCKCID(pkg/virt-handler/vsock/dial.go),且拨号在 POD 网络命名空间内进行。
综上,mdlayher/vsock是一份"标准库风格、实现克制、错误语义统一"的 Linux VM sockets Go 客户端/服务端库,既适合独立项目直接依赖,也已被 KubeVirt 用于承载其虚拟机控制面的 gRPC/TLS 通信——理解它的 API 与底层行为,是理解 KubeVirt vsock 控制通道的最佳切入点。
- 云原生
【免费下载链接】kubevirt
Kubernetes Virtualization API and runtime in order to define and manage virtual machines.
相关推荐
Podman 中的 Linux VM Socket(AF_VSOCK)编程:mdlayher/vsock 库解析与实战
Podman 中的 Linux VM Socket(AF_VSOCK)编程:mdlayher/vsock 库解析与实战 导读 VM socket( AF_VSO
容器运行时云原生CLI5分钟部署OpenShorts:开源AI短视频生成器Docker自托管完整教程(附GPU可选配置)
5分钟部署OpenShorts:开源AI短视频生成器Docker自托管完整教程(附GPU可选配置) OpenShorts 是一款开源的 AI 短视频生成器,能把
Moby 仓库中的 Go vsock 库解析:用 Linux VM Socket(AF_VSOCK)打通 hypervisor 与虚拟机
Moby 仓库中的 Go vsock 库解析:用 Linux VM Socket(AF_VSOCK)打通 hypervisor 与虚拟机 导读 本篇文章围绕 M
云原生容器运行时虚拟化容器编排
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考