Skip to content

About

Independent documentation of VRChat's Photon transport protocol (GpBinaryV18): framing, AES-256-GCM datagram encryption, handshake, world-join, per-event schemas, and IL2CPP metadata decryptors.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VRChat Photon protocol notes

Independent documentation of VRChat's Photon transport, recovered from static analysis of the 2026.3.3p2 Android client (libil2cpp.so, Unity 6000.0.67f1-DWR) and from AES-GCM–verified wire captures. Photon client version on the wire is 5.1.17.0. Serialization is GpBinaryV18.

Stack

HTTPS  api.vrchat.cloud     login, config, instance reservation (join token / PhotonToken)
WSS    ns.photonengine.io   NameServer: GetRegions, AuthenticateOnce → AES-256-GCM secret
UDP    master :27001/5055   JoinGame(226) → game-server address + token
UDP    game   :27002/5056   Init + JoinGame(226) + RaiseEvent(253) in-world

Matchmaking is the VRChat HTTPS API. Photon is the instance transport. On the game UDP connection the only LoadBalancing ops observed are JoinGame (226) and an unsolicited AuthenticateOnce (231) response; every subsequent VRChat message is RaiseEvent (253) / Event.

Transport in one page

Plaintext UDP datagram (12 bytes, big-endian):

[0:2]  peerID      u16     0xFFFF until VerifyConnect; server→client is 0x0000
[2]    flags       u8      0x00 plaintext, 0x01 encrypted, 0xCC CRC
[3]    cmdCount    u8
[4:8]  sentTime    u32 ms
[8:12] challenge   u32     connection id, constant for the session

Encrypted datagram: same 7-byte cleartext header (peerID ‖ 0x01 ‖ challenge), then nonce(12) ‖ ciphertext ‖ tag(16). Cipher is AES-256-GCM. Key is the 32-byte encryption_secret from NameServer AuthenticateOnce param 192. Nonce is a 96-bit counter (client counter is process-lifetime). AAD is header(7) ‖ nonce(12).

ENet command header (12 bytes, BE): [type][channel][flags][reserved=0x04][size u32][reliableSeq u32]. Reliable send is 0x06, unreliable 0x07, fragment 0x08, ping 0x05, Ack2 0x11. Unreliable workhorse in-world is 0x0B (EgSendUnreliableProcessed).

Photon message inside a command payload: F3 <msgType> <body>.

msgType name
1 InitResponse
2 OperationRequest
3 OperationResponse
4 Event
5 DisconnectReason

Op/event parameter tables are u8 count then count × (u8 key, typed value).

Join choreography

  1. HTTPS login (GET /auth/user, 2FA if needed) → auth cookie.
  2. HTTPS reservation for a location wrld_<uuid>:<instance>~region(xx) → join token.
  3. WSS NameServer AuthenticateOnce → encryption_secret + PhotonToken.
  4. UDP Connect / VerifyConnect, then plaintext HTTP-over-UDP POST /?init=&app=<appId>&clientversion=5.1.17.0&protocol=GpBinaryV18&sid=31& whose body is a GpBinaryV18 String of base64(PhotonToken). PhotonToken = 0x02 + N×16 opaque bytes. The server encrypts from its first reply.
  5. Master JoinGame {255: location, 215: 1} → {230: "ip:port", 221: token}.
  6. Game Init (same token) + JoinGame with player/game properties and the travel JWE.
  7. Post-join RaiseEvents: 34 (rate-limit table request), 67 (attestation, every ~5 s), then 33, 202, 3, 23, 25. Event 202 instantiates the local VRCPlayer (view ids actor × 100000 + {1,2,3}). Event 67 is the in-world anti-cheat attestation heartbeat: RSAES-PKCS1-v1.5 encryption of SHA256(ts ‖ SEP ‖ nonceHex ‖ SEP ‖ context ‖ SEP ‖ PREFIX) under a per-build RSA public key. Construction and input provenance: docs/attestation.md

Photon AppId: bf0942f7-9935-4192-b359-f092fa85bef1.

Docs

File Contents
docs/architecture.md Assemblies, hops, what lives where
docs/transport.md UDP framing, command types, fragments, channels
docs/encryption.md AES-256-GCM construction, key provenance
docs/gp18.md GpBinaryV18 type table and message framing
docs/nameserver.md WSS NameServer, Sec-WebSocket-Protocol: GpBinaryV18
docs/handshake.md Connect, Init, AuthenticateOnce
docs/world-join.md The four JoinGame(226) messages
docs/post-join.md Raise sequence after JoinGame
docs/events.md Event-code catalog
docs/attestation.md Event 67 construction (per-build secrets omitted)
docs/http-api.md Login / reservation HTTPS
docs/metadata.md BeeByte global-metadata.dat decryption (PC + Android)

Per-event payload schemas (outer GP18 + inner blobs) live in docs/events/.

Scripts

Python 3.9+, stdlib only.

# decode a Photon message, an init POST, or a raw GpBinaryV18 value
python3 scripts/decode_gp_binary.py --selftest
python3 scripts/decode_gp_binary.py --hex 'f302fd01f40322'
python3 scripts/decode_gp_binary.py capture.bin

# decrypt BeeByte-obfuscated IL2CPP metadata (writes a full decrypted copy)
python3 scripts/decrypt_metadata_android.py global-metadata.dat out.bin
python3 scripts/decrypt_metadata_pc.py global-metadata.dat out.bin

encode(decode(b)) == b holds for canonical GpBinaryV18 input, including captured Init and JoinGame messages.

Constants worth pinning

Item Value
Photon AppId bf0942f7-9935-4192-b359-f092fa85bef1
Client version (init query) 5.1.17.0
Serialization GpBinaryV18, sid=31
WSS subprotocol GpBinaryV18
Datagram GCM AES-256, AAD = 7-byte header ‖ 12-byte nonce, tag 16 B trailing
Fragment payload (plaintext) MTU − 48
PUN view-id stride 100 000 per actor (stock PUN is 1 000)
RaiseEvent param order 252 or 246, then 244, then 245

About

Independent documentation of VRChat's Photon transport protocol (GpBinaryV18): framing, AES-256-GCM datagram encryption, handshake, world-join, per-event schemas, and IL2CPP metadata decryptors.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages