FixCraft YUME stealth transport

YUME stealth and obfuscation

YUME stacks three independent layers of byte-shape camouflage on top of TLS 1.3. Each one defends against a different kind of observer; they’re orthogonal and can be enabled or disabled independently. All three are on by default.

Layer 1: real TLS 1.3 with a browser fingerprint

The TLS handshake is real, not forged. OpenSSL emits a genuine ClientHello, but its cipher suites, supported groups, signature algorithms, and ALPN list are configured to match a specific browser profile. JA3 / JA4 hashes fall in the browser cluster, and a passive TLS-fingerprint observer sees the same handshake shape they’d see from the configured browser.

Source: src/core/tls_stealth.cpp, src/core/tls_fingerprint.cpp.

Profile flag Mimics
--profile chrome (default) Chrome 135
--profile firefox Firefox 126
--profile safari Safari 17
--no-stealth Bare OpenSSL defaults; distinguishable as YUME, not recommended in hostile networks

Profile rotation per N connections is available via --tls-stealth-rotate and --tls-stealth-rotation-interval <N>.

Layer 2: HTTP/2 carrier handshake (--obfs)

After TLS handshake, the client emits the bytes a real Chrome would: an HTTP/2 connection preface (PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n), Chrome-shaped SETTINGS, a WINDOW_UPDATE, and a HEADERS frame opening stream 1 with a POST request. The path is /<token>/<nonce> where <token> = HMAC-SHA256(K, sni || hour_epoch || "yume-obfs-v2") truncated to 16 bytes hex, and K is HKDF-derived from --obfs-secret. The server replies with canned SETTINGS + SETTINGS-ACK + HEADERS :status=200 content-type=application/grpc-web+proto.

To a stateless DPI box, the first ~150 cleartext bytes of every YUME connection look exactly like a Chrome → CDN gRPC-web request.

Source: src/core/obfs_h2.cpp, src/core/obfs_signal.cpp.

Flag Effect
--obfs (default on) enable HTTP/2 carrier handshake
--no-obfs disable; tunnel goes raw YUME after TLS
--obfs-secret <string> shared secret used to bind the path token to a peer; client and server must agree

If --obfs-secret is unset on either side, the server falls back to a structural check (path matches /<32hex>/<16hex>). That defeats casual probes but doesn’t pin the tunnel to a specific authorised peer; set --obfs-secret on both ends for strict pinning.

The token rotates every hour. The verifier accepts ±1 hour of clock skew. A captured token cannot be replayed beyond that window and cannot decrypt the inner stream regardless.

Layer 3: real HTML facade (--real)

A browser that hits the same hostname and port with GET / HTTP/1.1 is served the configured HTML page (or a Wikipedia redirect by default). YUME clients and browsers cohabit on port 443. An active prober that completes TLS and sends a normal browser request gets a normal-looking web page back.

Flag Effect
--real serve a real HTML page to non-YUME requests
--real-index <path> HTML file to serve for GET /
--real-secret <string> embed an HMAC-derived hidden blob in the HTML (used for downstream identification by other YUME tools, unrelated to --obfs-secret)
--real-secret-file <path> load (or auto-generate) the secret from a file

--real and --obfs are independent and can both be on. They’re demuxed by the first cleartext bytes after TLS: a PRI * HTTP/2.0 preface goes to the --obfs validator; an HTTP/1.1 method-line goes to the --real HTML server.

What this defends against, what it doesn’t

Defends well against:

Does not defend against:

Quick recipes

Strict per-peer pinning. Both ends have an out-of-band shared secret:

# server
yumed --listen 443 --cert--key--auth-keys--obfs --obfs-secret 'shared-string'

# client
yume --server fixcraft.net --auth id_ed25519 --socks 1080 --obfs --obfs-secret 'shared-string'

Coexisting with a real website. Port 443 serves both browsers and YUME clients:

yumed --listen 443 --cert--key--auth-keys\
      --obfs --obfs-secret\
      --real --real-index ./www/index.html --real-secret-file ./.secrets/html_secret

Profile-rotating client. A different browser fingerprint every N connections:

yume --server--auth--socks 1080 --tls-stealth-rotate --tls-stealth-rotation-interval 50

Disable obfs entirely. Fastest, but recognisable as a YUME server to anything that probes:

yume --server--auth--socks 1080 --no-obfs

Inspecting what’s on the wire

Run a local-loopback yumed and capture with tcpdump:

sudo tcpdump -A -i lo -s 0 'port 18443'

The TLS handshake and ciphertext are encrypted, but the plaintext bytes that the local TLS code writes can be observed in the application’s own logs. With --obfs on, the post-handshake plaintext should begin with PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n followed by a SETTINGS frame; that’s the byte-shape DPI sees after decryption (or that any active TLS-MITM probe would see if the network can do TLS interception).