ObfuscatorClient
ObfuscatorClient applies a reversible payload transform before traffic moves
toward the transport side. In the current implementation, the only supported
method is XOR.
It is intended to be paired with ObfuscatorServer configured with the same
method, key, and header options. This is a lightweight traffic-shaping layer,
not encryption. For confidentiality or authentication, use EncryptionClient /
EncryptionServer or TLS.
The option-off path remains stateless. TLS-like framing uses normal-line receive state for stream reassembly, while exact packet lines remain stateless.
Typical Placement
Simple TCP path:
client side: TcpListener -> ObfuscatorClient -> TcpConnector
server side: TcpListener -> ObfuscatorServer -> TcpConnector
UDP path:
client side: UdpListener -> ObfuscatorClient -> UdpConnector
server side: UdpListener -> ObfuscatorServer -> UdpConnector
Raw packet path:
TunDevice -> ObfuscatorClient -> packet transport
packet transport -> ObfuscatorServer -> RawSocket
Use skip: "ipv4" or skip: "transport" when the payload is a raw IPv4
packet and headers must stay readable for routing, injection, or packet output.
What It Does
- Requires
settings.method: "xor". - Requires
settings.xor_key. - Applies XOR to upstream payload bytes before forwarding to
next. - Optionally prepends a 5-byte TLS-like record header after upstream XOR.
- Optionally strips the 5-byte TLS-like header before downstream XOR.
- Applies XOR to downstream payload bytes before forwarding to the previous node.
- Can leave the IPv4 header, or IPv4 plus TCP/UDP header, unobfuscated.
- Marks packet-line payloads for checksum recalculation after changing bytes.
- Keeps bounded receive state only for ordinary streams with TLS-like framing enabled.
Because XOR is symmetric, applying the same transform twice with the same key recovers the original payload.
Configuration Example
{
"name": "obfuscator-client",
"type": "ObfuscatorClient",
"settings": {
"method": "xor",
"xor_key": 90,
"skip": "transport",
"tls_record_header": true
},
"next": "transport-node"
}
Matching server:
{
"name": "obfuscator-server",
"type": "ObfuscatorServer",
"settings": {
"method": "xor",
"xor_key": 90,
"skip": "transport",
"tls_record_header": true
},
"next": "service-node"
}
Required Fields
Top-level fields:
| Field | Type | Description |
|---|---|---|
name | string | User-chosen node name. Must be unique inside the config file. |
type | string | Must be exactly "ObfuscatorClient". |
settings | object | Required. Must contain method and xor_key. |
next | string | Required in normal use. The next node receives obfuscated upstream payloads. |
Required settings fields:
| Field | Type | Description |
|---|---|---|
method | string | Must be "xor" in the current implementation. |
xor_key | integer | XOR key. The value is stored as one byte, so values outside 0..255 are converted to uint8_t. |
Optional Settings
| Setting | Type | Default | Description |
|---|---|---|---|
skip | string | "none" | Leading packet headers to leave clear before XOR. Accepted values are "none", "ipv4", and "transport". |
tls_record_header | boolean | false | Adds/removes a 5-byte TLS-like application-data record header on the transport-facing payload. |
tls_header | boolean | unset | Deprecated compatibility alias for tls_record_header. If present, it is accepted and a warning is logged. |
Skip Modes
skip controls how many leading bytes are not XORed:
| Value | Behavior |
|---|---|
"none" | XOR the whole payload. |
"ipv4" | If the payload starts with a valid IPv4 packet, leave the IPv4 header clear and XOR bytes after it. |
"transport" | If the payload starts with a valid IPv4 TCP/UDP packet, leave the IPv4 header and TCP/UDP header clear. For other IPv4 protocols, leave only the IPv4 header clear. |
If the payload is too short or not a valid IPv4 packet, the skip length becomes
0 and the whole payload is XORed.
For malformed or incomplete TCP/UDP headers under skip: "transport", the
implementation falls back to leaving only the IPv4 header clear.
TLS-Like Record Header
When tls_record_header is enabled, upstream transport-facing payloads are sent
as:
17 03 03 + length(2 bytes, big-endian) + obfuscated payload
The bytes imitate a TLS application-data record header:
| Field | Value |
|---|---|
| content type | 23 / 0x17 |
| legacy version | 0x0303 |
| length | obfuscated payload length, 16-bit big-endian |
On the downstream receive path, the header is validated and stripped before XOR runs.
This is not TLS. It has no handshake, no certificates, no SNI, no ALPN, no encryption, and no authentication. It only imitates the shape of a post-handshake TLS application-data record.
Stream framing behavior:
- Split large deliveries into records of at most 65,535 body bytes and apply XOR/skip per record.
- Reassemble fragmented headers/bodies and parse coalesced records, retaining partial tails.
- Send one encoded or decoded aggregate per batch, preserving FIFO without waiting for Resume.
- Forward every Pause/Resume in both directions, including repeats; retain only incomplete records.
- Bound retained input by a fixed 2,162,692 bytes (
2 MiB + 65,540), independent of pool sizes, RAM profile and splice settings. The 65,535-byte record-body limit is separate. - Close malformed or overflowing ordinary streams and release state on Finish.
Exact packet lines still require one complete record per packet. Truncated, mismatched, malformed, or oversized datagrams retain the existing drop policy; this mode adds no datagram fragmentation or cross-packet reassembly.
Direction Behavior
| Incoming callback | Behavior |
|---|---|
upstream Payload | XORs according to skip, optionally prepends the TLS-like header, marks packet-line checksum recalculation, then forwards to next. |
downstream Payload | Optionally validates and strips the TLS-like header, XORs according to skip, marks packet-line checksum recalculation, then forwards to the previous node. |
Init in either direction | Initializes normal-line receive state when framing is enabled, then forwards in the same direction. |
Finish in either direction | Releases normal-line receive state, then forwards away from the sender. |
Est, Pause, Resume | Forwards transparently in the same direction, including repeats. |
Exact packet lines carry no receive state. For framed normal lines, protocol or admission failure destroys local state, finishes the opposite side, then the source side recorded during Init. The nodes remain active for XOR and record processing on every payload; they do not permanently bypass themselves.
Buffer And Padding Notes
Node metadata advertises:
required_padding_left = 5
layer_group = kNodeLayerAnything
The 5 bytes are for the optional TLS-like header. If a buffer does not have
enough left padding when tls_record_header is enabled, the node clones the
payload into a new buffer with enough padding, recycles the original buffer, and
then prepends the header.
When the current line is the worker packet line, the node sets
sbufSetRecalculateChecksum(buf, true) on the surviving buffer after transforming payload bytes.
Implementation Notes
The XOR helper has optimized paths:
- AVX2 when available
- aligned 64-bit chunk processing on 64-bit builds
- aligned 32-bit or byte-wise fallback otherwise
These paths change performance only; visible behavior is still byte-wise XOR with the configured one-byte key.
Practical Notes
ObfuscatorClientandObfuscatorServermust use the samemethod,xor_key,skip, andtls_record_headersettings.- This is obfuscation, not cryptographic security.
skip: "none"is simplest for ordinary byte streams.skip: "transport"is useful for raw IPv4 TCP/UDP packets whose IP and transport headers need to remain readable.- If you obfuscate raw packet headers, downstream packet output may no longer be routable even though WaterWall marks checksums for recalculation.
Node Metadata
Source-backed metadata:
This node explicitly disables splice for the entire chain through
kNodeFlagBlocksSplice, even if kNodeFlagSupportsSplice is also set.
| Property | Value |
|---|---|
| node flags | kNodeFlagBlocksSplice |
can_have_prev | true |
can_have_next | true |
layer_group | kNodeLayerAnything |
layer_group_prev_node | kNodeLayerSameAsNext |
layer_group_next_node | kNodeLayerSameAsPrev |
required_padding_left | 5 bytes |