net.vmess
The VMess proxy protocol, AEAD form only: encrypted client and server, TCP and UDP, over TCP, TLS, WebSocket or any tunnel stream, with the cryptographic building blocks exposed.
import net.vmess
VMess is the older protocol of the V2Ray family, and unlike VLESS it encrypts for itself. The client proves it holds a user's UUID with an authentication id that contains the current time, sends the request (the target address and the session keys) encrypted under keys derived from the UUID, and then sends the payload in encrypted chunks. The server answers with its own encrypted header and its own chunks. Anyone watching sees random bytes, whatever carries them.
This package implements only the AEAD form, the one used since V2Ray 4.28 and the only one current clients still produce: the user is identified by the encrypted authentication id and the header is sealed with AES-128-GCM. The older MD5 based form, the one that needs alterId to be above 0, is not implemented. Set alterId to 0 on both sides (it is the default in current clients).
The package is both halves. Dial connects a client through net.transport and sends the header, Accept reads it on a server, Read and Write move the payload and ReadPacket reads one chunk as a UDP datagram. AsStream, DialOver and AcceptOver connect it to net.tunnel streams. The lower level functions (key derivation, the sealed header, the chunk encoder) are public, because the protocol is easiest to understand, and to test, one piece at a time.
The body encryptions
| Constant | What it does |
|---|---|
SecAES128GCM | Each chunk is sealed with AES-128-GCM. The default choice. |
SecChaCha20Poly1305 | Each chunk is sealed with ChaCha20-Poly1305. A good choice on a processor without AES instructions. |
SecNone | The chunks are framed but not encrypted. The header is still sealed. |
SecZero | No chunks and no encryption: the payload follows the header as it is. Only for use inside TLS. |
The header is always sealed with AES-128-GCM under keys derived from the UUID, whichever body type is chosen. The options in Request.option are bits: OptChunkStream frames the body in chunks, OptChunkMask hides the chunk lengths with a SHAKE-128 stream, OptPadding adds random padding to each chunk, OptAuthLength authenticates the lengths with the body cipher. Dial uses DefaultOption for the security type; to choose the bits yourself, call Client on a connection you opened.
What trips people up
- The two clocks must agree. The authentication id contains the time and a server accepts only ids within
TimeWindowseconds, 120, of its own clock. A phone or a server with a wrong clock getsunknown user or stale timestampand nothing else. This is the first thing to check when a correct UUID is refused. - Share one replay filter.
Acceptremembers the authentication ids it has seen and refuses a repeat. Make one withNewReplayFilterand pass the same one to everyAcceptcall of a server; it is safe to use from several threads. Once it holds more than 4096 ids it drops the ones older than four minutes. - A refused client is not told.
Dialonly sends the header. A wrong UUID, a stale clock or a replayed id makes the server close the connection, and the client sees that as a failed read. - The server must support the security type. An
Acceptthat reads a type other than the four above fails withunsupported security type. - There are no vmess:// links. The base64 JSON share format of V2RayN and others is not parsed here. Take the fields from it yourself.
- Close frees the keys. Call
Closeonce on each side when you are done.
The building blocks
This program derives a user's command key from a UUID, makes an authentication id for a given time, opens it again (which also gives back the time), shows that another user's key cannot open it, and seals a small header. The numbers that come out are fixed, so they can be compared with another implementation.
import net.transport
import net.vmess
const ID := "b831381d-6324-4d53-ad4f-8cda48b30811"
func main:
key := vmess.CmdKey(transport.ParseUUID(ID))
println "command key", transport.ToHex(key)
stamp := 1700000000 as i64
salt := transport.FromHex("a1b2c3d4")
authid := vmess.CreateAuthID(key, stamp, salt)
println "auth id", transport.ToHex(authid)
mut when := 0 as i64
println "opens with the right key:", vmess.OpenAuthID(key, authid, when), when == stamp
other := vmess.CmdKey(transport.ParseUUID("5783a3e7-e373-51cd-8642-c83782b807c5"))
println "refused for another user:", not vmess.OpenAuthID(other, authid, when)
nonce := transport.FromHex("1112131415161718")
sealed := vmess.SealHeader(key, transport.FromStr("a small header"), stamp, salt, nonce)
println "sealed header is", sealed.len, "bytes"
println "time window in seconds:", vmess.TimeWindow
println "default option for aes-gcm:", vmess.DefaultOption(vmess.SecAES128GCM)
println "default option for none:", vmess.DefaultOption(vmess.SecNone)
println "default option for zero:", vmess.DefaultOption(vmess.SecZero)
endQuick start: a client and a server over TCP
The server listens with transport.Listen, accepts with transport.Accept and hands each connection to vmess.Accept with its list of users and the shared replay filter. The client dials with vmess.Dial and picks the body encryption. The target in the header is what a real server would connect onward to; this echo server just reports it.
import mem
import str
import sync
import net.transport
import net.vmess
const ID := "b831381d-6324-4d53-ad4f-8cda48b30811"
mut port := 0
mut ready := sync.NewWaitGroup()
mut seen := ""
func server():
l := transport.Listen(0, transport.Settings {})
port = transport.ListenerPort(l)
sync.Done(ready)
mut users := Vector {} as Vector<vmess.User>
users.push(vmess.NewUser(ID))
mut rf := vmess.NewReplayFilter()
buf := mem.Allocate(4096 as u64)
repeat 3:
mut c := vmess.Accept(transport.Accept(l), users, rf)
if not c.ok:
seen += "refused(" + c.err + ") "
continue
end
seen += c.req.addr.host + " security " + str.FromInt(c.req.security as i64) + "; "
n := vmess.Read(c, buf, 4096)
vmess.Write(c, buf, n)
vmess.Close(c)
end
mem.Free(buf)
transport.CloseListener(l)
end
func talk(security: int):
target := transport.NewAddr("example.com", 443)
mut c := vmess.Dial("127.0.0.1", port, transport.Settings {}, ID, security, vmess.CmdTCP, target)
mut msg := transport.FromStr("hello through vmess")
vmess.Write(c, msg.p, msg.len)
buf := mem.Allocate(4096 as u64)
n := vmess.Read(c, buf, 4096)
println security, (buf as str).substr(0, n)
mem.Free(buf)
transport.FreeBytes(msg)
vmess.Close(c)
end
func main:
sync.Add(ready, 1 as i64)
t := spawn(server)
sync.Wait(ready)
talk(vmess.SecAES128GCM)
talk(vmess.SecChaCha20Poly1305)
talk(vmess.SecNone)
join(t)
println seen
endA user the server does not know
The client cannot tell by itself. The server finds no matching key, says unknown user or stale timestamp, and closes the connection. A real server should do nothing else: sending an error would tell a scanner that it found a proxy.
import sync
import net.transport
import net.vmess
const GOOD := "b831381d-6324-4d53-ad4f-8cda48b30811"
const OTHER := "5783a3e7-e373-51cd-8642-c83782b807c5"
mut port := 0
mut ready := sync.NewWaitGroup()
mut verdict := ""
func server():
l := transport.Listen(0, transport.Settings {})
port = transport.ListenerPort(l)
sync.Done(ready)
mut users := Vector {} as Vector<vmess.User>
users.push(vmess.NewUser(GOOD))
mut rf := vmess.NewReplayFilter()
mut c := vmess.Accept(transport.Accept(l), users, rf)
verdict = c.ok ? "accepted " + c.user : "refused: " + c.err
vmess.Close(c)
transport.CloseListener(l)
end
func main:
sync.Add(ready, 1 as i64)
t := spawn(server)
sync.Wait(ready)
mut c := vmess.Dial("127.0.0.1", port, transport.Settings {}, OTHER, vmess.SecAES128GCM, vmess.CmdTCP, transport.NewAddr("example.com", 80))
join(t)
println verdict
vmess.Close(c)
endVLESS carrying VMess
A stream wrapper turns any protocol into the carrier of another. Here an outer VLESS connection runs over an in-memory pipe and an inner VMess connection runs inside it, with different users and different targets. On a real network the outer one would be VLESS over WebSocket and TLS and the inner one VMess, which encrypts the payload end to end even when a CDN terminates the outer TLS.
import mem
import sync
import net.transport
import net.tunnel
import net.vless
import net.vmess
const OUTER := "b831381d-6324-4d53-ad4f-8cda48b30811"
const INNER := "5783a3e7-e373-51cd-8642-c83782b807c5"
mut pipe := tunnel.NewPipe()
mut done := sync.NewWaitGroup()
mut seen := ""
func server():
mut vl := Vector {} as Vector<vless.User>
vl.push(vless.NewUser(OUTER, ""))
mut vm := Vector {} as Vector<vmess.User>
vm.push(vmess.NewUser(INNER))
mut rf := vmess.NewReplayFilter()
outer := vless.AcceptOver(tunnel.PipeB(pipe), vl)
inner := vmess.AcceptOver(vless.AsStream(outer), vm, rf)
seen = outer.req.addr.host + " then " + inner.req.addr.host
s := vmess.AsStream(inner)
buf := mem.Allocate(256 as u64)
n := s.ReadBytes(buf, 256)
s.WriteBytes(buf, n)
mem.Free(buf)
s.Close()
sync.Done(done)
end
func main:
sync.Add(done, 1 as i64)
_t := spawn(server)
outer := vless.DialOver(tunnel.PipeA(pipe), OUTER, "", vless.CmdTCP, transport.NewAddr("hop.example", 443))
inner := vmess.DialOver(vless.AsStream(outer), INNER, vmess.SecChaCha20Poly1305, vmess.CmdTCP, transport.NewAddr("final.example", 443))
s := vmess.AsStream(inner)
s.Write("two layers deep")
buf := mem.AllocateZeroed(256 as u64)
n := s.ReadBytes(buf, 256)
println (buf as str).substr(0, n)
mem.Free(buf)
s.Close()
sync.Wait(done)
println "server saw", seen
endVMess over WebSocket and TLS behind a CDN
Do the CDN steps from the net.transport page: a proxied domain, a secret WebSocket path, TLS to the CDN with the front name as sni, your domain as host. VMess does the encryption, so it also protects the payload from the CDN.
import net.transport
import net.vmess
func main:
mut s := transport.Settings {}
s.network = "ws"
s.tls = true
s.sni = "front.example.net"
s.host = "vpn.example.org"
s.path = "/vm-31c9"
s.alpn = "http/1.1"
target := transport.NewAddr("example.com", 80)
mut c := vmess.Dial("front.example.net", 443, s, "b831381d-6324-4d53-ad4f-8cda48b30811", vmess.SecAES128GCM, vmess.CmdTCP, target)
if not c.ok:
println "could not connect:", c.err
ret
end
mut req := transport.FromStr("HEAD / HTTP/1.0\r\nHost: example.com\r\n\r\n")
vmess.Write(c, req.p, req.len)
transport.FreeBytes(req)
vmess.Close(c)
endOn the server, listen with network = "ws", the same path and host and with tls off when the CDN or your reverse proxy terminates TLS. For each connection, in a worker thread, call transport.AcceptRaw, transport.Upgrade and then vmess.Accept. Keep the server's clock right (the time window is 120 seconds), and tell your users to do the same.
Share links
Apps share a VMess server as vmess:// followed by base64 of a JSON object (the v2rayN form, the same one tunnel.ParseLink reads). ParseLink reads it into a Link: check ok and read err when it is false. FormatLink writes one back, LinkSettings turns it into the net.transport settings (WebSocket path and ?ed= early data, gRPC service name, TLS name) and DialLink dials it. SecurityByName and SecurityName convert between names such as chacha20-poly1305 and the security numbers; auto means AES-128-GCM. The legacy aid is parsed and kept but only 0 is meaningful.
Limits
- AEAD only. There is no legacy (
alterIdabove 0) mode, and no compatibility with servers or clients that need it. - The authenticated length option follows Xray.
OptAuthLengthderives its length key and nonce from the request key and IV in both directions, seals the length with AES-128-GCM (also for ChaCha20-Poly1305 bodies) and was checked against Xray and sing-box in the interop run (seedocs/VPN_INTEROP.mdfor the matrix).Dialnever sets it; choose the bits withClient. - Interoperability was tested separately. The header, the authentication id, the key derivation and the chunk formats are checked in this repository against fixed byte vectors and the two halves of this package against each other (over TCP, TLS, WebSocket and a pipe). The interop harness runs the package against real Xray and sing-box builds; its results are in
docs/VPN_INTEROP.md. - Multiplexing is Mux.Cool. Dial with
CmdMuxand callMuxClient; on the server, when the accepted request hasCmdMux, callMuxServer. Sessions come fromnet.transport(MuxOpen,MuxAccept). - Weak primitives by design. VMess uses MD5 for the command key and AES in ECB mode for one block of the authentication id; these are the protocol's, not choices of this package. The payload ciphers are authenticated (AES-GCM, ChaCha20-Poly1305).
SecNoneandSecZerodo not encrypt the payload. Use them only inside TLS you trust.- Replay protection is per process. The filter lives in memory, so a restart forgets it, but the time window still limits what can be replayed.
Types
User
struct User| Name | Type | Description |
|---|---|---|
id | str = "" | The user id as text. |
cmd_key | transport.Bytes | The command key derived from the user id. NewUser computes it. |
ReplayFilter
struct ReplayFilterAccept calls; it has a lock.| Name | Type | Description |
|---|---|---|
seen | HashMap | The ids already seen and the time each was seen. |
lock | sync.Mutex | The lock that guards seen. |
Conn
struct Conn| Name | Type | Description |
|---|---|---|
t | transport.Conn | The transport connection underneath. |
client | bool = false | True on the client side. |
ok | bool = false | True when the header exchange succeeded and the connection is usable. |
err | str = "" | The reason it failed, empty otherwise. |
req | Request | The request header: on a server what the client sent, on a client what it sent. |
user | str = "" | On a server: the id of the user that matched. |
wc | Codec | Internal: the encoder of the chunks this side writes. |
rc | Codec | Internal: the decoder of the chunks this side reads. |
resp_pending | bool = false | Internal: the server's response header has not been read yet. |
resp_key | transport.Bytes | Internal: the key of the response header. |
resp_iv | transport.Bytes | Internal: the initialization vector of the response. |
rbuf | transport.Bytes | Internal: decrypted bytes that have not been read yet. |
eof | bool = false | Internal: the peer ended the stream. |
closed | bool = false | Internal: Close has been called. |
Link
struct Linkvmess:// link, the form v2rayN and most apps share.| Name | Type | Description |
|---|---|---|
ok | bool = false | True when the link is complete and supported. |
err | str = "" | Why parsing failed. |
remark | str = "" | The ps label. |
host | str = "" | The server address. |
port | int = 443 | The server port. |
uuid | str = "" | The user id. |
alter_id | int = 0 | The legacy aid number, kept for round trips. Only 0 is meaningful here. |
security | str = "auto" | The body cipher name: auto, aes-128-gcm, chacha20-poly1305, none or zero. |
network | str = "tcp" | tcp, ws, httpupgrade or grpc. |
header_type | str = "none" | The type field, usually none. |
host_header | str = "" | The host field: the Host header of WebSocket and HTTP upgrade. |
path | str = "" | The WebSocket path, or the gRPC service name. |
tls | bool = false | True when the link says tls. |
sni | str = "" | The TLS server name. |
alpn | str = "" | The ALPN text. |
fingerprint | str = "" | The fp browser name. |
allow_insecure | bool = false | Skip certificate verification. |
Request
struct Request| Name | Type | Description |
|---|---|---|
version | int = 1 | The header version. Always 1. |
iv | transport.Bytes | The 16 byte initialization vector of the request body. The response keys are derived from it. |
key | transport.Bytes | The 16 byte key of the request body. |
resp_v | int = 0 | One byte the server must echo back in its response header. It tells the client the answer belongs to this request. |
option | int = 0 | The option bits: OptChunkStream, OptChunkMask, OptPadding, OptAuthLength. |
security | int = 3 | The body encryption: SecAES128GCM, SecChaCha20Poly1305, SecNone or SecZero. |
command | int = 1 | CmdTCP, CmdUDP or CmdMux. |
addr | transport.Addr | The target address. Unused by CmdMux. |
padding | int = 0 | How many random bytes of padding the header carries, 0 to 15. |
Constants and variables
Commands Cmd…
CmdTCP opens a TCP connection to the target, CmdUDP carries UDP datagrams, CmdMux is the multiplexing command, which the header can carry but nothing implements.| Name | Value |
|---|---|
CmdTCP | 1 |
CmdUDP | 2 |
CmdMux | 3 |
Body encryptions Sec…
security of a request and for Dial: SecAES128GCM (AES-128-GCM), SecChaCha20Poly1305 (ChaCha20-Poly1305), SecNone (framed, not encrypted), SecZero (no framing, no encryption).| Name | Value |
|---|---|
SecAES128GCM | 3 |
SecChaCha20Poly1305 | 4 |
SecNone | 5 |
SecZero | 6 |
Option bits Opt…
option of a request, combined with |: OptChunkStream frames the body, OptChunkMask masks the chunk lengths, OptPadding pads each chunk, OptAuthLength authenticates the lengths (not verified against a real peer).| Name | Value |
|---|---|
OptChunkStream | 1 |
OptChunkMask | 4 |
OptPadding | 8 |
OptAuthLength | 16 |
TimeWindow
const TimeWindow: i32 = 120Functions
AsStream
func AsStream(c: Conn): dyn tunnel.Streamtunnel.Stream so it can carry another protocol or be handed to tunnel code.DialOver
func DialOver(s: dyn tunnel.Stream, uuid: str, security: int, command: int, target: transport.Addr): ConnAcceptOver
func AcceptOver(s: dyn tunnel.Stream, users: Vector<User>, rf &: ReplayFilter): ConnMuxClient
func MuxClient(c: Conn): transport.MuxCmdMux.MuxServer
func MuxServer(c: Conn): transport.MuxCmdMux.NewUser
func NewUser(uuid: str): UserNewReplayFilter
func NewReplayFilter(): ReplayFilterDefaultOption
func DefaultOption(security: int): intSecZero, chunks for SecNone, chunks and masked lengths otherwise.Client
func Client(t: transport.Conn, uuid: str, security: int, option: int, command: int, addr: transport.Addr): Connok is false when the UUID is bad or the write fails.Dial
func Dial(host: str, port: int, s: transport.Settings, uuid: str, security: int, command: int, target: transport.Addr): Conntransport.Dial and the given settings, then sends the request with DefaultOption and a random session key. The reply is not read yet: a refused user shows up on the first read.Accept
func Accept(t: transport.Conn, users: Vector<User>, rf &: ReplayFilter): Connok is false, err says why and the transport is closed.Read
func Read(c &: Conn, buf: void*, max: int): intReadPacket
func ReadPacket(c &: Conn, out &: transport.Bytes): boolCmdUDP. False when the connection ended.Write
func Write(c &: Conn, p: void*, n: int): boolClose
func Close(c &: Conn)EncodeChunks
func EncodeChunks(security: int, option: int, key: transport.Bytes, iv: transport.Bytes, parts: Vector<transport.Bytes>, with_end: bool): transport.Byteswith_end is true. For tests and for implementing a variant.SecurityByName
func SecurityByName(name: str): intaes-128-gcm; auto means AES-128-GCM. -1 for an unknown name.SecurityName
func SecurityName(sec: int): strParseLink
func ParseLink(link: str): Linkvmess:// link. Check ok and err.FormatLink
func FormatLink(l: Link): strLink as a vmess:// text (base64 of the v2rayN JSON).LinkSettings
func LinkSettings(l: Link): transport.Settingstransport.Settings a link asks for.DialLink
func DialLink(l: Link, command: int, target: transport.Addr): ConnKDF
func KDF(key: transport.Bytes, label: str, a: transport.Bytes, b: transport.Bytes): transport.BytesKDF16
func KDF16(key: transport.Bytes, label: str, a: transport.Bytes, b: transport.Bytes): transport.BytesKDF cut to the first 16 bytes.KDF12
func KDF12(key: transport.Bytes, label: str, a: transport.Bytes, b: transport.Bytes): transport.BytesKDF cut to the first 12 bytes.CmdKey
func CmdKey(uuid: transport.Bytes): transport.BytesSha256
func Sha256(data: transport.Bytes): transport.BytesAES128Block
func AES128Block(key: transport.Bytes, block: transport.Bytes, decrypt: bool): transport.Bytesdecrypt, decrypts one 16 byte block with AES-128.Shake128
func Shake128(msg: transport.Bytes, outlen: int): transport.Bytesoutlen bytes long.CRC32
func CRC32(data: transport.Bytes): i64FNV1a
func FNV1a(data: transport.Bytes): i64CreateAuthID
func CreateAuthID(cmd_key: transport.Bytes, ts: i64, rand4: transport.Bytes): transport.BytesOpenAuthID
func OpenAuthID(cmd_key: transport.Bytes, authid: transport.Bytes, ts &: i64): boolSealHeader
func SealHeader(cmd_key: transport.Bytes, data: transport.Bytes, ts: i64, rand4: transport.Bytes, nonce: transport.Bytes): transport.BytesSealHeaderNow
func SealHeaderNow(cmd_key: transport.Bytes, data: transport.Bytes): transport.BytesSealHeader with the current time and fresh random bytes.OpenHeaderLength
func OpenHeaderLength(cmd_key: transport.Bytes, authid: transport.Bytes, enc_len: transport.Bytes, nonce: transport.Bytes): intOpenHeader
func OpenHeader(cmd_key: transport.Bytes, authid: transport.Bytes, enc: transport.Bytes, nonce: transport.Bytes): transport.Bytesbad set when it fails to authenticate.SealResponseHeader
func SealResponseHeader(resp_key: transport.Bytes, resp_iv: transport.Bytes, header: transport.Bytes): transport.BytesOpenResponseLength
func OpenResponseLength(resp_key: transport.Bytes, resp_iv: transport.Bytes, enc_len: transport.Bytes): intOpenResponseHeader
func OpenResponseHeader(resp_key: transport.Bytes, resp_iv: transport.Bytes, enc: transport.Bytes): transport.Bytesbad set when it fails to authenticate.EncodeRequest
func EncodeRequest(r: Request): transport.Bytestransport.FreeBytes.DecodeRequest
func DecodeRequest(b: transport.Bytes, r &: Request): bool