Skip to main content

RealityClient

RealityClient is the client-side Reality v2 tunnel. It starts each line with a real client TLS handshake through an internal TlsClient, derives fresh directional session material from that handshake, and takes over the established raw connection. TLS 1.3 first completes an authenticated request/ack/confirm handoff; TLS 1.2 retains its immediate profile-aware takeover.

Use it when the first bytes on the wire must look like a normal TLS client session to a visitor domain, while accepted WaterWall payload is protected by a shared Reality password.

Typical Placement

TcpListener -> RealityClient -> TcpConnector

The configured next node is the transport path to the Reality server. RealityClient does not open the remote socket by itself; put a connector or another transport node after it.

Basic Example

{
"name": "reality-client",
"type": "RealityClient",
"settings": {
"sni": "www.example.com",
"verify": true,
"password": "replace-with-a-strong-secret",
"algorithm": "chacha20-poly1305"
},
"next": "server-tcp"
}

Paired with a normal TCP transport:

{
"name": "server-tcp",
"type": "TcpConnector",
"settings": {
"address": "203.0.113.10",
"port": 443
}
}

Required Fields

FieldTypeDescription
typestringMust be RealityClient.
nextstringNode that carries the TLS and Reality record stream to the server.
settingsobjectMust be a non-empty object.
settings.snistringRequired by the internal TlsClient; this is the visitor TLS server name.
settings.passwordstringShared Reality secret containing 1 through 32 UTF-8 bytes. Must match the server.

Settings

RealityClient passes the same settings object to its internal TlsClient, so TLS-facing options such as sni, verify, ech-sni-trick, x25519mlkem768, and verbose follow TlsClient behavior.

FieldDefaultDescription
snirequiredVisitor TLS SNI used by the internal TlsClient.
verifytrueBoolean certificate verification setting passed to the internal TlsClient.
ech-sni-trickunsetOptional non-empty string passed to the internal TlsClient.
x25519mlkem768trueBoolean that enables or disables the hybrid TLS group in the internal TlsClient.
verbosefalseBoolean that enables extra internal TLS state logging.
passwordrequiredShared Reality secret containing 1 through 32 UTF-8 bytes.
algorithmchacha20-poly1305AEAD algorithm. Accepts chacha20-poly1305, chacha20poly1305, aes-gcm, aes-256-gcm, or aes256gcm.
methodsame as algorithmAlias for algorithm. Used only when algorithm is absent.
saltwaterwall-realitySalt used in key derivation. An explicitly configured value must contain 1 through 32 UTF-8 bytes and match the server.
kdf-iterations12000Integer key derivation rounds. Valid range is 1 through 1000000; fractional numbers are rejected.

aes-gcm requires AES-GCM support in the active crypto backend. If it is unavailable, tunnel creation fails.

Credential limits are measured in UTF-8 bytes, not characters, using the same byte length supplied to BLAKE2s. Optional defaults apply only when a key is absent; a present key with the wrong JSON type is a startup error. When both algorithm and method are present, algorithm wins, and a malformed primary value is not rescued by a valid alias. Unknown keys remain tolerated because this settings object is shared with the internal TlsClient.

Runtime Flow

On upstream Init, RealityClient:

  1. initializes its per-line state
  2. forwards Init into the internal TlsClient
  3. waits for the internal TLS handshake to complete

Upstream payload that arrives before takeover completion is queued. When the internal TlsClient reports handshake completion, RealityClient captures the negotiated TLS version, IANA cipher suite, client random, server random, and TLS 1.2 next read/write record sequences while the SSL object is still available. It selects the shared record profile and derives independent client-to-server and server-to-client keys and IVs from the captured binding and password-derived root key. An unsupported suite aborts takeover rather than using a catch-all layout.

TLS 1.2 releases the internal TLS state immediately and keeps its existing record counters and layouts. TLS 1.3 instead retains BoringSSL and sends HANDOFF_REQUEST at client-to-server sequence 0. While waiting for HANDOFF_ACK, the client tries only the expected ACK on a duplicate of each complete record. If authentication fails, the original record and receive sequence are unchanged and the record goes to BoringSSL for normal NewSessionTicket, KeyUpdate, cover application-data, or cover-alert processing. BoringSSL-generated protocol output is sent upstream before later controls. After ACK authenticates at server-to-client sequence 0, the client releases BoringSSL, sends HANDOFF_CONFIRM at client-to-server sequence 1, emits downstream Est, and flushes queued application payload beginning at sequence 2.

After takeover:

DirectionBehavior
Upstream payloadCleartext payload from the previous node is encrypted and framed as TLS-like application-data records, then sent to next.
Downstream payloadReality records from next are parsed, authenticated, decrypted, and forwarded as cleartext to the previous node.

Before the authenticated TLS 1.3 ACK, cover TLS close/error, local finish, or malformed input closes without a synthetic Reality alert because the peer may still be processing genuine TLS. After handoff, invalid framing, authentication failure, or invalid authenticated semantics produces at most one protected fatal bad_record_mac before both sides close when the wire remains writable.

Shutdown is role-specific. A local upstream Finish destroys Reality state and finishes only next; it never fabricates close_notify. A received authenticated server close_notify is consumed without a response, then RealityClient immediately destroys its state and closes both still-open sides. It never waits for a later TCP Finish, so an authenticated peer cannot pin the connection open by withholding FIN. A locally detected authenticated-record failure is the client's only alert-producing path and sends at most one fatal bad_record_mac before both sides close. A received fatal alert likewise closes both sides immediately without a response. A raw transport Finish closes only the remaining local side. No additional setting controls this behavior.

Record Format

Reality records are shaped according to the negotiated cover suite:

Negotiated coverVisible Reality body
TLS 1.3Reality-AEAD(payload + encrypted inner type 0x17) + 16-byte tag; no visible nonce/prefix; body length P + 17, maximum 16401.
TLS 1.2 ChaCha20-Poly1305Reality-AEAD(payload) + 16-byte tag; no visible nonce/prefix; body length P + 16, maximum 16400.
TLS 1.2 AES-GCM8-byte explicit nonce + Reality ciphertext + 16-byte tag; maximum 16408.
TLS 1.2 AES-CBC/SHA-116-byte random explicit IV + block-aligned opaque body; maximum 16432.

For GCM, client-to-server explicit nonces continue BoringSSL's captured next TLS write sequence in big-endian form. For CBC, the opaque body length is round_up(payload + TLS MAC length + 1, block size). An encrypted two-byte payload length and zero filler make the Reality AEAD result match that length; no CBC encryption or TLS CBC MAC is performed.

The TLS-like header uses:

FieldValue
Content type0x17 application data
Version0x0303
LengthSelected profile's visible prefix plus authenticated opaque body

Control alerts use BoringSSL-compatible public shapes:

Negotiated coverAlert header and body
TLS 1.3Outer 0x17, version 0x0303, 19-byte body; encrypted plaintext is the two-byte alert plus inner type 0x15.
TLS 1.2 AES-GCMOuter 0x15, version 0x0303, 26-byte body with an 8-byte explicit nonce.
TLS 1.2 AES-CBC/SHA-1Outer 0x15, version 0x0303, 48-byte body with a fresh 16-byte IV and block-aligned ciphertext.
TLS 1.2 ChaCha20-Poly1305Outer 0x15, version 0x0303, 18-byte body.

The encrypted semantic values are 01 00 for close_notify and 02 14 for fatal bad_record_mac. Data and alerts share one strict sequence per direction. The associated data binds the semantic record kind and negotiated TLS version in addition to the profile and public record image, preventing data/alert substitution. These are Reality-AEAD camouflage records, not genuine TLS closure or TLS-key alert encryption.

TLS 1.3 handoff controls use outer 0x17, legacy version 0x0303, and encrypted inner application type 0x17. Their versioned plaintext contains the control code that must match the AAD kind plus authenticated CSPRNG padding selected by the shared public-length policy. The TLS body length is sampled uniformly from 22..1172 bytes (3..1153 padding bytes), the range observed across the covered BoringSSL post-handshake paths. Padding is not delivered to the application. Request, ACK, confirmation, data, and alerts share the normal directional counters and cannot substitute for one another. There is no JSON control-length setting.

The visible prefix is authenticated camouflage but is not the AEAD nonce. Each direction has an independent Reality sequence starting at zero; the nonce is its secret 12-byte base IV with the big-endian Reality sequence XORed into the final eight bytes. The TLS 1.2 GCM facade sequence is tracked separately.

The associated data binds the profile ID, v2 record domain, direction, Reality sequence number, session ID, TLS header, visible-prefix length, and visible prefix. The receive side authenticates only the exact next Reality sequence. Therefore duplicate, deleted, reordered, reflected, cross-profile, and cross-connection records fail authentication. Counters close before wrap.

Buffer And Lifecycle Notes

  • RealityClient has per-line state for the downstream record parser, queued pre-handshake upstream payload, session ID, directional keys/IVs, and send/receive counters.
  • It does not create or destroy the normal line. It only destroys its own line state when the line finishes or when it closes both directions after a Reality error.
  • During a locally generated fatal alert it enters terminal state before transmission, suppresses re-entrant payload/flow-control callbacks, owns state destruction if the send kills the line, and stops further teardown immediately.
  • The node advertises 21 bytes of required left padding: 5 bytes for the header and the worst-case 16-byte CBC explicit IV.
  • Empty payload buffers are recycled without sending a Reality record.
  • Application plaintext fragments are automatically capped at the native TLS limit of 16384 bytes. Larger WaterWall payload callbacks are split immediately; 32768 bytes becomes exactly two full records, with no cross-callback buffering.

Security Model And Limitations

The reviewed camouflage threat model is a non-terminating on-path observer. It may capture and reassemble the full bidirectional stream; inspect TLS record types, versions, lengths, directions, ordering, timing, visible nonces/IVs, and close behavior; manipulate bytes; and initiate probes while knowing the implementation. It is assumed not to know the Reality password or genuine TLS traffic secrets, not to terminate TLS, and not to control or collude with the configured cover destination. It also cannot alter a completed TLS handshake without failing TLS Finished verification.

Per-session Reality material depends on the fresh client and server randoms from the genuine TLS handshake. There is no independent Reality server challenge dedicated to session freshness. Cross-connection replay protection therefore assumes that the observer cannot predict or control both handshake randoms. A malicious or colluding cover destination that deliberately helps reproduce a previously observed handshake binding is outside this model; defending that stronger model would require an independently authenticated server challenge.

Reality camouflage reproduces selected public TLS record properties, including suite-specific record shapes, lengths, explicit nonces/IVs, controls, alerts, and close behavior. After takeover, however, the records contain Reality-AEAD ciphertext rather than genuine ciphertext under the cover connection's TLS traffic keys. Reality does not claim to survive TLS termination or to reproduce every Chrome timing distribution, write size, burst pattern, or higher-level application semantic. A TLS-terminating observer can test the real traffic keys against later records and is outside the reviewed camouflage boundary.

Operational Notes

  • RealityClient depends on TlsClient; the build requires INCLUDE_TLS_CLIENT=ON.
  • The first phase is a real TLS client handshake, so the sni, certificate verification behavior, and TLS fingerprinting options matter.
  • password, salt, kdf-iterations, and algorithm must match RealityServer.
  • The authenticated TLS 1.3 handoff is incompatible with earlier unpublished v2 builds that switched at main-handshake completion. Upgrade both peers together; TLS 1.2 layouts remain unchanged and there is no old-layout or v1 fallback. The obsolete max-frame-size setting is rejected rather than ignored.
  • The password-derived value is a root key, not a record key. Every real TLS connection derives fresh directional material, and password rotation invalidates future sessions without requiring a replay database.
  • This node is a stream tunnel, not a packet tunnel.

Node Metadata

PropertyValue
PositionMiddle stream tunnel
Requires nextYes
Previous layerAnything
Next layerAnything
Per-line stateYes
Creates linesNo
Destroys linesNo
Required left padding21