REALITY Security Layer
Introduction
REALITY is a custom security protocol that extends TLS 1.3 to allow the server to impersonate any real TLS server without needing its certificate or private key. Instead of presenting its own certificate, the REALITY server proxies the TLS handshake to a legitimate "dest" server. Authorized clients (who know the server's public key and a Short ID) can authenticate the server through a custom scheme embedded in the TLS 1.3 ClientHello Session ID. Unauthorized clients or active probers receive a genuine certificate from the dest server, making the proxy indistinguishable from the real service. No Certificate Authority is needed.
Key Files
transport/internet/reality/reality.go-- Client (UClient) and server (Server) connection logictransport/internet/reality/config.go--GetREALITYConfig(), config extraction
Architecture
sequenceDiagram
participant C as REALITY Client
participant S as REALITY Server
participant D as Dest Server (e.g. google.com)
Note over C: Knows: PublicKey, ShortId, ServerName
Note over S: Knows: PrivateKey, ShortIds[], ServerNames[]
C->>S: TLS ClientHello (SessionId = encrypted auth)
S->>S: Decrypt SessionId, verify ShortId + timestamp
alt Authorized Client
S->>C: TLS ServerHello (with ed25519 cert signed by AuthKey)
C->>C: Verify cert signature with AuthKey
Note over C,S: Authenticated REALITY connection
else Unauthorized / Probe
S->>D: Forward ClientHello
D->>S: Real ServerHello + Certificate
S->>C: Forward real response
Note over C,S: Client sees real google.com cert
Note over S: Connection becomes transparent proxy to Dest
endClient Implementation
UClient
reality.UClient (reality/reality.go:117-277) performs the REALITY handshake:
Step 1: Build uTLS Connection
func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destination) (net.Conn, error) {
uConn := &UConn{Config: config}
utlsConfig := &utls.Config{
VerifyPeerCertificate: uConn.VerifyPeerCertificate,
ServerName: config.ServerName,
InsecureSkipVerify: true,
SessionTicketsDisabled: true,
}
fingerprint := tls.GetFingerprint(config.Fingerprint)
uConn.UConn = utls.UClient(c, utlsConfig, *fingerprint)InsecureSkipVerify is true because verification is handled by REALITY's custom VerifyPeerCertificate, not Go's built-in chain.
Step 2: Construct Authenticated Session ID
The Session ID (32 bytes) is constructed and encrypted (reality.go:138-176):
hello := uConn.HandshakeState.Hello
hello.SessionId = make([]byte, 32)
// Bytes 0-2: Xray version
hello.SessionId[0] = core.Version_x
hello.SessionId[1] = core.Version_y
hello.SessionId[2] = core.Version_z
hello.SessionId[3] = 0 // reserved
// Bytes 4-7: Current Unix timestamp (big-endian)
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
// Bytes 8-15: Short ID (8 bytes)
copy(hello.SessionId[8:], config.ShortId)Step 3: Derive Auth Key
The Auth Key is derived from the ECDH shared secret (reality.go:152-169):
publicKey, _ := ecdh.X25519().NewPublicKey(config.PublicKey)
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
// Falls back to MlkemEcdhe for post-quantum key exchange
uConn.AuthKey, _ = ecdhe.ECDH(publicKey)
// HKDF derivation
hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20], []byte("REALITY")).Read(uConn.AuthKey)The HKDF uses the first 20 bytes of ClientHello.Random as salt and the literal string "REALITY" as info.
Step 4: Encrypt Session ID
aead := crypto.NewAesGcm(uConn.AuthKey)
aead.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw)- Nonce: Last 12 bytes of
ClientHello.Random(bytes 20-31) - Plaintext: First 16 bytes of the Session ID
- Additional Data: The entire raw ClientHello
- Output: 16 bytes ciphertext + 16 bytes GCM tag = overwrites full 32-byte Session ID
The encrypted Session ID is then copied back into the raw ClientHello bytes (reality.go:175).
Step 5: Verify Server Certificate
VerifyPeerCertificate (reality/reality.go:76-115) checks if the server is a genuine REALITY server:
func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
certs := // parse rawCerts
if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok {
h := hmac.New(sha512.New, c.AuthKey)
h.Write(pub)
if bytes.Equal(h.Sum(nil), certs[0].Signature) {
// Optional ML-DSA-65 verification for post-quantum
if len(c.Config.Mldsa65Verify) > 0 {
// Verify ML-DSA-65 signature over ClientHello + ServerHello
}
c.Verified = true
return nil
}
}
// Fall back to standard x509 verification (real cert from dest server)
opts := x509.VerifyOptions{DNSName: c.ServerName, ...}
certs[0].Verify(opts)
return nil // verification passes (it is a real cert), but Verified stays false
}The REALITY server generates an ed25519 certificate whose:
- Public key: ed25519 public key
- Signature:
HMAC-SHA512(AuthKey, PublicKey)
If this HMAC matches, the client knows it is talking to the real REALITY server. If not, the certificate is verified normally (it came from the dest server), but c.Verified remains false.
Step 6: Handle Verification Failure
If uConn.Verified is false after handshake (reality.go:183-274), the client mimics real browser behavior:
if !uConn.Verified {
// "Spider" mode: crawl the dest server to generate realistic traffic
client := &http.Client{Transport: &http2.Transport{...}}
// GET pages, follow links, add cookies
// This makes the connection look like a real browser visiting the site
time.Sleep(randomDuration)
return nil, errors.New("REALITY: processed invalid connection")
}The spider crawls the dest server's pages with realistic timing, then closes the connection. This makes active probing indistinguishable from a real browser visit.
Server Connection
reality.Server (reality/reality.go:52-55) wraps with the reality library:
func Server(c net.Conn, config *reality.Config) (net.Conn, error) {
realityConn, err := reality.Server(context.Background(), c, config)
return &Conn{Conn: realityConn}, err
}The actual server logic is in the github.com/xtls/reality library, which:
- Reads the ClientHello
- Decrypts the Session ID using its private key
- Validates the Short ID, timestamp, and client version
- If authorized: generates an ed25519 certificate signed with the shared AuthKey
- If unauthorized: proxies to the dest server transparently
Configuration
Client Config
From reality/config.go:74-83:
func ConfigFromStreamSettings(settings *internet.MemoryStreamConfig) *Config {
config, ok := settings.SecuritySettings.(*Config)
if !ok { return nil }
return config
}Client needs:
- ServerName: SNI for the TLS handshake (the impersonated server)
- Fingerprint: uTLS fingerprint (required, cannot be empty)
- PublicKey: Server's X25519 public key (32 bytes)
- ShortId: Client's Short ID (up to 8 bytes)
- SpiderX: Starting path for the spider crawl
- SpiderY: Array of 10 int64 values controlling spider behavior (cookie padding, concurrency, intervals, etc.)
- Show: Debug mode that prints REALITY state to stdout
- Mldsa65Verify: ML-DSA-65 public key for post-quantum verification
Server Config
GetREALITYConfig (reality/config.go:16-58) builds the reality.Config:
func (c *Config) GetREALITYConfig() *reality.Config {
config := &reality.Config{
Show: c.Show,
Type: c.Type,
Dest: c.Dest, // dest server address
Xver: byte(c.Xver), // PROXY protocol version
PrivateKey: c.PrivateKey, // X25519 private key
MinClientVer: c.MinClientVer, // minimum client version
MaxClientVer: c.MaxClientVer, // maximum client version
MaxTimeDiff: time.Duration(c.MaxTimeDiff) * time.Millisecond,
SessionTicketsDisabled: true,
}
// Populate ServerNames map and ShortIds map
config.ServerNames[serverName] = true
config.ShortIds[shortId] = true
// Optional ML-DSA-65 signing key
// Optional rate limiting for fallback connections
return config
}Server needs:
- PrivateKey: X25519 private key (32 bytes)
- ServerNames: Allowed SNI values
- ShortIds: Allowed Short IDs (map of [8]byte)
- Dest: Address of the real server to proxy to for unauthorized clients
- MaxTimeDiff: Maximum allowed clock skew (milliseconds)
- MinClientVer / MaxClientVer: Allowed Xray client version range
Session ID Wire Format
Byte 0-2: Xray version (x.y.z)
Byte 3: Reserved (0)
Byte 4-7: Unix timestamp (big-endian uint32)
Byte 8-15: Short ID (8 bytes, may be partially zero)
[Encrypted with AES-GCM using AuthKey]
[Nonce = ClientHello.Random[20:32]]
[AAD = raw ClientHello bytes]The server decrypts this to:
- Check Short ID against its allowed list
- Verify timestamp is within
MaxTimeDiff - Check client version is within allowed range
Post-Quantum Support
Key Exchange
REALITY supports X25519-MLKEM768 key exchange when available in the TLS fingerprint (reality.go:156-161):
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe
if ecdhe == nil {
ecdhe = uConn.HandshakeState.State13.KeyShareKeys.MlkemEcdhe
}Certificate Verification
Optional ML-DSA-65 (Dilithium) verification (reality.go:88-95):
if len(c.Config.Mldsa65Verify) > 0 {
h.Write(c.HandshakeState.Hello.Raw)
h.Write(c.HandshakeState.ServerHello.Raw)
verify, _ := mldsa65.Scheme().UnmarshalBinaryPublicKey(c.Config.Mldsa65Verify)
if mldsa65.Verify(verify.(*mldsa65.PublicKey), h.Sum(nil), nil, certs[0].Extensions[0].Value) {
c.Verified = true
}
}This provides post-quantum authentication: the server signs HMAC(AuthKey, PublicKey || ClientHello.Raw || ServerHello.Raw) with its ML-DSA-65 private key, and the client verifies with the pre-shared public key.
Key Log Support
Both client and server support TLS key logging for debugging (reality/config.go:61-72):
func KeyLogWriterFromConfig(c *Config) io.Writer {
if len(c.MasterKeyLog) <= 0 || c.MasterKeyLog == "none" { return nil }
writer, _ := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
return writer
}Transport Integration
REALITY integrates with transports at two points:
Client Side
Each transport checks for REALITY config and applies it:
// tcp/dialer.go:89-93
if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
conn, err = reality.UClient(conn, config, ctx, dest)
}Server Side
Transports wrap their listener:
// tcp/hub.go:76-79
if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
l.realityConfig = config.GetREALITYConfig()
go goreality.DetectPostHandshakeRecordsLens(l.realityConfig)
}DetectPostHandshakeRecordsLens is called at startup to fingerprint the dest server's post-handshake behavior, enabling more accurate impersonation.
Implementation Notes
- No CA needed: REALITY does not require any certificates. The server either presents the dest server's real certificate (for unauthorized clients) or a dynamically generated ed25519 certificate (for authorized clients).
- Active probe resistance: Unauthorized connections are transparently proxied to the dest server. The dest server's real certificate is served, making the probe indistinguishable from a direct connection.
- Spider anti-fingerprinting: When a REALITY client receives a real certificate (indicating a probe or misconfiguration), it crawls the dest server like a real browser before disconnecting, preventing timing-based detection.
- Fingerprint required: Unlike TLS, REALITY requires a uTLS fingerprint (
reality.go:133-136). Standard Go TLS cannot be used because REALITY needs access to the ClientHello internals. - Session ID fixed position: The Session ID occupies bytes 39+ of the raw ClientHello (
reality.go:142), which is a fixed position in the TLS 1.3 ClientHello structure. - Clock synchronization: Client and server timestamps must be within
MaxTimeDiff(default varies by configuration). Large clock skew causes authentication failure. - Short ID as access control: Different Short IDs can be assigned to different users/clients, enabling per-user access control without changing keys.
- Rate limiting:
LimitFallbackUpload/LimitFallbackDownloadcan throttle bandwidth for unauthorized (fallback) connections (config.go:41-49).