Skip to lesson
Game Hacking Academy
Preparing lesson text…
Exit reader mode
Voice and speed

Preparing read-aloud controls…

Lesson 9.7 Listening edition

Local Proxies and Bidirectional Streams

Understand how a local proxy relays two byte streams while preserving direction, framing, and the original messages.

About 20 minutes

Section: What a proxy does.

A local T C P, transmission control protocol, proxy accepts a client connection, opens a connection to your local server, and relays bytes both ways.

In brief: A relay preserves both directions of the conversation while providing a place to observe their bytes.

Lesson diagram

Begin as a transparent relay. If the protocol fails before you modify anything, fix the relay first. This first version treats bytes as bytes. Only after it passes the normal Wesnoth login should the proxy switch to the framed, message-aware upstream path below. The two modes have different failure behavior: a transparent relay does not need to decode; a message-aware relay must decide what to do when decoding fails.

Section: Restrict both endpoints.

A proxy is easy to connect to the wrong service if an address is copied from a different test. Validate both endpoints as loopback addresses before opening either connection; this helper returns the accepted address so the caller cannot forget which one passed the check:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

This course proxy is intentionally not a general remote interception tool.

Section: Relay one direction.

The standard library can copy a stream until EOF:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

T C P is full duplex, so run two relays:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

For many connections, an async runtime such as Tokio scales better. Threads keep the first lab easy to see.

Section: Half-closes matter.

One direction can reach EOF while the other still has data to send. A reliable local Wesnoth proxy should use shutdown(Write) on the destination after that relay finishes, then let the server-to-client relay deliver any remaining game messages before both sockets close.

Alternative: One side can stop speaking while still listening. Closing both directions immediately discards that distinction.

Section: Log metadata, not unlimited payloads.

Useful bounded log lines include:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

If you parse frame previews, cap their length and avoid logging secrets or full chat histories by default.

Section: Diagnose handshake failures.

A failed local handshake

When a transparent proxy fails, compare direct and proxied captures:

Did both directions start?

Were bytes changed or dropped?

Did the client send the server’s port or address inside the protocol?

Is there TLS (Transport Layer Security, the encryption behind HTTPS) or a certificate bound to a hostname?

Did a relay block because it waited for EOF?

Did one side expect a U D P, user datagram protocol, companion channel?

Comparing the handshake data

Do not bypass certificate validation. For encrypted protocols, create a development configuration with certificates you control.

Section: Parse without changing.

The safest next step is a tee:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Keep parsing off the relay’s critical path. If the parser cannot keep up, drop log entries rather than blocking the connection. This tee is for observation. It cannot decide where to insert a new message, because a T C P read does not identify a frame boundary. The insertion path therefore waits for a complete bounded frame and handles parse errors explicitly.

Bound the parser’s queue as well as each frame. What a full queue may discard depends on the message meaning: a newer position snapshot may replace an older one under a documented policy, but discarding an unprocessed item grant loses the item. The tee can drop its log copies only because the relay still forwards every original frame; the forwarding path itself never drops game bytes to keep up.

Narration: Observing first provides a baseline: later edits can be compared against traffic that the relay already handled correctly.

Section: Inject a real local Wesnoth chat response.

The original lab used proxy port 27015 and server port 15000. Start wesnothd.exe, start the local proxy, then tell Wesnoth to connect to localhost:27015. Reaching the normal lobby through the proxy proves both directions work.

For injection, one task must own the upstream writer so two threads cannot interleave bytes. After the special four-zero negotiation has passed, have the client-to-server path read one complete Wesnoth frame at a time. Forward the original bytes unchanged, but inspect a decoded copy:

Code example. The code’s comments explain: 📦 `read_frame` returns one complete length-prefixed message. A plain `read` here could split one frame or combine several frames. A decode error stops this modified upstream path before sending this frame. The transparent relay above has no such dependency on parsing. 📦 Preserve the legitimate game packet exactly as captured and send it first; inspecting a copy must not silently replace normal traffic. 🔒 This task is the sole owner of `server`, so the original and injected frames cannot be interleaved by the reverse relay.

Keep the server-to-client direction as an ordinary transparent relay. The handshake needs one small state flag because its first four zero bytes are not a gzip frame; forward those four bytes directly before switching the upstream parser into framed mode.

The contains check is enough only for this controlled one-command fixture. It could match that text inside an unrelated W M L field. A general proxy should parse the W M L tree from Lesson 9.4 and inspect the actual chat-message field and session state before inserting a response. The complete lab below uses the same narrow shortcut, so do not treat it as a general message router.

Now send \wave from the real Wesnoth client. The server receives both the original command and the injected Hello! message on the already authenticated connection. This is the practical advantage of the proxy: the official client performs its own login while the proxy observes and modifies the loopback traffic.

Section: Verify the proxy.

The local proxy is correct when:

direct and proxied sessions behave the same;

byte counts agree with a packet capture;

either side closing ends the session cleanly;

an undecodable frame in the message-aware mode returns a clear error before that frame is forwarded; proxy_pair then shuts down both sockets before waiting for the reverse relay. A separate transparent mode could relay bytes without decoding them;

non-loopback endpoints are rejected;

the only modification is the documented \wave response;

removing both decoding and injection returns the proxy to byte-for-byte transparent behavior.

Section: Run the complete proxy.

Start wesnothd.exe on its normal local port, then run:

Terminal window

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Connect the official Wesnoth 1.14.9 client to localhost:27015. Type \wave after login and confirm the injected Hello! appears. The complete two-direction relay is wesnoth_proxy.rs. It binds only to loopback, forwards the unframed negotiation, parses bounded upstream frames, preserves each original frame, injects through the single upstream writer, half-closes both directions, and keeps the server-to-client stream transparent.

Additional detail: Complete lab source: wesnoth_proxy.rs.

Code example. The code’s comments explain: ⚠️ The negotiation request is the protocol's one unframed client message. ✅ EOF stops this direction before any incomplete frame is forwarded. 🔁 Forward the authentic frame even when it does not trigger the lab. 🧹 Signal “no more client bytes” while leaving the read half available for the downstream task to drain the server's final response. 🔀 Clones share each socket but each thread owns one traffic direction. Only `upstream` ever writes to the game server, preserving frame order.

Only the upstream task writes to the server. That single-writer rule prevents the original frame and injected frame from being interleaved. The downstream task stays byte-for-byte transparent, so one experimental hook cannot accidentally rewrite both directions.

The proxy needs T C P because its two endpoints are network clients. When two cooperating programs run on the same computer, they can exchange the same kind of bounded message through shared memory or a named pipe instead. Lessons 9.8 and 9.9 compare those local transports.