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 端口,把每个流转发到服务端。
[[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"[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[[outbound]]tag = "vmess-out"protocol = "vmess"server = "proxy.example.com"port = 10086
[outbound.settings]id = "11111111-2222-3333-4444-555555555555"security = "aes-128-gcm"
[route]default = "vmess-out"启动前先用 etemenanki-app --test -c <file> 检查每个文件。用 uuidgen 或 cat /proc/sys/kernel/random/uuid 为每个用户生成真实的 UUID;UUID 是用户唯一的凭据。
VMess 入站使用通用入站键(tag、listen、port、stream、address_family、sniffing,见入站),另加下面的 [inbound.settings] 表:
| 键 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
users | array of tables | 否 | [] | 允许连接的账户,每个用户一个表,所有用户的权限相同。空列表可以通过校验,但之后每个连接都会认证失败。Xray 的 clients 键会被当作未知字段拒绝。 |
users[].id | string | 是 | — | 用户的 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,但之后没有任何客户端能通过认证。
服务端对新连接逐个尝试用户,所以握手开销随用户数量增长。在非常大的列表中,频繁连接的用户会被自动移到扫描顺序的前面,因此不需要手动排序。
客户端加密方式
Section titled “客户端加密方式”正文加密方式由客户端选择,并在请求头中声明。入站没有相关设置,接受下面两种:
客户端 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 正确,就检查时钟:
-
在服务端确认时钟已同步。使用 systemd 时,
timedatectl status应显示System clock synchronized: yes。使用 chrony 时,chronyc tracking会显示当前偏差。 -
如果没有同步,开启 NTP,例如执行
sudo timedatectl set-ntp true,或者启动你的发行版所用的 chrony 或 ntpd 服务。 -
在两端分别运行
date -u,比较客户端和服务端的时钟。两者的差距必须远小于 120 秒。 -
重试连接。不需要重启 etemenanki-app;服务端每次握手都会读取系统时钟。
VMess 出站需要通用出站键 server 和 port(缺少任一个会报 outbound <tag>: missing server 或 missing port),并可以使用 stream 和 address_family,见出站。它自己的 [outbound.settings] 表有两个键:
| 键 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
id | string | 是 | — | 服务端上账户的 UUID,可以使用入站接受的任意格式。不写会报 missing field 错误,格式错误会报 outbound <tag>: invalid uuid "…"。 |
security | string (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,入站支持它(见下一节)。
Mux 和 XUDP
Section titled “Mux 和 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 秒内什么都没发送 | 检查客户端配置和网络路径 |