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
| Field | Type | Description |
|---|---|---|
type | string | Must be RealityClient. |
next | string | Node that carries the TLS and Reality record stream to the server. |
settings | object | Must be a non-empty object. |
settings.sni | string | Required by the internal TlsClient; this is the visitor TLS server name. |
settings.password | string | Shared 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.
| Field | Default | Description |
|---|---|---|
sni | required | Visitor TLS SNI used by the internal TlsClient. |
verify | true | Boolean certificate verification setting passed to the internal TlsClient. |
ech-sni-trick | unset | Optional non-empty string passed to the internal TlsClient. |
x25519mlkem768 | true | Boolean that enables or disables the hybrid TLS group in the internal TlsClient. |
verbose | false | Boolean that enables extra internal TLS state logging. |
password | required | Shared Reality secret containing 1 through 32 UTF-8 bytes. |
algorithm | chacha20-poly1305 | AEAD algorithm. Accepts chacha20-poly1305, chacha20poly1305, aes-gcm, aes-256-gcm, or aes256gcm. |
method | same as algorithm | Alias for algorithm. Used only when algorithm is absent. |
salt | waterwall-reality | Salt used in key derivation. An explicitly configured value must contain 1 through 32 UTF-8 bytes and match the server. |
kdf-iterations | 12000 | Integer 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:
- initializes its per-line state
- forwards
Initinto the internalTlsClient - 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:
| Direction | Behavior |
|---|---|
| Upstream payload | Cleartext payload from the previous node is encrypted and framed as TLS-like application-data records, then sent to next. |
| Downstream payload | Reality 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 cover | Visible Reality body |
|---|---|
| TLS 1.3 | Reality-AEAD(payload + encrypted inner type 0x17) + 16-byte tag; no visible nonce/prefix; body length P + 17, maximum 16401. |
| TLS 1.2 ChaCha20-Poly1305 | Reality-AEAD(payload) + 16-byte tag; no visible nonce/prefix; body length P + 16, maximum 16400. |
| TLS 1.2 AES-GCM | 8-byte explicit nonce + Reality ciphertext + 16-byte tag; maximum 16408. |
| TLS 1.2 AES-CBC/SHA-1 | 16-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:
| Field | Value |
|---|---|
| Content type | 0x17 application data |
| Version | 0x0303 |
| Length | Selected profile's visible prefix plus authenticated opaque body |
Control alerts use BoringSSL-compatible public shapes:
| Negotiated cover | Alert header and body |
|---|---|
| TLS 1.3 | Outer 0x17, version 0x0303, 19-byte body; encrypted plaintext is the two-byte alert plus inner type 0x15. |
| TLS 1.2 AES-GCM | Outer 0x15, version 0x0303, 26-byte body with an 8-byte explicit nonce. |
| TLS 1.2 AES-CBC/SHA-1 | Outer 0x15, version 0x0303, 48-byte body with a fresh 16-byte IV and block-aligned ciphertext. |
| TLS 1.2 ChaCha20-Poly1305 | Outer 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
RealityClienthas 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
21bytes of required left padding:5bytes for the header and the worst-case16-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
16384bytes. Larger WaterWall payload callbacks are split immediately;32768bytes 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
RealityClientdepends onTlsClient; the build requiresINCLUDE_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, andalgorithmmust matchRealityServer.- 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-sizesetting 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
| Property | Value |
|---|---|
| Position | Middle stream tunnel |
Requires next | Yes |
| Previous layer | Anything |
| Next layer | Anything |
| Per-line state | Yes |
| Creates lines | No |
| Destroys lines | No |
| Required left padding | 21 |