Transport Layer
This chapter specifies how Konstruct messages are carried from a client to the server and (currently) onward to the recipient. It defines the FFI binary envelope (CFE), the on-wire framing, the gRPC service surface, and the VEIL anti-censorship transport tier.
Keywords MUST, MUST NOT, SHOULD, MAY are per RFC 2119.
6.1 Layering overview
Plaintext (application)
│
▼
┌─ Konstruct cryptographic core (Rust)
│ X3DH/PQXDH + Double Ratchet (Ch. 4-5)
│ padding (PKCS#7 mod 255)
│ WirePayload pack (§6.2)
▼
AEAD ciphertext frames
│
▼
CFE binary envelope (§6.3) over UniFFI / JNI / direct C FFI
│
▼
gRPC bidirectional stream (§6.4) — HTTP/2 over TLS 1.3
│
▼ (optional, when direct TLS is blocked)
VEIL tier (§6.5) — veil-front (obfs4 / WebTunnel retired)
│
▼
TCP / QUIC over the public internet
Layers below Konstruct (TLS 1.3, HTTP/2, TCP, QUIC) are standard and out of scope here.
A QUIC/HTTP-3 transport (construct-transport) is also in production as
an alternative to the HTTP/2 path, selected by the client-side transport
router with an HTTP/2 fallback; it is not yet specified normatively in
this chapter (planned for a future revision).
6.2 Wire format (WirePayload)
Every encrypted Konstruct message crosses the wire as a WirePayload — a packed binary frame with the layout defined in §5.3. Restated for completeness:
| Field | Type | Size | Description |
|---|---|---|---|
| message_number | u32 BE | 4 B | Double Ratchet sending counter Ns |
| dh_public_key | bytes | 32 B | Current sending DH public (DHs.pub) |
| otpk_id | u32 BE | 4 B | OPK id consumed by the X3DH initiator; 0 if N/A |
| kyber_otpk_id | u32 BE | 4 B | Kyber-OPK id consumed; 0 if N/A |
| kem_len | u16 BE | 2 B | Length of the KEM ciphertext that follows; 0 in Suite 1 |
| prev_chain_length | u32 BE | 4 B | Previous-chain length PN |
| suite_id | u16 BE | 2 B | 0x0001 Suite 1 or 0x0002 Suite 2 |
| kem_ct | bytes | kem_len B | ML-KEM-768 ciphertext (Suite 2 only, 1088 B) |
| aead_frame | bytes | variable | `nonce(12) |
Total fixed header size: 52 bytes
(construct-core/src/wire_payload.rs:30).
A WirePayload is the unit of work the Double Ratchet produces and
consumes. It MUST NOT carry plaintext routing fields outside of
suite_id and the lengths needed to parse the envelope; metadata such
as sender, recipient, timestamps, and conversation ids belongs in the
transport-level wrapper, not in the WirePayload itself.
6.3 CFE — Construct Frame Encoding
When a WirePayload (or any other typed protocol message) crosses the FFI boundary between the Rust core and a platform binding (Swift, Kotlin), it MUST be wrapped in a CFE envelope. CFE eliminates the JSON parsing attack surface previously present at the FFI line.
6.3.1 Envelope layout
CFE envelope (16-byte header + payload) ::=
magic : [u8; 2] = [0x43, 0x46] -- "CF"
version : u8 = 0x01
msg_type : u8 -- CfeMessageType enum tag
payload_len : u32 BE -- length of the MessagePack body
flags : u8 -- reserved, MUST be 0
reserved : [u8; 3] = [0x00; 3]
crc32 : u32 BE -- CRC-32 over (magic..reserved || payload)
payload : [u8; payload_len] -- MessagePack body
Header constants are defined in construct-core/src/cfe/envelope.rs:
CFE_MAGIC = [0x43, 0x46], CFE_VERSION = 0x01,
CFE_HEADER_LEN = 16.
6.3.2 Required validations
A receiver of a CFE envelope MUST:
- Verify the magic bytes match exactly. Mismatch → reject.
- Verify the version is supported (currently only
0x01). - Verify
payload_lendoes not exceed the implementation-defined maximum (reference: 256 KiB). - Verify
crc32matches recomputed CRC over the header (with the crc32 field zeroed) plus the payload. - Decode the payload as MessagePack only if all checks above pass.
These checks are what make CFE strictly safer than the JSON
predecessor: a malformed envelope is rejected before any
deserialisation is attempted, and the bounded payload_len prevents
unbounded allocation.
6.3.3 Message types
The msg_type byte selects the MessagePack schema for the payload.
The reference defines 14 incoming and 28 outgoing message types
covering events such as IncomingMessage, SessionStateChanged,
Action::SendEncryptedMessage, etc. The enum is normative; new
variants MUST be added with a new value, never by repurposing an
existing one.
6.4 gRPC service surface
Konstruct uses gRPC over HTTP/2 over TLS 1.3 as the primary transport.
The protobuf service definitions live in the
konstruct-msg/construct-protos package (separate repository); the
ones relevant to a client implementer are:
| Service | RPC | Direction |
|---|---|---|
| AuthService | GetPowChallenge, RegisterDevice, AuthenticateDevice, RefreshToken | unary, no JWT required |
| UserService | CheckUsernameAvailability | unary, no JWT required |
| UserService | (other) | unary, JWT required |
| DeviceService | * | unary, no JWT required |
| MessagingService | MessageStream | bidirectional stream, JWT required |
| SignalingService | Signal | bidirectional stream, JWT required |
| KeyService | prekey upload / fetch | unary, JWT required |
JWT-required RPCs MUST carry an authorization: Bearer <token> header
and an x-user-id header. The reference adds them in an
AuthInterceptor; an interoperable client implementation MUST do the
same.
MessageStream carries WirePayload frames (§6.2) as bytes in the
request/response stream. The server treats the byte field as opaque
and routes by metadata fields outside the WirePayload.
6.5 VEIL — anti-censorship transport tier
When the direct gRPC-over-TLS path is blocked, throttled, or fingerprinted by an adversarial network, the client MAY route through VEIL instead. VEIL is a pluggable transport tier with several backend strategies:
| Backend | Status | Wire shape on the network |
|---|---|---|
| veil-front | Production | TLS 1.3 to an honest cover application; the relay routes valid AUTH frames to the tunnel and everything else to the cover app via a constant-shape gate. The primary (and only production) obfuscation backend on mobile. |
| obfs4 / WebTunnel | Retired | Superseded by veil-front and cut by active DPI in the target region. Adapters remain in-tree but are not registered on mobile builds; the standalone relay repository is archived. |
The VEIL coordinator (in construct-veil/src/veil/coordinator.rs)
runs a happy-eyeballs probe race over the configured backends and
keeps per-backend persistent quality scores in a small SQLite store.
The winner is dispatched as the data plane; losers are cancelled.
6.5.1 Pluggable transport selection
A client MUST honour:
- An explicit user preference (
VeilMode = .off | .auto | .on). - A network-fingerprint hash that namespaces per-backend score caches (so that scores from network A do not pollute scores on network B).
- A per-backend
MethodId(obfs4 = 0,webTunnel = 1,masque = 2,veilFront = 3). Method ids are normative on the Rust C FFI ofconstruct-veil.
6.5.2 Constant-shape gate (veil-front)
For the veil-front backend, the relay MUST satisfy:
- Failed authentication MUST be routed to the cover application using the cover's own response timing and shape. There MUST NOT be a separate "tunnel rejected" code path with distinguishable timing.
- Frame-level length bucketing MUST be applied to the tunnel direction so that record-length distributions are bounded.
veil_front_ticket_b64MUST be supplied by the client; an empty ticket field MUST cause the veil-front method to be excluded from the probe race rather than silently downgraded.
The constant-shape requirement is the load-bearing property of veil-front; if a future deployment violates it, the wire becomes distinguishable from the cover application and the construction's purpose is defeated.
6.5.3 Connection ladder and graceful degradation
In VeilMode = .auto (the default) the client is direct-first: it
attempts the plain gRPC/QUIC path first and escalates to VEIL only on a
real connection failure. It MUST NOT pre-activate a relay purely because
of coarse geography — a censored network that is momentarily reachable
directly should use the direct path, and a relay is engaged only when the
direct attempt actually fails.
The result is a ladder that degrades with the hostility of the network:
| Network tier | Path used |
|---|---|
| Free / uncensored | Direct gRPC/QUIC; HTTP/2 fallback. |
| DPI blacklist (throttle / fingerprint direct TLS) | Escalate to veil-front (honest-front TLS to a cover application). |
| National allowlist (only permitted destinations reachable) | Not crossable by obfuscation alone — this is an explicit non-goal of the transport tier. |
| Blackout (no connectivity) | Out of scope for the server-routed transport; an offline-mesh foundation is a separate design track. |
This is deliberately honest: obfuscation buys reachability against classification, not against an adversary who drops everything except an allowlist. See Architecture Overview for the tiered model. VEIL also is not a metadata-hiding layer on its own — it helps a connection blend in, but a network observer who already sees the connection can still infer timing and volume; sealed sender (Chapter 8) and padding (§5.7) are the metadata mechanisms, not VEIL.
6.6 Transport guarantees and non-guarantees
What the transport layer guarantees
| Property | Mechanism |
|---|---|
| Tamper detection on the FFI line | CFE CRC-32 + magic bytes |
| Bounded FFI input size | CFE payload_len cap |
| Server cannot read message content | Cryptographic core (Ch. 5), not the transport |
| Length privacy from a network observer | PKCS#7 padding (§5.7) + VEIL length bucketing |
| Censorship resistance | VEIL backends (§6.5) |
| Memory safety at the FFI boundary | CFE owned Vec<u8> (no raw pointers) + bounded length |
What the transport layer does not guarantee
| Exposure | Mitigation status |
|---|---|
| IP visibility to the relay operator | Inherent — a relay terminates your connection and sees its source address. Server-side, only a salted hash is retained (Ch. 8 §8.6); to keep the address off the path, route through VEIL, a VPN, or Tor. |
| Server sees per-connection metadata (timestamps, session durations) | Inherent to a client–server design; raw client IPs are not persisted (salted hash only). |
| Active DPI in a hostile region | veil-front (honest-front TLS) is the production answer; the retired obfs4/WebTunnel backends were cut by active DPI. Even veil-front does not cross a national allowlist (where only explicitly permitted destinations are reachable) — see Architecture Overview. |
| Sender identity to the server | Removed on all user traffic — sealed sender is on by default (Ch. 8); the identified path is fail-closed, not a silent downgrade. |
6.7 Configuration constants summary
| Constant | Value | Source |
|---|---|---|
| CFE magic | [0x43, 0x46] | cfe/envelope.rs:8 |
| CFE version | 0x01 | cfe/envelope.rs:9 |
| CFE header length | 16 bytes | cfe/envelope.rs:10 |
| CFE max payload | 256 KiB | reference implementation cap |
| WirePayload header length | 52 bytes (fixed) | wire_payload.rs:30 |
| Padding modulus | 255 | traffic_protection/padding.rs |
| VEIL probe timeout | implementation-defined (reference: a few seconds per backend) | construct-veil/src/veil/coordinator.rs |
6.8 References
- WirePayload:
construct-core/src/wire_payload.rs - CFE envelope:
construct-core/src/cfe/envelope.rs,cfe/types.rs - gRPC service definitions:
konstruct-msg/construct-protos - VEIL coordinator and FFI:
konstruct-msg/construct-veil