Framing
TCP is a byte stream — the OS delivers bytes in arbitrary chunks that bear no relation to application message boundaries. A single read() may return half a message, exactly one message, or three messages fused together.
The framer is the first processing phase for every byte that arrives on a proxied connection. Its job is to cut the raw stream into discrete, atomic units called frames — one frame = one complete application-level message. Everything downstream (tamper, protocol parsing, forge, replay) operates on frames.
ProtoPoke runs one framer instance per direction per session:
client ──bytes──▶ [client→server framer] ──frames──▶ tamper / parse / log
server ──bytes──▶ [server→client framer] ──frames──▶ tamper / parse / log
UDP forwarders
Framing only applies to the stream-oriented transports (TCP and SOCKS5).
UDP is already message-oriented — one datagram is one frame — so UDP
forwarders always use the raw framer and the framer selector is
disabled for them.
Choosing a Framer¶
| Protocol style | Framer | Example protocols |
|---|---|---|
| Unknown / first look | raw (default) |
Any — just observe raw bytes |
| Line-based text | delimiter with \r\n or \n |
HTTP headers, SMTP, FTP, Redis |
| Null-terminated | delimiter with \x00 |
C-string protocols |
| Binary with length header | length_prefix |
Most game/chat/custom binary protocols |
| Line-oriented with mixed endings | line |
HTTP/1.x, any \r\n or \n protocol |
| Custom boundary logic | Custom framer script | Anything else |
How to find the right framer
Capture a few frames with raw first, open them in a hex editor, and look for patterns. A 2- or 4-byte integer at the start whose value matches the remaining byte count is a length prefix. Repeated \r\n or \x00 terminations mean a delimiter framer.
Built-in Framers¶
raw (default)¶
Every read() chunk becomes one frame immediately. No buffering or boundary detection. Good for initial observation; unreliable for parsing.
delimiter¶
Accumulates bytes until a configurable byte sequence appears, then emits everything before it as one frame. The delimiter is consumed and not included in the frame.
length_prefix¶
Reads a fixed-size integer header that declares the payload length, buffers until that many bytes arrive, then emits the full prefix + payload as one frame.
# 4-byte big-endian length field
fwd = ForwarderConfig(
...,
framer_name="length_prefix",
framer_kwargs={"prefix_length": 4, "byte_order": "big"},
)
# 2-byte little-endian, length field at offset 3, add 6 to include header
fwd = ForwarderConfig(
...,
framer_name="length_prefix",
framer_kwargs={
"prefix_length": 2,
"byte_order": "little",
"prefix_offset": 3,
"length_add": 6,
},
)
Config tab → Framer: length_prefix → configure prefix length, byte order, offset, and length adjustment
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
prefix_length |
int | — | Size of the length field: 1, 2, 4, or 8 bytes |
byte_order |
str | "big" |
"big" or "little" |
prefix_offset |
int | 0 |
Byte offset where the length field starts |
length_add |
int | 0 |
Constant added to the length value (to include header bytes) |
line¶
Convenience wrapper around delimiter that splits on \r\n and also accepts bare \n.
Custom Framer¶
When none of the built-in framers fit, write a Python script with two functions. No imports from ProtoPoke are needed.
Loading a Custom Framer¶
custom_framer_path takes precedence over framer_name.
Required Functions¶
def on_data(data: bytes, state: dict, direction: str) -> list[bytes]:
"""Called when bytes arrive. Return complete frames, or [] if more data is needed."""
...
def on_flush(state: dict, direction: str) -> list[bytes]:
"""Called when the connection closes. Return any remaining buffered data."""
...
Parameters:
| Argument | Type | Description |
|---|---|---|
data |
bytes |
Raw bytes from the latest read() call |
state |
dict |
Mutable dict shared between both directions for this session; persists for the connection lifetime |
direction |
str |
"c2s" (client → server) or "s2c" (server → client) |