跳转到内容

VMess

VMess 是 V2Ray 的协议,它用用户 UUID 加时间戳认证每个连接,然后加密请求头和正文本身。etemenanki-app 在两个方向上都使用现代的 AEAD 形式的 VMess:作为入站,它为 Xray 和 V2Ray 客户端提供服务;作为出站,它可以连接任何接受 AEAD 的 VMess 服务端。

在搭建 VMess 服务端或客户端、从 Xray 迁移 VMess 配置,或者客户端连接时报认证错误时,请阅读本页。下文几乎所有内容都取决于两个特性:VMess 只支持 AEAD,并且依赖两端时钟都准确。

特性 etemenanki-app
头部格式 仅 AEAD,即 Xray 所说的 alterId = 0;没有 alterId 键。
正文加密方式 aes-128-gcm、chacha20-poly1305
认证 用户 UUID 加时间戳,时间戳与服务端时钟相差不能超过 120 秒
防重放 每个认证 ID 只接受一次
传输层 TCP、TLS、WebSocket、gRPC(见传输层)
UDP 支持,承载在 TCP 连接内
mux.cool 和 XUDP 入站自动提供;出站不做多路复用

VMess 自己加密载荷,所以可以直接跑在明文 TCP 上。在公网上,通常会把它放进 TLS、WebSocket over TLS 或 gRPC over TLS 中,让流量看起来像普通的 HTTPS。VMess over gRPC 和 TLS 实践完整演示了一次部署。

服务端通过明文 TCP 接受两个用户,并把所有流量直接发出。客户端开放一个本地 SOCKS 端口,把每个流转发到服务端。

server.toml
[[inbound]]
tag = "vmess-in"
protocol = "vmess"
listen = "0.0.0.0"
port = 10086
[inbound.settings]
users = [
{ id = "11111111-2222-3333-4444-555555555555" },
{ id = "11111111-2222-3333-4444-666666666666" },
]
[[outbound]]
tag = "direct"
protocol = "freedom"
[route]
default = "direct"

启动前先用 etemenanki-app --test -c <file> 检查每个文件。用 uuidgen 或 cat /proc/sys/kernel/random/uuid 为每个用户生成真实的 UUID;UUID 是用户唯一的凭据。

VMess 入站使用通用入站键(tag、listen、port、stream、address_family、sniffing,见入站),另加下面的 [inbound.settings] 表:

键类型必填默认值说明
usersarray of tables否[]允许连接的账户,每个用户一个表,所有用户的权限相同。空列表可以通过校验,但之后每个连接都会认证失败。Xray 的 clients 键会被当作未知字段拒绝。
users[].idstring是—用户的 UUID。接受带连字符的标准形式、32 位十六进制、{…} 花括号形式或 urn:uuid: 前缀,大小写均可。其他字符串会报 inbound <tag>: invalid uuid "…"。用户表只接受这一个键,所以 alterId、email、level 都会报错。

users 中的每一项都是一个只有 id 这一个键的表。解析器会拒绝其他任何键,所以从 Xray 复制过来、带有 alterId、email 或 level 的用户会让配置无法通过:

configuration invalid: inbound vmess-in: invalid settings: unknown field `alterId`, expected `id`
in `users`

删掉多余的键即可。alterId = 0 不需要任何替代写法,因为 AEAD 是唯一的模式。

id 必须是真正的 UUID。下面这些形式都能解析,大小写均可:

形式 示例
带连字符 11111111-2222-3333-4444-555555555555
纯十六进制 11111111222233334444555555555555
花括号 {11111111-2222-3333-4444-555555555555}
URN urn:uuid:11111111-2222-3333-4444-555555555555

Xray 还接受任意短字符串作为 ID,并把它映射成 UUID。etemenanki-app 不支持:id = "my-custom-id" 会报 inbound vmess-in: invalid uuid "my-custom-id": invalid character: found `m` at 0。请给这类用户分配 UUID。

列表中的所有用户权限相同。etemenanki-app 没有按用户的路由规则或流量计数器;基于同一内核的面板节点 agent katana 在 VMess 之上增加了按用户的流量计费。

users 列表为空或缺失时可以通过 --test,但之后没有任何客户端能通过认证。

服务端对新连接逐个尝试用户,所以握手开销随用户数量增长。在非常大的列表中,频繁连接的用户会被自动移到扫描顺序的前面,因此不需要手动排序。

正文加密方式由客户端选择,并在请求头中声明。入站没有相关设置,接受下面两种:

客户端 security(Xray 命名) 在 etemenanki-app 入站上的结果
aes-128-gcm 接受
chacha20-poly1305 接受
auto 接受:客户端在连接前会把 auto 解析为上面两种之一
none、zero 或其他任何加密方式 拒绝:vmess: unsupported security type N,N 是客户端发送的加密方式编号(none 为 5)

当前的 Xray 版本已不再提供 none 和 zero,并把未知值当作 auto 处理;这条拒绝规则针对的是仍会发送这些值的旧版 Xray 和 V2Ray 客户端。

仍在使用旧版头部(alterId 大于 0)的客户端走不到这一步。它发出的最初几个字节不是 AEAD 认证 ID,所以服务端会把它当作未知用户拒绝。

每个 VMess 连接都以一个 16 字节的认证 ID 开头。客户端用从用户 UUID 派生的密钥,把当前 Unix 时间、一个随机值和一个校验和加密,得到这个 ID。服务端依次尝试每个用户的密钥,直到某个密钥解密出有效的校验和,再把其中的时间与自己的时钟比较:

  • 时间必须与服务端时钟相差不超过 120 秒,快慢均可。边界是闭区间:正好相差 120 秒可以通过,121 秒则失败。
  • 服务端会把每个已接受的 ID 记住 240 秒(窗口的两倍),并拒绝重复使用同一 ID 的连接。正常客户端每个连接都会生成新的 ID,所以这只会拦住重放的握手。

客户端时钟与服务端时钟相差超过 120 秒时,连接的失败方式与 UUID 错误完全相同:vmess: unknown user or invalid auth id。时区没有影响,因为双方都使用 Unix 时间;只有绝对时钟才重要。

如果客户端报告认证失败,而你确定 UUID 正确,就检查时钟:

  1. 在服务端确认时钟已同步。使用 systemd 时,timedatectl status 应显示 System clock synchronized: yes。使用 chrony 时,chronyc tracking 会显示当前偏差。

  2. 如果没有同步,开启 NTP,例如执行 sudo timedatectl set-ntp true,或者启动你的发行版所用的 chrony 或 ntpd 服务。

  3. 在两端分别运行 date -u,比较客户端和服务端的时钟。两者的差距必须远小于 120 秒。

  4. 重试连接。不需要重启 etemenanki-app;服务端每次握手都会读取系统时钟。

VMess 出站需要通用出站键 server 和 port(缺少任一个会报 outbound <tag>: missing server 或 missing port),并可以使用 stream 和 address_family,见出站。它自己的 [outbound.settings] 表有两个键:

键类型必填默认值说明
idstring是—服务端上账户的 UUID,可以使用入站接受的任意格式。不写会报 missing field 错误,格式错误会报 outbound <tag>: invalid uuid "…"。
securitystring (enum)否"aes-128-gcm"正文加密算法。aes-128-gcm 和 auto 都选择 AES-128-GCM,不写这个键也一样;chacha20-poly1305 选择 ChaCha20-Poly1305。匹配时不区分大小写。其他值(包括 none 和 zero)都会报 unknown vmess security "…"。不要和开启 TLS 的 [outbound.stream].security 混淆。

security 接受下面这些值,不区分大小写:

取值 加密方式
不写这个键 AES-128-GCM
auto AES-128-GCM
aes-128-gcm AES-128-GCM
chacha20-poly1305 ChaCha20-Poly1305
其他任何值 配置错误

在 Xray 中,auto 会在 CPU 支持 AES 硬件加速时选择 AES-128-GCM,否则选择 ChaCha20-Poly1305;在这里它始终表示 AES-128-GCM。客户端运行在没有 AES 指令的硬件上时,请显式选择 chacha20-poly1305。none 和 zero 会被拒绝;入站同样会拒绝使用它们的客户端:

configuration invalid: unknown vmess security "none"

这个错误以小写显示取值,而且不指明是哪个出站,所以看到它时要在文件中检查每个 VMess 的 security 键。

出站总是请求 chunk masking 和 global padding,这也是 Xray AEAD 客户端的默认行为。没有键可以关闭它们。它为每个 TCP 流打开一个上游连接,不会把多个流复用到同一个连接上。

在出站设置中写 alterId 同样会报错:outbound <tag>: invalid settings: unknown field `alterId`, expected `id` or `security` 。

sequenceDiagram
    participant C as 客户端
    participant S as etemenanki-app 入站
    participant T as 目标
    C->>S: 认证 ID(16 字节)
    Note over S: 查找用户,检查 120 秒窗口和重放集合
    C->>S: 加密的请求头(命令、目标、加密方式)
    alt TCP 命令
        S->>T: 连接(由路由决定出站)
        T-->>S: 已连接
        S-->>C: 加密的响应头
    else UDP 或 mux 命令
        S-->>C: 立即返回加密的响应头
    end
    C->>S: 加密的正文 chunk
    S->>T: 明文
    T-->>S: 回复
    S-->>C: 加密的正文 chunk

这个顺序带来几个细节:

  • 对于 TCP 请求,服务端要等出站连接成功后才发送响应头。如果连接目标失败,服务端会直接关闭客户端连接,不发送响应。
  • 当 TCP 目标是 IP 地址且 sniffing 开启(默认开启)时,服务端会读取最初的正文 chunk(最多等待 300 毫秒),以便在打开出站之前恢复出用于路由的域名。目标本来就是域名时不会嗅探。见路由。
  • 什么都不发送的客户端会在 10 秒后被断开。收到最初的字节后,客户端有 10 秒时间发完认证 ID 和请求头。已建立的连接在双向都没有流量 300 秒后关闭。限制列出了这些超时和连接上限。

VMess 把 UDP 承载在同一个 TCP(或 TLS、WebSocket、gRPC)连接内;入站不会打开 UDP socket。

  • 入站。 使用 UDP 命令的请求在头部中指定一个目标。客户端发送的每个正文 chunk 是发往该目标的一个数据报,每个回复数据报也作为一个 chunk 返回。服务端立即响应请求头,不等待第一个数据包。
  • 出站。 当路由把一个 UDP 流发给 VMess 出站时,出站会向该流的目的地址打开一个使用 UDP 命令的 VMess 连接,每个数据报对应一个 chunk。这个连接上的所有数据包都发往头部中指定的目标,回复也都归属于该目标。

如果需要在单个连接上与多个对端交换 UDP,Xray 客户端会使用 XUDP,入站支持它(见下一节)。

VMess 入站支持 mux.cool,即 Xray 客户端通过 "mux": { "enabled": true } 开启的多路复用。它没有对应的配置键:使用 mux 命令的请求会被自动识别并处理。

  • 承载连接中的每个子流都单独路由、单独拨号,拥有自己的目的地址。承载连接本身的目的地址是占位值 v1.mux.cool:0,它永远不会被路由或拨号。
  • 子流的数据可以以任意方式切分到 VMess 块(chunk)和 mux 帧中。一个 mux 帧跨越多个 VMess 块时,服务端会把它重新拼装起来,包括在一次读取中同时到达的多个块,并把每个子流的字节完整、按序地转发出去。这一点已通过 Xray mux 客户端上传 64 KiB 数据进行测试。
  • 一个承载连接最多容纳 256 个子流。超出上限的新子流(或复用了仍在使用的会话 ID 的子流)会被服务端拒绝:服务端回复一个结束帧并丢弃其数据;承载连接及其他子流继续运行。
  • XUDP(帧中携带逐包地址的 UDP 子流)可以正常工作:一个 UDP 子流可以与多个对端交换数据包,回复会归属到正确的对端。UDP 子流会一直持续到客户端结束它或承载连接关闭。服务端会读取 XUDP 全局 ID,但不会用它在新连接上恢复会话。
  • 大于 8 KiB 的回复数据报放不进一个 mux 帧,会被丢弃。
  • 对子流的嗅探只查看其首帧中的数据,因此一个子流不会阻塞其他子流。

VMess 出站没有 mux 客户端。在这里,mux 是为兼容 Xray 客户端而提供的服务端功能。

VMess 入站和出站可以运行在所有 stream 传输层之上:

[stream].network [stream].security 结果
tcp(默认) 无 VMess 直接跑在 TCP 上
tls 无或 tls VMess over TLS
ws 无或 tls VMess over WebSocket,可选再套 TLS
grpc 无或 tls VMess over gRPC,可选再套 TLS

network = "tcp" 搭配 security = "tls" 会被拒绝;TLS over TCP 应写 network = "tls"。监听 Unix socket 的入站只接受不带传输层的 VMess;其他任何 network 或 security 都会被拒绝,例如报 inbound <tag>: protocol vmess over a unix socket does not support stream security "tls"。传输层介绍了所有 stream 键,VLESS over WebSocket 和 TLS 展示了一个 WebSocket 部署,只需更换协议及其设置即可套用到 VMess。

配置错误会在运行 --test、启动和重载时出现。连接错误以 debug 级别记录,例如 vmess connection from Some(203.0.113.7) ended: proxy core: vmess: unknown user or invalid auth id;在 [log] 表中设置 level = "debug" 才能看到。下表列出的是 proxy core: 前缀之后的消息。

消息 原因 解决方法
invalid settings: unknown field `alterId`, expected `id` Xray 风格的用户带有 alterId、email 或 level 每个用户只保留 id
invalid settings: unknown field `clients`, expected `users` 使用了 Xray 的列表名 clients 改名为 users
invalid uuid "…" ID 不是 UUID 使用 uuidgen 生成的 UUID
unknown vmess security "…" 出站的 security 是 none、zero 或拼写错误 使用 aes-128-gcm、chacha20-poly1305 或 auto,或者不写
outbound <tag>: missing server 或 missing port 出站没有上游地址 添加 server 和 port
vmess: unknown user or invalid auth id UUID 错误、两端时钟相差超过 120 秒、握手被重放,或是旧版 alterId 客户端 先检查 UUID,再检查两端时钟
vmess: unsupported security type N 客户端使用 none、zero 或其他不支持的加密方式 把客户端设为 aes-128-gcm、chacha20-poly1305 或 auto
client did not complete its request in time 客户端开始了握手,但没有在 10 秒内完成 检查网络路径和客户端的传输层设置
inbound handshake timed out after 10s(没有 proxy core: 前缀) 客户端已连接,但 10 秒内什么都没发送 检查客户端配置和网络路径