IpManipulator
IpManipulator is a packet tunnel for mutating raw IPv4 packets inside
WaterWall layer-3 chains.
It is one of WaterWall's main packet-side trick nodes. Use it when packet bytes
are already raw IP packets, for example behind TunDevice, RawSocket,
WireGuardDevice, or another packet tunnel. Do not place it in an ordinary
TCP byte-stream chain.
Most tricks in this node are packet-shaping and middlebox-behavior experiments. They are highly network-dependent. A trick that works on one path can be ignored, dropped, or harmful on another path.
What It Does
- Reads packet payloads on upstream and downstream packet paths.
- Applies enabled IPv4 packet tricks in place where possible.
- Can rewrite IP protocol numbers for TCP and UDP.
- Can rewrite TCP flag bits.
- Can append original TCP/UDP ports to packet tails and rewrite live ports.
- Can duplicate final outgoing packets.
- Can create crafted TLS ClientHello-related packet shapes.
- Can fragment and shuffle TLS ClientHello packets with SNI Blender.
- Updates IPv4 header and TCP checksums in place for protocol swaps and simple TCP flag changes; size-changing or payload-crafting tricks request full checksum recalculation.
IpManipulator is created with WaterWall's packet-tunnel API. Normal
connection-style Finish, Pause, Resume, and Est callbacks are not part
of its intended runtime path.
Typical Placement
Packet-chain examples:
TunDevice -> IpManipulator -> RawSocket
TunDevice -> WireGuardDevice -> IpManipulator -> UdpStatelessSocket
Paired protocol-swap deployment:
edge A packet chain -> IpManipulator -> packet transport
edge B packet chain -> IpManipulator -> packet output
When a trick needs an auxiliary branch, such as real-sni-upstream-node,
crafted-server-hello-upstream-node, or real-fin-upstream-node, configure that
branch separately from the normal next path.
Configuration Example
Simple protocol-number swap:
{
"name": "ip-manipulator",
"type": "IpManipulator",
"settings": {
"protoswap-tcp": 253,
"protoswap-udp": 252
},
"next": "next-packet-node"
}
SNI Blender example:
{
"name": "ip-manipulator",
"type": "IpManipulator",
"settings": {
"sni-blender": true,
"sni-blender-packets": 4
},
"next": "next-packet-node"
}
Mixed advanced example:
{
"name": "ip-manipulator",
"type": "IpManipulator",
"settings": {
"protoswap-tcp": 253,
"protoswap-udp": 252,
"sni-blender": true,
"sni-blender-packets": 4,
"packet-duplicate": 1,
"source-port-ghost": true,
"dest-port-ghost": true
},
"next": "next-packet-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 "IpManipulator". |
settings | object | Must enable at least one supported trick. |
next | string | Usually required for useful forwarding. Some tricks explicitly require a normal top-level next. |
Startup fails if no trick is enabled:
IpManipulator: no tricks are enabled, nothing to do
Trick Summary
| Trick family | Main settings | Direction | Purpose |
|---|---|---|---|
| Protocol swap | protoswap-tcp, protoswap-udp | both | Rewrite IPv4 protocol numbers and restore them on the opposite side. |
| SNI Blender | sni-blender, sni-blender-packets | upstream | Fragment and shuffle IPv4/TCP TLS ClientHello packets. |
| Packet duplicate | packet-duplicate | final send path | Send extra copies of the final packet before the original. |
| first-sni | first-sni | upstream | Send a crafted ClientHello copy before the original. |
| smuggle-sni | smuggle-sni, real-sni-upstream-node | upstream | Send the real ClientHello on a helper branch and a delayed crafted copy on the normal path. |
| overlap-sni | overlap-sni, crafted-server-hello-upstream-node | upstream plus downstream tracking | Overlap a generated ClientHello with captured real TLS bytes. |
| ech-sni-trick | ech-sni-trick | upstream | Send the fake ClientHello embedded by TlsClient inside ECH payload bytes as an out-of-order TCP segment. |
| synfin-sni | synfin-sni | upstream | Mix a real first TLS chunk, a close packet, fake SYN, crafted ClientHello, filler TLS bytes, and remaining real bytes. |
| smuggle-fin | smuggle-fin, real-fin-upstream-node | upstream plus downstream echo tracking | Inject a mirrored `FIN |
| TCP flag rewrite | up-tcp-bit-*, dw-tcp-bit-* | both | Force, toggle, or remap TCP flag bits. |
| Port ghost | source-port-ghost, dest-port-ghost | upstream apply, downstream restore | Carry original ports in packet tails while rewriting live transport ports. |
Protocol Swap
Protocol swap changes the IPv4 Protocol field. For example, TCP is normally
protocol number 6 and UDP is normally 17.
| Setting | Type | Description |
|---|---|---|
protoswap | integer | Legacy alias for protoswap-tcp. |
protoswap-tcp | integer | Replacement protocol number for TCP packets. |
protoswap-udp | integer | Replacement protocol number for UDP packets. |
Valid protocol replacement values are 0 through 255.
When a packet comes back with a configured replacement number, IpManipulator
restores it to normal TCP or UDP. For a symmetric tunnel, enable compatible
protocol-swap settings on the peer-side packet chain too.
Replacement values must not be the literal TCP (6) or UDP (17) protocol
numbers, even when only one family is configured, because genuine traffic using
the reused number could not be distinguished from mapped traffic. When both
families are enabled, their configured replacement values must also be distinct.
protoswap-tcp-2 has been removed and a stale configuration containing it is
rejected. Migrate to the single reversible protoswap-tcp mapping. This also
ensures every fragment of one IPv4 datagram receives the same mapped protocol
number.
Upstream tricks run while the packet still carries its real protocol number.
Immediately before normal egress, IpManipulator applies port ghost, completes
the IPv4 and transport checksums using that real protocol, swaps the protocol
number, and repairs the IPv4 header checksum. Downstream restores the protocol
before port-ghost restoration or any TCP/TLS trick parses the packet.
If an upstream packet already carries this node's configured replacement,
IpManipulator treats the node as the explicit unwrapping half of a chained
transport pair: it restores the real protocol, completes checksum work under
that protocol, and forwards the packet unmapped. To keep a packet wrapped past
a later node, do not enable the same protocol swap on that node.
TCP Flag Rewrite
TCP flag rewriting can force, toggle, or remap TCP flag bits. The older Persian documentation described this as "playing with TCP bits"; that is still the core idea, but the current implementation accepts more values.
The key format is:
up-tcp-bit-<flag>
dw-tcp-bit-<flag>
Supported flags:
cwr, ece, urg, ack, psh, rst, syn, fin
Accepted values:
off
on
toggle
flip
switch
packet->cwr
packet->ece
packet->urg
packet->ack
packet->psh
packet->rst
packet->syn
packet->fin
flip and switch are aliases for toggle.
Example:
{
"up-tcp-bit-ack": "packet->fin",
"up-tcp-bit-fin": "packet->ack",
"dw-tcp-bit-ack": "packet->fin",
"dw-tcp-bit-fin": "packet->ack"
}
Bit actions are evaluated from the original packet flags, not line by line against already-mutated flags. That means you can safely swap two bits by copying each one from the other original bit.
If any flag changes in simple mode (without preserve-tcp-bitflags), the TCP
flags byte is rewritten and the TCP checksum is updated incrementally in place
with updateIpv4TransportChecksum16(). Simple flag changes preserve any
pre-existing checksum recalculation flag without creating a new request.
Preserve TCP Bitflags
| Setting | Type | Default | Description |
|---|---|---|---|
preserve-tcp-bitflags | boolean | false | Backs up the original TCP flags byte before rewriting it, so the opposite packet direction can restore the packet later. |
This mode is useful when you want a packet to travel through one side of a path with altered TCP control bits, then return to its original TCP flag shape on the reverse direction.
Direction workflow:
- In a direction with configured TCP-bit rewrite actions,
IpManipulatorstores the original full TCP flags byte as the last byte of the IPv4 packet. - It then rewrites the live TCP flags according to
up-tcp-bit-*ordw-tcp-bit-*. - It increases the IPv4 total length by one byte and marks the packet for checksum recalculation.
- On the opposite direction, when that direction has no TCP-bit rewrite actions
but the other direction does,
IpManipulatorreads the final packet byte, restores the TCP flags field from it, removes that byte, shrinks the IPv4 total length, and marks checksums for recalculation.
The configured TCP-bit actions select which direction backs up and which direction restores. Enabling this option does not assign a fixed role to upstream or downstream:
| Configured TCP-bit actions | Upstream behavior | Downstream behavior |
|---|---|---|
| Upstream only | Back up the original flags, append one byte, and apply the upstream actions. | Restore the flags from the final byte and remove it. |
| Downstream only | Restore the flags from the final byte and remove it. | Back up the original flags, append one byte, and apply the downstream actions. |
| Upstream and downstream | Back up, append, and apply the upstream actions. | Back up, append, and apply the downstream actions. Neither direction restores. |
| Neither direction | No backup or restoration. | No backup or restoration. |
Example upstream rewrite with downstream restore:
{
"preserve-tcp-bitflags": true,
"up-tcp-bit-ack": "packet->fin",
"up-tcp-bit-fin": "packet->ack"
}
With that shape, upstream TCP packets carry their original flags in the final
packet byte before ACK/FIN are remapped. Downstream packets handled by the same
configuration have no dw-tcp-bit-* rewrite actions, so the final byte is used
to restore the original TCP flags and is then removed.
This mode only applies to whole, non-fragmented IPv4 TCP packets with complete IP and TCP headers. It assumes the extra final byte survives the path unchanged. If another node or network device strips, rewrites, pads, or interprets trailing payload bytes, restoration can fail or restore the wrong flags.
Every rewrite operation with preserve-tcp-bitflags appends one byte. Before
final egress, IpManipulator transport-segments eligible whole IPv4 TCP data
packets when that byte, port-ghost bytes, or both would exceed
GLOBAL_MTU_SIZE. Every segment gets independently derived live and saved
flags, one complete set of trailers, and continuous sequence space that
accounts for SYN consuming one sequence number. A 1500-byte TCP input can
therefore remain behind a 1500-byte WaterWall MTU without reserving operator
headroom merely for these trailers. An exact-fit buffer is grown when the final
packet is otherwise valid.
IpManipulator does not implement IPv4 fragmentation or reassembly for this
purpose. Oversized UDP, RST, unsupported URG, and other packets that cannot be
safely transport-segmented are logged with rate limiting and dropped. UDP
deployments still need operator-provided MTU headroom for port-ghost bytes.
In particular, enabling this option by itself does not append or remove bytes; at least one direction must have a TCP-bit rewrite action configured.
Stateful SNI composition rules
Only one of first-sni, smuggle-sni, overlap-sni, synfin-sni, and
ech-sni-trick may be enabled in one IpManipulator. Any one of those
stateful tricks is also rejected with same-instance sni-blender or
packet-duplicate because the state machine emits its transcript directly.
Use two nodes when both operations are needed:
... -> IpManipulator(stateful SNI only)
-> IpManipulator(sni-blender and/or packet-duplicate)
-> ...
The stateful node must come first in upstream traversal. Duplication before the
stateful node duplicates its inputs and is not equivalent to duplicating its
final output. sni-blender plus packet-duplicate remains supported when no
stateful SNI trick is enabled.
Rejected TCP-bit compositions
Configuration fails at creation for these combinations:
- any
up-tcp-bit-*action together withfirst-sni,smuggle-sni,overlap-sni,synfin-sni, orech-sni-trick, with or withoutpreserve-tcp-bitflags. Upstream TCP-bit actions run before stateful SNI inspection, while packets generated and replayed by those state machines do not consistently re-enter TCP-bit processing. preserve-tcp-bitflagstogether with at least oneup-tcp-bit-*action andsni-blender. The backup byte is appended before SNI Blender creates IPv4 fragments, but the peer restoration path skips fragments and cannot remove that byte.
These combinations remain accepted:
preserve-tcp-bitflagswith no TCP-bit actions, which is a no-op encoder- downstream-only
dw-tcp-bit-*actions with a stateful SNI trick or withsni-blender, because they do not touch the upstream flow-opening SYN or ClientHello - simple upstream TCP-bit actions with
sni-blenderwhenpreserve-tcp-bitflagsis disabled
preserve-tcp-bitflags remains unsupported with any future upstream stage that
fragments or reshapes a metadata-bearing TCP packet.
SNI Blender
SNI Blender does not rewrite the SNI string. It changes the IP packet shape of an upstream TLS ClientHello.
| Setting | Type | Required | Description |
|---|---|---|---|
sni-blender | boolean | yes | Enables TLS ClientHello fragmentation and shuffle. |
sni-blender-packets | integer | yes when enabled | Number of fragments. Valid range: 2 through 16. |
Behavior:
- detects upstream IPv4/TCP packets carrying a TLS ClientHello
- skips packets that are already fragmented
- preserves the TCP header in the first fragment
- splits the IP payload into multiple IPv4 fragments
- rounds fragment offsets to 8-byte boundaries as required by IPv4
- sends the fragments in shuffled order
Practical notes from the older Persian docs still apply:
- The traffic must contain a visible TLS ClientHello at the packet layer.
- If the user's existing application already sends TLS, WaterWall does not need to create TLS only for this trick.
- If the chain itself must produce TLS, put
TlsClient/TlsServerin the appropriate stream path before packets reach this node. - In local or "serverless" experiments, the operating system routing/capture setup must make the relevant packets pass through WaterWall.
This trick is network-dependent. Some paths drop fragmented ClientHello packets; others may pass them.
Packet Duplicate
| Setting | Type | Description |
|---|---|---|
packet-duplicate | integer greater than 0 | Sends this many duplicate packets, then sends the original packet once. |
Duplication happens in the final send path after the other enabled tricks have finished shaping the packet.
Port Ghost
Port ghost applies on upstream final send and restores on downstream receive.
| Setting | Type | Description |
|---|---|---|
source-port-ghost | boolean | Append the original source port to the end of whole IPv4 TCP/UDP packets, then rewrite the live source port to a deterministic pseudo-random high port. |
dest-port-ghost | boolean | Append the original destination port to the end of whole IPv4 TCP/UDP packets, then rewrite the live destination port to a deterministic pseudo-random high port. |
If both are enabled, the source port bytes are appended first and the destination port bytes second.
Downstream packets that still carry those tail bytes restore the original live ports and shrink the packet back to its original length. Fragmented IPv4 packets are skipped.
Ordinary and crafted whole TCP data packets share one final egress segmenter.
Each emitted segment receives exactly one port-ghost trailer and, when
configured, one independently restorable saved-flags byte. SYN/CWR appear
only on the first segment, FIN/PSH only on the final segment, and
ACK/applicable ECE on every segment. Every completed segment stays within
GLOBAL_MTU_SIZE, then final packet duplication is applied to that segment.
Oversized UDP cannot use TCP segmentation and is rate-limited-log-and-drop. IpManipulator does not introduce IPv4 fragmentation/reassembly, so UDP port ghost still requires operator-provided MTU headroom.
Dedicated real-SNI, mirrored-FIN, and overlap-SNI server-hello helper branches preserve their original tuples and do not receive a port-ghost trailer.
first-sni
first-sni sends one or more crafted ClientHello copies before the original
ClientHello.
| Setting | Type | Default | Description |
|---|---|---|---|
first-sni | non-empty string | required to enable | SNI value written into the crafted ClientHello copy. |
first-sni-count | integer greater than 0 | 1 | Number of crafted copies to send before the original. |
first-sni-replay-delay | integer ms, >= 0 | 0 | Delay between crafted replays after the first one. |
first-sni-final-delay | integer ms, >= 0 | 0 | Delay between the last crafted copy and the original packet. |
first-sni-ttl | integer 0 through 255 | preserve original | IPv4 TTL for the crafted copy. |
first-sni-random-tcp-sequence | boolean | false | Randomize the crafted copy's TCP sequence number. |
Only the crafted copy is modified; the original ClientHello remains on the normal path. If the ClientHello has TLS 1.3 PSK binders and changing the SNI would require recomputing binders, the trick skips the crafted copy and leaves the original path alone.
Multi-segment capture begins only when a packet contains a recognizable ClientHello first segment. Unrelated and non-TLS TCP payloads fail open immediately without entering a speculative prestart queue. Recognized, in-order ClientHellos can still span multiple TCP segments.
IPv4 fragments, including an MF=1 first fragment and any nonzero-offset later
fragment, never enter First-SNI capture and continue on the normal fail-open
path. When replay/final output is delayed, the transcript tail is inserted into
a bounded FIFO before later packets and released at one absolute deadline.
smuggle-sni
smuggle-sni captures and reassembles a TLS ClientHello record across single or multi-segment TCP payloads (up to 16 segments / 16 KB) on the 4-tuple, sends the real ClientHello through a helper upstream branch immediately in order, and sends a generated multi-segment fake ClientHello batch through the normal next path after smuggle-sni-delay-ms. The fake batch strictly preserves original TCP segment boundaries, original TCP payload lengths, original TCP sequence numbers, and any trailing payload bytes beyond the ClientHello record. If length mismatch, fragment timeout/discontinuity, or generation failure occurs, smuggle-sni fails open immediately to the normal path.
| Setting | Type | Default | Description |
|---|---|---|---|
smuggle-sni | non-empty string | required to enable | SNI value written into the crafted ClientHello copy. |
smuggle-sni-delay-ms | integer ms, >= 0 | 0 | Delay before sending the crafted copy batch on the normal path. |
real-sni-upstream-node | string | required | Node name that receives the real captured ClientHello immediately. |
This trick requires a normal top-level next. The helper node must exist and
must not point back to IpManipulator itself. In practice, make the helper a
separate branch head rather than the same node as the normal next.
IpManipulator creates an internal no-chain TlsClient helper to generate the
crafted ClientHello shape for this trick.
Later packets inside its delay window enter the same bounded FIFO and use the
flow's absolute deadline. The fake transcript is scheduled before that FIFO;
FIN and RST stay behind already queued data.
overlap-sni
overlap-sni captures early TLS packets, builds a generated Chrome-like
ClientHello, sends a crafted server-side TLS packet through a helper branch, and
overlaps generated and real bytes on the TCP sequence range.
| Setting | Type | Default | Description |
|---|---|---|---|
overlap-sni | non-empty string | required to enable | SNI value for the generated ClientHello. |
overlap-sni-delay-ms | integer ms, >= 0 | 0 | Delay for packets after the fake TCP SYN and later upstream packets while the overlap window is active. |
overlap-sni-syn-ttl | integer 0 through 255 | preserve original | IPv4 TTL for the fake TCP SYN. |
crafted-server-hello-upstream-node | string | required | Helper branch that receives the crafted server-side TLS packet. |
This trick requires a normal top-level next. The helper branch must exist and
must not point back to IpManipulator.
Later upstream packets use the flow's absolute overlap-window deadline and a
bounded FIFO instead of receiving a fresh copy of overlap-sni-delay-ms. The
crafted overlap tail is scheduled before that FIFO can release.
ech-sni-trick
ech-sni-trick is designed to work with TlsClient.settings.ech-sni-trick.
TlsClient embeds a fake ClientHello inside GREASE
encrypted_client_hello payload bytes. IpManipulator then performs the
packet-side out-of-order send while keeping the original ClientHello bytes
unchanged.
| Setting | Type | Default | Description |
|---|---|---|---|
ech-sni-trick | string, 1 through 255 bytes | required to enable | Must match the related TlsClient.settings.ech-sni-trick value for the same flow. |
data-shard-1-delay | integer ms, >= 0 | 0 | Delay before releasing the first original held packet after sending the fake inner ClientHello segment. |
data-shard-2-delay | integer ms, >= 0 | 0 | Additional delay before releasing original packets 2 through N consecutively in their original sequence order. A one-packet capture ignores this setting. |
The combined shard delay must fit in the supported unsigned 32-bit delay range.
The out-of-order fake-inner packet is sent with PSH set. If the crafted
out-of-order packet would exceed GLOBAL_MTU_SIZE, the flow is rejected instead
of reshaped.
Capture is sequence-aware and does not depend on packet ordinals. A payload-free
TCP SYN, with or without ECN ECE/CWR, opens a flow. TCP Fast Open SYN data
is unsupported and passes through. Capture starts only when upstream payload
contains a validated complete or partial TLS ClientHello prefix; unrelated and
non-TLS payloads are never placed in a speculative prestart queue. Contiguous
bytes continue validation through the five-byte TLS record header and four-byte
handshake header until the exact record length is complete. Payload-free
packets, including ACK-only packets before or during capture, pass unchanged and
do not advance or disable capture.
Capture is bounded to 16 packets, a 16384-byte TLS record, and a 1500 ms timeout. A sequence gap, malformed or unsupported record, packet or size limit, timeout, or overlap that cannot be proven to be an exact retransmission fails open: held originals are released unchanged in TCP sequence order and that flow generation becomes passthrough. During incomplete capture, an exact retransmission matching an already-held segment's sequence, length, and bytes is consumed without disabling capture; partial and ambiguous overlaps fail open.
After capture, IpManipulator requires a valid outer SNI, a valid
encrypted_client_hello extension, and exactly one embedded inner TLS
ClientHello whose SNI parses completely and matches ech-sni-trick byte for
byte. Missing, malformed, truncated, mismatched, or ambiguous candidates fail
open without emitting a fake inner packet. This mechanism remains GREASE
camouflage rather than real encrypted ECH.
On success, the original ClientHello bytes, packet boundaries, and TCP sequence
numbers remain unchanged. IpManipulator first sends one out-of-order TCP
packet containing only the matching embedded ClientHello bytes. It then:
- releases original packet 1 after
data-shard-1-delay; - waits the additional
data-shard-2-delay; and - releases original packets 2 through N consecutively in their original order.
A one-packet capture uses only the first delay. A two-packet capture preserves the original two-shard timing, while a larger capture does not create multiple equal-deadline timer messages that could reorder packets.
During delayed release, only an exact retransmission of an original segment that
is still pending is swallowed. ACK-only traffic, non-overlapping later payload,
and partial or ambiguous overlaps pass unchanged. TCP FIN and RST remain
connection-lifecycle traffic and are never swallowed. A matching close in either
direction cancels every original packet still awaiting release; an upstream
close during incomplete capture first releases held originals, while a
downstream close disposes of the incomplete capture without later injection.
Each delayed original is bound to the nonzero generation of the ECH flow that captured it as well as its 4-tuple. Reusing the same 4-tuple starts a new generation and first invalidates the old generation's incomplete capture and pending originals, so stale capture or release timers cannot inject old ClientHello data. While originals are pending, the flow remains protected from ordinary idle expiry; normal idle expiry resumes after the final release.
synfin-sni
synfin-sni is an advanced upstream TLS ClientHello trick. It sends a real
first TLS data chunk, then a close packet, a fake SYN, a crafted fake
ClientHello, generated TLS-looking filler bytes, and finally the remaining real
ClientHello bytes.
| Setting | Type | Default | Description |
|---|---|---|---|
synfin-sni | non-empty string | required to enable | SNI value for the generated ClientHello. |
synfin-sni-additional-range-min | integer 0 through 65535 | 0 | Minimum extra real ClientHello bytes added to the first real chunk. |
synfin-sni-additional-range-max | integer 0 through 65535 | same as min when min is set alone, otherwise 0 | Maximum extra real bytes; one random value is chosen per flow and clamped before the real SNI bytes. |
synfin-sni-syn-ttl | integer 0 through 255 | preserve original | TTL for the fake SYN. |
synfin-sni-fin-ttl | integer 0 through 255 | preserve original | TTL for the close packet. |
synfin-sni-fake-ttl | integer 0 through 255 | preserve original | TTL for the full crafted fake ClientHello packet. |
synfin-sni-random-syn-checksum | boolean | false | Randomize IPv4/TCP checksums on the fake SYN. |
synfin-sni-random-fin-checksum | boolean | false | Randomize IPv4/TCP checksums on the close packet. |
synfin-sni-random-syn-sequence | boolean | false | Randomize the fake SYN sequence number. |
synfin-sni-random-fin-sequence | boolean | false | Randomize the close packet sequence number. |
synfin-sni-use-rst | boolean | false | Send `RST |
TTL value 0 is a real TTL override. Omit the TTL field entirely if you want to
preserve the captured packet TTL.
The crafted sequence is emitted synchronously in the documented order with no artificial per-packet 20 ms sleep or configurable pacing delay.
smuggle-fin
smuggle-fin injects a mirrored FIN/ACK through a helper branch and temporarily
queues traffic on the flow-owner worker until the expected downstream echo is
seen and the optional delay expires. Matching reverse packets that arrive on
another worker are copied to the owner worker before they enter the same ordered
queue. Each queued packet retains its own checksum-recalculation intent.
| Setting | Type | Default | Description |
|---|---|---|---|
smuggle-fin | boolean | required to enable | Enables mirrored FIN/ACK injection. |
fin-sni-delay-ms | integer ms, >= 0 | 0 | Delay after the expected downstream echoed FIN/ACK before queued packets are replayed. |
real-fin-upstream-node | string | required | Helper branch that receives the crafted mirrored FIN/ACK. |
This trick requires a normal top-level next. The helper node must exist and
must not point back to IpManipulator.
Packets without TCP payload, packets that are already SYN, FIN, or RST,
non-TCP packets, and fragmented IPv4 packets are left alone.
Stateful Flow Tables
| Setting | Type | Default | Description |
|---|---|---|---|
stateful-flow-limit | integer, 32 through 1048576 | 65536 | Hard upper bound on the active flows that each enabled stateful trick may track. |
first-sni, smuggle-sni, overlap-sni, synfin-sni, ech-sni-trick, and
smuggle-fin each receive their own bounded table with this limit. The tables
never grow beyond it. When a shard is full, IpManipulator reclaims a small,
fixed budget of entries whose idle deadlines have passed. If that does not free
a slot, admission fails open, the packet continues unchanged, and a
rate-limited warning is logged. A live flow is never evicted to admit another
flow, which protects ECH and Smuggle-FIN records that own held packet buffers.
Every table uses one normalized four-tuple as its canonical key, so forward and reverse packets resolve to the same record, hash, shard, and lock, including across workers. A valid replacement SYN explicitly cancels the previous generation's timers, captures, and queues as applicable before reinitializing that sole record in the new orientation. Duplicate canonical entries are not admitted.
The shard count follows the worker count, capped at 64, and the configured limit is partitioned so shard limits sum to the requested total. Lookup uses hash buckets and idle expiry uses a per-shard deadline heap, avoiding whole-table scans in packet callbacks. Flow-entry pointers remain valid only while their shard mutex is held; timers and cross-worker messages therefore identify flows by normalized tuple and generation.
First-SNI, Smuggle-SNI, and Overlap-SNI delay barriers retain at most 16 packets and 256 KiB per flow. One tunnel-wide generation binds the release action to the exact tuple instance, so a stale timer cannot release a reused tuple. Batches are detached under the shard lock and forwarded outside it. Count/byte exhaustion fails open in FIFO order, and reset, timeout, or table destruction recycles every retained packet.
Mutual Exclusion
The parser rejects every pair within the five-member stateful SNI set:
first-sni, smuggle-sni, overlap-sni, synfin-sni, and
ech-sni-trick. It also rejects any of them with same-instance sni-blender
or packet-duplicate. See the supported two-node chain under "Stateful SNI
composition rules" above.
Runtime Pipeline
Upstream payload order:
smuggle-fin- TCP flag rewrite
synfin-sniech-sni-trickoverlap-snismuggle-snifirst-sni- SNI Blender
- final egress: port ghost, checksum under the real protocol, protocol swap, then packet forwarding/duplication
Downstream payload order:
- protocol restore
- port-ghost restore
smuggle-finech-sni-trickoverlap-sni- TCP flag rewrite
- SNI Blender downstream hook
smuggle-snidownstream logging hook- final downstream send
For internal smuggle-fin replay, upstream packets restart at the upstream
entry because smuggle-fin is the first stage. Downstream packets resume
immediately after smuggle-fin; their protocol and port-ghost restoration
already ran before queuing and are not repeated. This does not change the normal
downstream order above.
Lifecycle and Packet-Line Semantics
IpManipulator is a pure packet tunnel:
- its node metadata advertises
kNodeLayer3, while both neighbor constraints remainkNodeLayerAnything - it has no per-packet connection lifecycle
- packet payload callbacks are the intended runtime path
Finish,Pause,Resume, andEstcallbacks are invalid for normal use- it must not destroy packet lines during runtime
On upstream packet-line Init, the normal next path is initialized. Helper
branches for real-SNI, real-FIN, and overlap-SNI crafted server hellos are
initialized too when configured. Aliased helper branches receive Init only
once.
Limits and Padding
| Value | Size |
|---|---|
required_padding_left | 0 bytes |
| SNI Blender fragment count | 2 through 16 |
| protocol replacement number | 0 through 255 |
stateful-flow-limit | 32 through 1048576 |
| ECH hostname length | 1 through 255 bytes |
| ECH capture | up to 16 packets and a 16384-byte TLS record |
IpManipulator does not require chain left padding. Some tricks allocate or
clone packet buffers internally when crafting replacement packets.
Notes and Caveats
- The current implementation modifies IPv4 packets. IPv6 packets are generally ignored by the tricks described here.
- Many tricks require whole, non-fragmented IPv4 TCP packets with a complete TCP header.
- Most SNI-family tricks are upstream-only and operate on TLS ClientHello packets.
- The node relies on later packet output code honoring
line->recalculate_checksum. - Helper branch nodes should be separate branches, not the same node as the
normal
next. sni-blender-packetsis required whensni-blenderis enabled and must be between2and16.trick_sni_blender_packets_delay_maxexists in the C state struct, but the current JSON parser does not expose or use it.