Skip to content

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 logic
  • transport/internet/reality/config.go -- GetREALITYConfig(), config extraction

Architecture

mermaid
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
    end

Client Implementation

UClient

reality.UClient (reality/reality.go:117-277) performs the REALITY handshake:

Step 1: Build uTLS Connection

go
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):

go
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):

go
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

go
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:

go
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:

go
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:

go
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:

  1. Reads the ClientHello
  2. Decrypts the Session ID using its private key
  3. Validates the Short ID, timestamp, and client version
  4. If authorized: generates an ed25519 certificate signed with the shared AuthKey
  5. If unauthorized: proxies to the dest server transparently

Configuration

Client Config

From reality/config.go:74-83:

go
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:

go
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:

  1. Check Short ID against its allowed list
  2. Verify timestamp is within MaxTimeDiff
  3. 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):

go
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):

go
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):

go
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:

go
// 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:

go
// 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/LimitFallbackDownload can throttle bandwidth for unauthorized (fallback) connections (config.go:41-49).

Technical analysis for re-implementation purposes.