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.
Instance MTU
Optional integer mtu defaults to core misc.mtu at construction (core fallback
1500). Its range is 68..65535 bytes: complete IPv4 packet length, including applicable trailers, used by its existing egress and trick paths.
Each instance stores its validated value immutably; other instances and later core
changes do not change its behavior. Null, wrong types, fractions, zero, negative
or out-of-range values reject construction. An unsupported inherited default also
fails; supply a supported node override. This is not measured path MTU or MTU negotiation.
A valid enabled trick is still required. This setting is not a universal ingress
filter and does not add generic UDP fragmentation.
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 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 decoy ClientHello copies with a replaced host name before the original. |
| smuggle-sni | smuggle-sni, real-sni-upstream-node | upstream | Send the real ClientHello on a helper branch, an equal-length generated ClientHello on the normal path, and hold follow-on traffic behind an ordering deadline. |
| overlap-sni | overlap-sni | upstream plus downstream close cleanup | Rewrite one sequence range twice, separated by a fake SYN. |
| 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, optional random/TLS-looking filler, 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.
With multiple workers, decoding through the normal packet path selects a worker from the restored TCP/UDP flow tuple after the applicable port transform. Downstream stateful tricks run after this handoff; upstream decoding resumes at final forwarding so transforms are not repeated. Protocol encoding itself adds no worker hop. Preserved fragments follow the restored first fragment's transport tuple; tails arriving first wait in bounded shared fragment storage. One-worker delivery is direct. Same-worker delivery can continue directly when its destination queue has no older accepted work; otherwise it joins that queue. This preserves queue admission order, not a global arrival order across readers.
ProtoSwap normalizes the TCP/UDP checksum's source and destination address
contributions to zero before mapping, then restores the current addresses'
contributions when decoding. The actual IP addresses stay intact. The mapped
checksum still uses the native TCP (6) or UDP (17) pseudo-header protocol,
not the mapped number. Address rewrites between endpoints therefore preserve
valid checksums after decoding, without payload scans, packet expansion, or
per-flow state. Both endpoints must use this checksum convention.
Without an existing recalculation request, the incremental adjustment preserves
invalid checksum residuals and detection of payload corruption. Disabled IPv4
UDP checksum zero stays zero; enabled UDP arithmetic zero is transmitted as
0xffff. TCP zero is an ordinary checksum value.
Every fragment receives the protocol mapping. Only the fragment containing both transport checksum bytes receives the address adjustment, including a later TCP fragment after an 8- or 16-byte first fragment. Fragmentation may occur before encoding or between endpoints; checksum conversion itself needs no reassembly or ordering state. Decoded worker selection uses the fragment association described above. Fragments of one datagram must receive consistent final addresses. Malformed IPv4/transport lengths, partial checksum fields and invalid local fragment geometry pass through unchanged, with their pending request preserved. Trailing buffer bytes are excluded from the transform; final writers still validate the packet length.
Native shaping and port ghost run before upstream normalization. An existing checksum request is completed under the native protocol and actual addresses before encoding, in either callback direction; a failed request prevents mapping. For fragments, that request repairs only the IPv4 header. Decoding restores the address contribution and native protocol before downstream port restoration or TCP/TLS parsing. Existing downstream checksum requests are handed onward; upstream decoding can complete them immediately. ProtoSwap creates no new recalculation request. Arbitrary payload edits while mapped require a transform that understands this checksum convention.
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 any requested 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
mtu. 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 from 1 through 16 | 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
mtu, 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 and mirrored-FIN helper branches preserve their original tuples and do not receive a port-ghost trailer.
first-sni
first-sni puts one or more decoy copies of the client's own TLS ClientHello,
with only the host name replaced, on the wire ahead of the real ClientHello of
the same TCP connection. It is upstream-only and never touches downstream
traffic.
Many inspection systems classify a TLS connection from the first ClientHello
they observe on a 4-tuple and keep that verdict for the rest of the connection.
first-sni targets that behavior: a device that latches onto the first
ClientHello records the decoy name, while the destination server is expected to
receive only the real one. This is the classic "fake packet" desynchronization
idea applied to the SNI field. The real ClientHello itself is never rewritten,
fragmented, split or reordered; it still travels intact, exactly once, at its
real sequence number.
| Setting | Type | Default | Description |
|---|---|---|---|
first-sni | non-empty string, 1 through 65535 bytes | required to enable | Host name written into the decoy ClientHello copies. Only the first host_name entry of the copy is rewritten. |
first-sni-count | integer from 1 through 16 | 1 | Number of decoy copies sent before the original. |
first-sni-replay-delay | integer ms, >= 0 | 0 | Spacing between decoy copies after the first one. Only meaningful when the count is greater than 1. |
first-sni-final-delay | integer ms, >= 0 | 0 | Extra delay between the last decoy copy and the original ClientHello. |
first-sni-ttl | integer 0 through 255 | preserve original | IPv4 TTL for the decoy copies. 0 is a real override; omit the field to keep the original TTL. |
first-sni-random-tcp-sequence | boolean | false | Give each decoy copy a fresh random 32-bit TCP sequence number. |
Startup fails when (first-sni-count - 1) * first-sni-replay-delay, or that
value plus first-sni-final-delay, exceeds the supported unsigned 32-bit
millisecond range.
What goes on the wire
With the default count of 1 and both delays at 0, everything happens inside
one upstream packet callback:
- The TCP handshake is untouched.
SYN,SYN|ACKand the firstACKpass through unchanged. - The client sends its ClientHello in one segment: sequence
S, payload lengthL, normallyPSH|ACK. IpManipulatorparses that payload. It requires a complete TLShandshake (0x16)record whose ClientHello message exactly fills the record body and contains aserver_nameextension with at least onehost_nameentry. A payload that only begins such a record enters the multi-segment capture described below; anything else fails open.- The packet is cloned and only the first
host_namestring is replaced. Six length fields are corrected by the same delta:host_namelength, server-name-list length,server_nameextension length, extensions length, ClientHello handshake length and TLS record length, plus the IPv4 total length. - Everything else in the copy is the original's bytes: the same IPv4
identification, flags and TTL, the same TCP ports, sequence number,
acknowledgement number, window, flags and options, and the same TLS version,
random, session id, cipher list and remaining extensions. first-sni-ttlthen overwrites the copy's TTL if configured, andfirst-sni-random-tcp-sequenceoverwrites its TCP sequence number if enabled.- The decoy is sent first with recomputed IPv4 and TCP checksums. There is no bad-checksum option in this trick; both packets always leave with valid checksums.
- The original ClientHello is forwarded immediately afterwards, unchanged, also with recomputed checksums.
Sequence numbers and keeping the decoy off the server
Unless first-sni-random-tcp-sequence is enabled, the decoy carries the real
ClientHello's sequence number S, so both segments claim the same position in
the TCP byte stream. A configured name that is d bytes longer makes the decoy
cover S to S + L + d against the real packet's S to S + L; a shorter name
covers less. The two segments overlap, and each receiver keeps whichever copy
its overlap handling accepts.
That overlap is the point for an inspecting device and the problem for the server. A decoy that arrives as valid in-window data is delivered to the TLS server, and the real segment that follows is then treated as a duplicate, so the server answers the decoy host name. Because the client's TLS stack computes its transcript over its own ClientHello, such a connection normally fails with a certificate or handshake error instead of carrying traffic.
Configure at least one of the two mechanisms that keep the decoy away from the destination:
first-sni-ttllow enough that a router between the inspection point and the server discards the decoy. The value is path dependent and has to be measured, for example with a traceroute; the discarding router will normally return ICMP time-exceeded toward the client address. Too low and the inspector never sees the decoy, too high and the server does.first-sni-random-tcp-sequence, which places the decoy far outside the server's receive window. A conforming TCP stack discards out-of-window data and answers at most with a duplicate or challenge ACK, while an inspector that reassembles by direction without validating sequence numbers still parses the decoy as this connection's ClientHello. It has no effect on inspectors that do track sequence numbers and windows.
Both can be combined. Leaving both unset is only sensible when the configured value equals the real host name, which turns the copy into a plain duplicate segment and removes the decoy effect.
What an intermediate device reconstructs depends on its own overlap handling: a per-packet inspector sees two ClientHellos and normally acts on the first, a reassembling inspector that keeps first-arrived bytes for a sequence range also ends up with the decoy, and one that validates windows or TTLs, or prefers later data for an overlapping range, sees the real name. No standard guarantees any of these outcomes, so the result is path specific.
Repeats and delays
- Copy 1 always leaves synchronously, ahead of everything else.
- With
first-sni-replay-delayat0, allfirst-sni-countcopies are crafted and sent back to back in the same callback with no pacing between them. - With a nonzero replay delay, copies 2 to N are scheduled at
1 x delay,2 x delay, ...(N-1) x delayafter copy 1 and are all crafted in advance, so each pending copy holds a full packet buffer until its send time. Keep the count modest. - The original ClientHello is released after
(count - 1) * replay-delay + final-delaymilliseconds, that is, immediately after the last decoy whenfirst-sni-final-delayis0. - While that window is open, later upstream packets of the same 4-tuple, ACKs
and
FIN/RSTincluded, are queued behind the held original in a per-flow FIFO of at most 16 packets and 256 KiB, so nothing overtakes the delayed ClientHello. Downstream packets are never delayed. Delaying the flow also delays the client's own ACKs, so keep the totals in the tens of milliseconds. - FIFO overflow, a packet of the flow arriving on another worker, and a timer that cannot be armed all fail open in order: queued packets are flushed first, then the current packet.
- A payload-free opening
SYNon the same 4-tuple resets the flow record, so a reused tuple starts a fresh, delay-free flow. Anything the previous generation still held is discarded rather than forwarded. - With both delays at
0no timer is armed and no delayed state is created; the trick is then purely synchronous. - The per-flow record is bounded by
stateful-flow-limitand expires after 20 minutes of inactivity. If no record can be admitted, the schedule collapses to an immediate send, with a warning, still in the documented order.
Multi-segment ClientHello
Capture begins only on a segment whose payload actually begins a recognizable ClientHello record, so unrelated and non-TLS TCP payloads fail open immediately without entering a speculative prestart queue. Strictly contiguous following segments are appended until the TLS record is complete, bounded by 16 packets, a 16384-byte record and a 1500 ms timeout since the last accepted segment.
On completion the trick works on one assembled packet: the first segment's IPv4
and TCP headers followed by all captured payload bytes. The original segments are
dropped rather than forwarded. Two effects are visible on the wire: the
destination receives the real ClientHello as one larger segment starting at the
first segment's sequence number instead of the client's original segment pattern,
and that coalesced packet carries the first segment's TCP flags, window and
acknowledgement number. Byte content and sequence continuity are preserved. An
assembled packet, or a decoy built from it, that exceeds mtu is
transport-segmented again at final egress with continuous sequence numbers.
A sequence gap, an out-of-order or retransmitted segment, exceeding the packet or record limits, the capture timeout, or a same-flow packet arriving on another worker releases the held segments unchanged and in order.
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 first-sni does nothing
The trick fails open, forwarding traffic unchanged, when:
- the packet is not IPv4, not TCP, or is an IPv4 fragment
- the payload is not, and does not begin, a TLS ClientHello record, the ClientHello spans more than one TLS record, or its handshake length does not match the record body
- the ClientHello has no
server_nameextension or nohost_nameentry - the ClientHello carries a TLS 1.3
pre_shared_keyextension with PSK binders and the configured value would actually change the SNI bytes.IpManipulatordoes not own the PSK secret needed to recompute valid binders, so it skips the copy and leaves the original path alone - the substitution would overflow a 16-bit TLS length field, the 24-bit handshake length or the IPv4 total length, or would produce a non-positive length. The reason is logged and the original packet is forwarded
- no packet buffer is available for the copy
Capture slots are shared per tunnel, 16 per worker. When all of them are busy, the oldest capture is abandoned and its held segments are released unchanged.
How first-sni differs from the other SNI tricks
sni-blenderfragments the real ClientHello at the IPv4 layer and invents no host name.first-sniadds a whole extra packet and leaves the real one intact.smuggle-snisends the real ClientHello out through a separate helper branch and puts the crafted copy on the normal path.first-snisends both on the normal path, decoy first.overlap-snibuilds a multi-packet transcript with a fakeSYNand two versions of one sequence range.synfin-sniadditionally uses a syntheticFIN/RSTand TLS-looking filler.first-sniemits no synthetic control packets and never splits the real ClientHello.ech-sni-trickreorders bytes that already exist inside the real ClientHello, whose decoy hello was embedded earlier byTlsClient;IpManipulatorwrites no host name of its own there.
Limitations and side effects
- Apart from the host name and its length fields, the decoy is byte identical to
the real ClientHello, including the IPv4 identification and the TLS
randomand session id, so a device that correlates ClientHello contents can recognize the pair. - No padding extension is adjusted, so a configured name of a different length produces a decoy of a different size than the real packet.
- Each configured copy re-sends the full ClientHello payload, and a long configured name can push a copy past the MTU and cause extra transport segmentation at egress.
- The trick shapes only the client's ClientHello. It does nothing for DNS, address-based filtering, or anything the server sends.
- All packets of one flow must stay on one worker; otherwise the trick fails open for that flow with a warning.
Example:
{
"name": "ip-manipulator",
"type": "IpManipulator",
"settings": {
"first-sni": "www.example.com",
"first-sni-count": 2,
"first-sni-replay-delay": 5,
"first-sni-final-delay": 10,
"first-sni-ttl": 6
},
"next": "next-packet-node"
}
smuggle-sni
smuggle-sni creates a path-dependent split view of one TLS ClientHello. After
capturing the real ClientHello, it sends the client's original TCP segments on
real-sni-upstream-node. It sends a separately generated, equal-length
ClientHello carrying the configured host name on the normal next branch. The
two views occupy the same TCP sequence ranges, but they are routed to different
branch entries rather than deliberately sent as an ordered overlap on one
branch.
The intended topology lets an inspection device on the normal branch parse the generated SNI while the destination receives the real ClientHello through the helper branch. The technique does not itself ensure that separation. If the branches converge at the wrong point, packets are reordered, or the destination accepts the generated view, the connection can fail.
| Setting | Type | Default | Description |
|---|---|---|---|
smuggle-sni | string, 1 through 255 bytes | absent (disabled) | Enables the trick and supplies the host name for the generated normal-branch ClientHello. The effective limit comes from the internal TlsClient. |
smuggle-sni-delay-ms | integer ms, >= 0 | 0 | Time after successful capture when the generated batch becomes eligible. It does not control the separate follow-on ordering window. |
real-sni-upstream-node | string | required when enabled | Auxiliary branch that receives the original captured ClientHello packets. |
The shared stateful-flow-limit setting also applies, accepts 32 through
1048576, and defaults to 65536. The node requires both a normal top-level
next and the helper. The named helper must exist, must not be this
IpManipulator, must differ from next, and must be reachable and available
for binding when the chain is constructed.
{
"name": "ip-manipulator",
"type": "IpManipulator",
"settings": {
"smuggle-sni": "www.example.com",
"smuggle-sni-delay-ms": 20,
"real-sni-upstream-node": "real-clienthello-path"
},
"next": "generated-clienthello-path"
}
The generated record uses the internal TlsClient fingerprint, default ALPN
order h2, http/1.1, and has x25519mlkem768 disabled. The 50 ms speculative
prestart timeout, 1500 ms fragmented-record timeout, 50-second ordering window,
capture limits, generated fingerprint, and packet headers have no
Smuggle-SNI-specific settings.
Which flow and ClientHello are selected
Smuggle-SNI operates only on whole, non-fragmented IPv4 TCP packets and creates
a flow record only after seeing a payload-free opening SYN. The accepted
opener has SYN plus an optional combination of ECE and CWR; ACK, FIN,
RST, PSH, URG, any other flag, or TCP Fast Open payload makes it
ineligible. If this node did not observe that opener, later packets on the tuple
pass without Smuggle-SNI processing.
The first two upstream packets in a tracked flow pass normally and count as
warmup. Normally they are the opening SYN and the client's final handshake
ACK; the downstream SYN|ACK is not counted. Capture begins with the next
upstream packet. This ordinal rule means a ClientHello sent in the second
upstream packet is not transformed on that transmission; only a later eligible
copy, such as a retransmission after warmup, could become the capture candidate.
At the capture point, the TCP payload must begin a TLS handshake record whose
handshake is a ClientHello with a usable first host_name SNI entry. A complete
one-segment record is accepted immediately. An incomplete record is retained
while exact contiguous continuations are collected. The starting segment must
contain at least the first nine TLS bytes so the record and ClientHello lengths
are known; a shorter prefix does not start capture.
- every continuation must have nonzero payload and sequence
previous.seq + previous.payload_len, using normal 32-bit TCP sequence arithmetic - the complete capture may use at most 16 TCP packets
- the complete TLS record, including its five-byte record header, may be at most 16,384 bytes
- the ClientHello handshake must exactly fill that one TLS record; a ClientHello split across TLS records or followed by another handshake message in the same record is unsupported
- capture expires 1500 ms after the last captured fragment if no valid continuation advances it
An ACK-only packet, retransmission, gap, overlap, or out-of-order continuation does not get merged into an active capture. It ends the attempt and releases the retained packets followed by the current packet on the normal path. IPv4 fragments and unparseable packets bypass this capture and can overtake held segments; the capture remains pending until a valid continuation or timeout.
Smuggle-SNI also has a speculative prestart queue for packet reordering. If the first capture candidate has nonzero payload of at least 128 bytes, has no recognizable ClientHello prefix, and is not an explicitly unsupported TLS layout, it can be held until 50 ms after the last speculative update in case an earlier-sequence ClientHello start arrives afterward. Up to 16 distinct sequence-numbered packets can be retained. Once a start arrives, only packets that form the exact next sequence are drained into the ClientHello capture; unrelated remainder packets are released normally. Timeout, duplicate sequence, capacity pressure, or replacement of the speculative slot releases the held packets unchanged and makes that flow passthrough. A short non-TLS packet or an ACK-only candidate is not speculatively held and selects passthrough immediately. The prestart queue has no separate byte cap beyond its 16-packet bound and the size of each IPv4 packet.
Capture and prestart slots come from tunnel-wide pools sized at 16 of each per configured worker and shared with the other tricks that use TLS capture. If a pool is full, its oldest active slot is released unchanged to admit the new capture. All fragments of a capture must stay on one WaterWall worker. A cross-worker continuation fails open: each retained packet is returned through its owner worker, but original global arrival order cannot be guaranteed.
How the two ClientHello views are built
Let the real ClientHello TLS record have length R, including its five-byte TLS
record header. Let captured TCP packets P0 ... Pn carry contiguous payload
lengths L0 ... Ln, beginning at TCP sequence S. The final captured packet
may also contain bytes after the end of the ClientHello record, so
L0 + ... + Ln can be greater than R.
After capture, the internal TlsClient creates a complete new ClientHello with
the configured smuggle-sni. This is not an in-place SNI edit or a clone of the
client's fingerprint. The generated record uses the internal client's TLS
randomness, extensions, key share, and ALPN values described above. It must
parse as exactly one ClientHello record, must contain the configured host name,
and its complete length G must satisfy G = R. Changing the configured host
can change G and therefore whether the equality succeeds, although generated
padding and extension choices mean the length relationship is not necessarily
one-for-one.
For every captured packet Pi, IpManipulator makes one normal-branch packet
Fi by copying the entire original IPv4/TCP packet and replacing only the
portion of its TCP payload that intersects the first R stream bytes with the
corresponding generated bytes. Therefore, before final egress processing:
Fihas the same source and destination addresses and ports, TCP sequence and acknowledgement numbers, payload length, TCP flags, window and options, IPv4 identification and TTL, and packet boundary asPi- concatenating the first
Rpayload bytes ofF0 ... Fnyields the generated ClientHello exactly - payload bytes after the real ClientHello record are copied unchanged from the original packet
- checksums are recalculated because the TLS payload changed
The original P0 ... Pn packets are then sent, in reconstructed sequence order,
through real-sni-upstream-node. That helper send preserves the original tuple
and does not apply this node's port-ghost trailer. The generated F0 ... Fn
batch uses the normal next branch, where compatible final protocol swap,
port-ghost, checksum, and MTU handling can still run. Final MTU shaping may split
an oversized packet, so the captured boundaries are preserved only up to that
egress stage.
The helper originals and normal generated batch bypass the remaining Smuggle-SNI/stateful-SNI pipeline rather than being inspected again. Same-node packet duplication, SNI Blender, other stateful SNI tricks, and upstream TCP-bit rewrites are configuration-incompatible with Smuggle-SNI. Protocol swap, port ghost, downstream-only TCP-bit actions, and Smuggle-FIN may coexist subject to their own behavior; the real helper packets still use the tuple-preserving path described above.
Branch order, delay, and the 50-second barrier
The first captured segment may already have waited for the remainder of the ClientHello. Once capture and generation succeed, the observable transcript is:
t0: send real P0 ... Pn to real-sni-upstream-node
t0 + D: send generated F0 ... Fn to normal next
t0 + W: release later queued upstream packets to normal next
D = smuggle-sni-delay-ms
W = max(50000, D + 1) milliseconds
All real packets are submitted to the helper before the fake batch becomes
eligible. At D = 0, the complete fake batch is emitted synchronously after the
real batch. There is no pacing between segments inside either batch. Branches
can have different latency, however, so this submission order is not a guarantee
of arrival order after they traverse the network.
The D setting delays only the generated batch. It does not set the release
time for later client traffic. A separate ordering barrier remains open until
t0 + W; this is 50 seconds for every delay through 49,999 ms, including zero,
and one millisecond after the fake due time for larger delays. Parseable
same-flow upstream packets arriving during that window—including ACKs,
retransmissions, application data, and FIN/RST—enter a FIFO behind the
complete generated batch. Downstream packets are never put in this barrier.
Unparseable packets, IPv4 fragments, and unrelated flows bypass it and can
overtake retained traffic.
The follow-on FIFO holds at most 16 packets and 256 KiB. If it reaches either
limit, a packet arrives on a different worker, the deadline is already past, or
a timer cannot be scheduled, the barrier fails open: any unsent generated
packets leave first, then the retained FIFO, then the current packet. This
preserves per-owner-worker order but can send the generated transcript earlier
than configured. A queued upstream FIN or RST remains behind earlier data
and removes the flow record after release.
Speculative prestart packets that are not incorporated into the contiguous ClientHello are outside this post-success barrier. They are released through the normal path when prestart processing finishes and, in reordering cases, can appear before the generated batch.
What each participant may see
- Client: sends only the real ClientHello segments. WaterWall suppresses those segments from the normal branch, sends them through the helper, and can hold later client packets for the ordering window. The added capture and barrier delays can trigger TCP retransmissions, RTO backoff, congestion-window changes, or an application-visible stall.
- Normal-path middlebox: is intended to reconstruct the generated record at
the client's real sequence numbers and parse the configured SNI. Because its
segment sizes, flags, acknowledgement values, and general header shape are
copied from the client packets, the TCP envelope resembles the captured flow,
although the TLS fingerprint and cryptographic contents come from WaterWall's
internal
TlsClient. - Helper-path device or destination: receives the original ClientHello segments after capture completes. If the helper route reaches the destination while the generated packets are kept from it, the server sees the client's real TLS random, extensions, key share, SNI, and transcript. Equal record lengths keep subsequent TCP sequence numbers aligned when traffic later uses the normal path again.
If the destination receives only the generated ClientHello, its ServerHello and key schedule correspond to WaterWall's independent ClientHello, not to the client's TLS state, so the client will generally be unable to continue the handshake. If it receives both views, they are conflicting bytes on identical sequence ranges; first-arrival/last-arrival reassembly, loss, retransmission, normalization, and path ordering determine which view survives. The real batch is submitted first, but that does not make server-side first-arrival behavior safe across separate branches.
The trick therefore depends on topology: the observer intended to see the fake must be on the normal branch, the endpoint intended to complete TLS must see the real branch view, and later traffic and responses must rejoin coherently. NAT, firewall state, asymmetric routing, anti-spoofing, TCP normalization, and different MTUs on the two branches can all defeat that arrangement.
Fail-open behavior, closes, and flow lifetime
Before any real packet is sent to the helper, the attempt fails open to the
normal branch when the flow opener was not observed or its table record cannot
be admitted, the capture candidate is unsupported, the record is malformed or
lacks SNI, sequence continuity or worker affinity is lost, a capture/prestart
timeout or limit is reached, memory or timer setup fails, or the generated
record is invalid or has a length other than R. Retained originals are
forwarded unchanged by Smuggle-SNI through normal egress. On same-worker paths
they are released in capture order; cross-worker recovery cannot promise global
wire order.
If the real ClientHello contains a TLS 1.3 pre_shared_key extension with PSK
binders and the configured SNI differs from the real SNI, Smuggle-SNI also fails
open. WaterWall does not have the resumption secret needed to recompute binders
for an altered ClientHello. The source applies this guard only when the SNI
would change; an equal configured SNI is not rejected by this condition, though
all other generated-record validation and equal-length requirements still
apply.
Once the helper originals have been sent, a delay-scheduler failure cannot undo that branch transmission. It instead releases the generated batch immediately on the normal branch. Tunnel or flow-table destruction disposes of still-held capture, fake, and FIFO buffers rather than forwarding them during teardown.
An upstream FIN or RST during prestart or ClientHello capture flushes held
originals to the normal path and removes the flow. On the normal same-worker
path, those held packets are submitted before the close; cross-worker recovery
cannot guarantee global order. During the post-success barrier, the close is
queued behind earlier upstream traffic. A downstream FIN or RST is never
delayed; it flushes an unfinished capture but removes a post-success flow
immediately, which disposes of any still-delayed generated or follow-on upstream
packets. Other downstream traffic is unchanged. A lightweight downstream hook
can log a ServerHello that begins in one whole TCP payload, but it does not
validate the branch outcome or alter the packet.
Downstream TCP-bit rewrites are allowed in the same node and run before that
logging/close hook. Consequently, adding or removing FIN/RST with a
dw-tcp-bit-* setting also changes whether Smuggle-SNI observes a downstream
close and performs this cleanup.
For an admitted tracked flow, either success or ordinary fail-open leaves the
normalized four-tuple in passthrough state and Smuggle-SNI does not retry.
Activity can retain that record for up to 20 minutes of idle time. A valid new
opening SYN starts a new generation; it discards any old capture/prestart
packets and destroys an old delay barrier rather than replaying those stale
bytes. An upstream or downstream FIN/RST normally removes the record as
described above.
How smuggle-sni differs from the other tricks
first-sniedits the real ClientHello's SNI and length fields in extra copies and sends both decoy and original on the normal branch. Smuggle-SNI generates the whole alternate record, requires equal record length, and routes the real packets through a helper.overlap-sniandsynfin-sniemit conflicting sequence ranges and synthetic TCP control packets on the normal branch. Smuggle-SNI uses no fakeSYN,FIN, orRST; its split view comes from branch routing.ech-sni-trickextracts a byte-identical decoy already embedded byTlsClientand sends it out of order. Smuggle-SNI creates a new conventional ClientHello from its configured host name.sni-blenderfragments and shuffles the real packet at IPv4 without creating another TLS transcript.smuggle-findoes not inspect TLS at all and routes a synthetic reverseFIN|ACKwhile holding normal traffic.
overlap-sni
overlap-sni replaces two real ClientHello segments with a crafted transcript
on the normal upstream path. It sends the real beginning of the ClientHello,
then a fake TCP SYN, then a generated ClientHello carrying a different host
name on the same sequence numbers, and finally the rest of the real
ClientHello. The downstream path does not rewrite packets; it only observes
FIN/RST to clean up the corresponding flow.
Where first-sni relies on a decoy packet being seen first, overlap-sni
targets TCP reassembly. The intended split view is that the destination keeps
the first-arriving real bytes for the overlapping range, while an intermediate
device treats the fake SYN as a new flow or replaces earlier bytes with the
later overlap and parses the generated host name. TCP overlap handling and
middlebox state machines vary, so neither view is guaranteed.
This is one of the most invasive tricks in the node. It changes the client's TCP
segment boundaries, injects a non-handshake SYN into an established flow,
delays subsequent upstream traffic, and deliberately blocks some ClientHello
layouts. Test it with representative packet captures and on the intended path.
| Setting | Type | Default | Description |
|---|---|---|---|
overlap-sni | string, 1 through 255 bytes | required to enable | Host name for the generated ClientHello. A longer value prevents node startup because the internal TlsClient enforces its 255-byte host-name limit. |
overlap-sni-delay-ms | integer ms, >= 0 | 0 | Delay between the immediately sent real-prefix/fake-SYN pair and packet X, the generated ClientHello. Real continuation packets follow at fixed 2 ms intervals. |
overlap-sni-hold-timeout-ms | integer ms, > 0 | 50 | Maximum wait for the immediately following contiguous segment that completes the selected ClientHello; expiry releases the held segment unchanged. |
overlap-sni-syn-ttl | integer 0 through 255 | copy the held packet's TTL | IPv4 TTL for the fake TCP SYN only. 0 is a real override; omit the field to copy the held segment's TTL. |
This trick requires a normal top-level next.
The generated hello is built by an internal TlsClient, not copied from the
real client. It therefore uses that client's TLS fingerprint, default ALPN list
(h2, http/1.1), and a non-post-quantum key share. Let G be its length. A
longer configured host name normally increases G, enlarges the overlapping
range, and makes the requirement that the real SNI start at or after offset G
harder to satisfy. At runtime, generation fails open if G is greater than 900
bytes or greater than the two captured real payloads combined.
overlap-sni-delay-ms also contributes to an absolute post-transcript window of
max(5000 ms, overlap-sni-delay-ms + 5 ms). Values from 0 through 4995
change the timing of packet X but not the five-second deadline for later
upstream packets; larger values extend that deadline. Increasing the delay gives
an observer more time to process the fake SYN and reduces the chance that X
overtakes the real prefix in the network, at the cost of latency and greater
retransmission/timeout risk.
overlap-sni-hold-timeout-ms should remain comfortably below the sender's TCP
retransmission timeout. Raising it helps only when the next ClientHello segment
arrives more than 50 ms later; a retransmission arriving first makes the pair
fail open.
A low overlap-sni-syn-ttl can let an inspection device near IpManipulator
observe the fake SYN while a later router expires it before the destination.
The usable value depends on path length and where WaterWall sits. A value of 0
normally confines the packet to the local link. This field affects no other
packet in the transcript.
The shared stateful-flow-limit also bounds the overlap flow table. If the table
cannot admit an opening SYN, that flow remains untracked and passes unchanged.
Which flow and packets are selected
A flow record is created only by a payload-free opening SYN (SYN, optionally
with ECN ECE/CWR, but no ACK, FIN, RST, or payload). TCP Fast Open SYN
data is therefore not eligible. Traffic on a 4-tuple whose opening SYN this
node did not see passes unchanged. A later opening SYN on the same tuple starts
a new flow generation.
- Warmup — the first two upstream packets of the flow pass unchanged. In a
normal connection these are the
SYNitself and the client's handshake- completingACK, so the decision point is the third upstream packet, that is, the client's first data segment. - Decision — the third packet is inspected. A payload-free packet, a payload that is not a TLS ClientHello, and a complete ClientHello record all move the flow straight to passthrough and are forwarded unchanged. Only a payload that begins a ClientHello record without completing it is held.
- Hold — the segment is retained inside
IpManipulatorand nothing is sent. A timer armed atoverlap-sni-hold-timeout-msreleases it unchanged if the flow does not continue. - Pair — the very next upstream packet must carry nonzero payload beginning
exactly at
held.seq + held.payload_len. The two payloads must together contain a parseable ClientHello, including its host name, in one TLS record. On success they are replaced by the transcript below. An ACK-only packet, sequence gap, overlap/retransmission, still-incomplete record, ClientHello split across TLS records, or malformed SNI releases both packets unchanged and makes the flow passthrough. The trick waits for exactly one completion segment, not an arbitrary number of segments.
Because only an incomplete first ClientHello is held, this trick does nothing for clients whose ClientHello fits in one TCP segment. Its packet-ordinal rule also means an extra client packet between the handshake ACK and ClientHello can disable it even when the ClientHello is split.
Successful wire transcript
The held segment and the completing segment are validated as strictly
contiguous — current.seq == held.seq + held.payload_len, both with nonzero
payload — and virtually reassembled into one combined payload. IpManipulator
then generates its own ClientHello for overlap-sni through an internal
TlsClient and parses the real host name out of the combined bytes.
Let:
Sbe the held segment's first TCP sequence numberHbe its TCP payload lengthRbe the two real payload lengths combinedGbe the complete generated ClientHello lengthObe the real host name's offset from the start of the combined TCP payload
A successful transcript requires 0 < G <= 900, G <= R, and O >= G. At the
time the second real segment arrives, the normal upstream path emits or
schedules:
- Packet Y, immediately — real bytes
[0, G)at sequenceS. It copies the held packet's IP/TCP headers and TCP flags. This is the real version of the overlapping range. - Fake
SYN, immediately after Y — a header-only packet on the same 4-tuple with sequenceS - 1, acknowledgement0, and onlySYNset. It copies the held packet's remaining IPv4/TCP header fields, including its TCP header length, options, and window; only its length, sequence, acknowledgement, flags, identification, optional TTL, and checksums are changed. - Packet X, after
overlap-sni-delay-ms— the generated ClientHello, exactlyGbytes at sequenceS. It covers the same sequence interval as packet Y but contains the configured host name and internalTlsClientfingerprint. - Real continuation, at 2 ms intervals after X — if
G < H, one packet carries real bytes[G, H)atS + G, followed by a packet containing the completing segment at its original sequenceS + H. IfG >= H, only the still-needed part of the completing segment, real bytes[G, R), is sent atS + G; its earlier bytes were already included in packet Y.
Every packet is emitted with recomputed checksums and each carries its own
incrementing IPv4 identification: packet Y uses the held packet's identification,
then the fake SYN, packet X, and any continuation packets increment it by one.
Packets Y and X use the held packet's flags. When a held-segment continuation is
followed by a completing-segment continuation, the former keeps only CWR,
ECE, URG, and ACK; the last packet keeps the completing segment's flags.
The original two packets are recycled and never appear on the wire. The
replacement therefore contains four or five packets, depending on whether G
reaches into the completing segment. Local scheduling preserves the order above,
but the network can still drop or reorder packets. The configured delay is the
only adjustable separation; the 2 ms continuation spacing is fixed.
What each participant sees
- The destination is intended to accept packet Y first and retain those real
bytes. If the fake
SYNreaches an established TCP endpoint, standards-oriented stacks commonly drop it and send a challenge or duplicate ACK, but other stacks, firewalls, and load balancers may reset or otherwise disturb the connection. If the connection remains established, a first-arrival-wins TCP receiver treats packet X as conflicting duplicate data for[S, S + G), and the continuation completes the real stream. Its TLS layer then receives the original ClientHello. - A reassembling middlebox receives an incomplete real prefix in packet Y;
the enforced
O >= Gcondition keeps the real host name out of that prefix. If the fakeSYNresets its flow state, packet X is then a complete ClientHello with the configured host name. A device that does not reset but prefers later bytes for an overlap can reach the same result. The subsequent real bytes start at or after the end of packet X and may be ignored once the device has classified the complete generated hello. - The client did send the original two segments, but
IpManipulatorsuppresses them and sends the replacement transcript on its behalf. The client can see duplicate/challenge ACKs or a reset caused by the fakeSYN. If the server does not acknowledge the reconstructed real bytes, the client may retransmit its original ClientHello segments.
The desired split view depends on packet Y reaching the server before packet X,
the fake SYN not terminating server-side state, and the middlebox using a
different policy from the server. Loss, path reordering, TCP normalization, SYN
proxies, and devices that validate receive windows can all defeat the technique.
In particular, if packet X reaches the destination before packet Y, the
destination may accept the generated bytes instead of the real ones and the TLS
connection will not have the intended transcript.
Blocked flows
The implementation requires the complete generated hello to end no later than
the start of the real host name: O >= G. This keeps the real SNI wholly outside
the ambiguous range and in the later real continuation. If O < G, the trick
does not fall back to normal forwarding. It logs the real name and both offsets,
drops the held and current packets, and marks the flow blocked.
A blocked flow drops every later upstream packet. An upstream FIN or RST
removes the record but is itself dropped; a downstream FIN or RST is
forwarded unchanged and also removes the record. IpManipulator does not inject
a reset to tell the client why the data disappeared, so the usual visible result
is a stalled connection followed by a client timeout or a close initiated by the
server/path.
This is a deliberate fail-closed choice rather than a fail-open one, and it is
the main risk of enabling overlap-sni. It becomes more likely when the
generated hello is large relative to the real one. A longer configured name
tends to increase G; the real client's extension ordering and padding
determine O. Many normal ClientHellos place SNI too early for this condition,
so representative packet captures should be checked before deployment.
Delay window, retransmissions, and close packets
After a successful transcript the flow enters passthrough with a delay window of
max(5000 ms, overlap-sni-delay-ms + 5 ms). While that window is open, later
upstream packets of the flow, including ACKs and FIN/RST, are queued in the
per-flow FIFO of at most 16 packets and 256 KiB, and released together at the
window deadline. The scheduled transcript tail always leaves ahead of that FIFO.
Downstream traffic is never delayed.
FIFO overflow, a packet of the flow arriving on another worker, or a timer that cannot be armed releases pending output in order rather than extending the stall: the remaining crafted transcript goes first, followed by queued packets and then the current packet. On a busy flow, the FIFO limit can therefore end the nominal five-second window early.
The FIFO also catches TCP retransmissions sent after the crafted transcript has been built. A retransmission or ACK-only packet arriving while the initial segment is merely held instead fails the required two-packet pairing, releases the held packet followed by the current packet unchanged, and makes the flow passthrough.
The downstream path never changes packet bytes. A downstream FIN or RST
during the initial hold releases that held upstream segment unchanged and
removes the flow. During the post-transcript delay window it removes the flow and
cancels any still-scheduled crafted continuation and queued upstream packets;
the downstream close packet itself continues unchanged. An upstream FIN or
RST in the delay window remains ordered behind the crafted transcript and any
older queued packets.
When overlap-sni does nothing
The trick fails open, releasing the held segment and forwarding traffic unchanged, when:
- the flow's opening
SYNwas never observed by this node - the packet is not IPv4, not TCP, or is an IPv4 fragment
- the third packet has no payload, is not a TLS ClientHello, or already contains a complete ClientHello record
- the completing segment does not arrive within
overlap-sni-hold-timeout-ms - the very next packet is not sequence-contiguous with the held one, has no payload, or still does not complete a supported one-record ClientHello
- the combined bytes do not parse as a ClientHello with a host-name entry
- the internal
TlsClientcannot generate a hello, the generated hello is longer than 900 bytes, or it is longer than the real combined payload - the replacement packets cannot be built, the hold timer cannot be armed, the
flow table cannot admit the opening
SYN, or retained state crosses workers
The O < G case described under "Blocked flows" is the intentional exception:
it fails closed and does not release the two selected real packets.
How overlap-sni differs from the other SNI tricks
first-snicopies the client's own ClientHello, changes its host name, and sends the decoy before the untouched real packet. It can operate on a single-segment ClientHello and uses TTL or a random sequence number to keep the decoy away from the server; it does not inject a fake handshakeSYN.smuggle-snican capture up to 16 ClientHello segments, sends the real segments through an auxiliary branch, and later sends a generated copy on the normal branch while preserving the captured segmentation.overlap-sniaccepts exactly two ClientHello segments and puts both views on the same normal path.synfin-snialso starts from a two-segment ClientHello but adds a craftedFIN|ACKorRST|ACK, a fakeSYN, a generated hello, and TLS-looking filler in one immediate transcript.overlap-snihas no synthetic close or filler packet and instead relies on the exact Y/X overlap plus configurable delay.sni-blenderfragments and reorders the real packet at the IPv4 layer without inventing another host name.ech-sni-trickreorders a fake inner ClientHello already embedded byTlsClient; it does not generate packet X here.
Limitations and side effects
- Only whole, non-fragmented IPv4 TCP packets are eligible. IPv6 and IPv4 fragments pass unchanged.
- A normal top-level
nextis required. All crafted packets use that path; there is no overlap-specific helper branch. - The generated hello follows the internal
TlsClientfingerprint, including its default ALPN list, rather than the real client's fingerprint. Only the generated packet contains the configured name; the real ClientHello bytes are never rewritten. - The client's two original segment boundaries and the completing segment's IPv4 identification are not preserved. The replacement is four or five packets with copied headers, sequential IPv4 identifiers, valid recomputed checksums, and fixed 2 ms tail spacing.
- The fake
SYNis a protocol violation on an established connection and may be normalized, challenged, dropped, or answered with a reset. A lowoverlap-sni-syn-ttlcan keep it from the server but cannot guarantee how a nearer device interprets it. - Each flow's packets must remain on one WaterWall worker while a packet or delay barrier is retained. A worker mismatch logs a warning and fails open.
- Flow records expire after 20 minutes of inactivity and are bounded by
stateful-flow-limit. Tuple reuse begins a new generation so an old timer cannot release data into the replacement flow. - The trick cannot share one
IpManipulatorinstance with another stateful SNI trick,sni-blender,packet-duplicate, or an upstream TCP-bit action. Use a separate node if another compatible packet-shaping stage is required.
Example:
{
"name": "ip-manipulator",
"type": "IpManipulator",
"settings": {
"overlap-sni": "www.example.com",
"overlap-sni-delay-ms": 5,
"overlap-sni-hold-timeout-ms": 50,
"overlap-sni-syn-ttl": 6
},
"next": "next-packet-node"
}
ech-sni-trick
ech-sni-trick coordinates with TlsClient to expose a decoy ClientHello as
an out-of-order TCP segment before the real outer ClientHello is released. It
does not encrypt ECH or rewrite the real ClientHello. The data transformation is
upstream-only; the downstream path leaves packet bytes untouched and watches
only for a TCP FIN or RST that should cancel retained state.
| Setting | Type | Default | Description |
|---|---|---|---|
ech-sni-trick | string, 1 through 255 bytes | required to enable | Host name that must appear in the embedded decoy ClientHello. Configure the same byte string in the corresponding TlsClient. |
data-shard-1-delay | integer ms, >= 0 | 0 | Time from sending the out-of-order decoy to releasing original packet 1. |
data-shard-2-delay | integer ms, >= 0 | 0 | Additional time after original packet 1 before originals 2 through N are released together in order. Ignored for a one-packet capture. |
stateful-flow-limit | integer, 32 through 1048576 | 65536 | Shared setting that bounds the ECH flow table; see "Stateful Flow Tables." |
The ech-sni-trick value is matched byte for byte and case-sensitively; no DNS-
name normalization is performed. Changing it only in IpManipulator causes a
runtime mismatch and makes the captured ClientHello fail open unchanged.
Changing it in both nodes changes the decoy SNI and usually changes the embedded
ClientHello length, its position within the outer hello, and the out-of-order
packet size.
The two delays are relative. If the decoy leaves at time T, original packet 1
leaves at T + data-shard-1-delay, and originals 2 through N leave at
T + data-shard-1-delay + data-shard-2-delay. Their sum must fit the supported
unsigned 32-bit millisecond range. Increasing the first delay gives a packet-
oriented observer more time to process the decoy before lower sequence numbers
arrive. Increasing the second separates the first original shard from the rest.
Both choices also prolong TCP reordering and TLS-handshake latency. At 0, the
relevant packets retain the same send order but leave back to back.
This trick requires a normal top-level next. It has no TTL, random-sequence,
packet-count, or helper-branch setting.
Example IpManipulator settings:
{
"name": "ech-packet-shaper",
"type": "IpManipulator",
"settings": {
"ech-sni-trick": "cover.example",
"data-shard-1-delay": 10,
"data-shard-2-delay": 10
},
"next": "next-packet-node"
}
The TlsClient that creates this flow must use the same decoy value while its
ordinary sni remains the destination name:
{
"sni": "origin.example",
"ech-sni-trick": "cover.example"
}
What the trick is for
This trick targets inspection devices that parse a TLS record found in an individual TCP segment, or that make an early classification from out-of-order data before the missing lower sequence range arrives. It puts a complete decoy ClientHello on the wire first, at a valid but future TCP sequence number. The ordinary outer ClientHello follows later and remains the byte stream delivered to a correct TCP/TLS endpoint.
Unlike an overlap trick, the early segment does not conflict with the eventual stream. It is a copy of bytes that already occur at the same sequence interval inside the outer ClientHello. A strict TCP reassembler therefore reconstructs only the original outer ClientHello. The hoped-for different view exists only at an observer that parses the early segment by itself or remembers its SNI before reassembly completes. This behavior is path- and middlebox-dependent and is not guaranteed.
How the decoy gets into the outer ClientHello
The feature has two coordinated parts:
TlsClient.settings.ech-sni-trickcreates a separate, complete TLS ClientHello record whose SNI is the configured decoy name. It uses the configuredTlsClientALPN list in the same order and disables thex25519mlkem768key share for this decoy so that it remains smaller.TlsClientplaces those raw record bytes, unencrypted, in the payload field of a GREASEencrypted_client_helloextension in the real outer ClientHello. The outer cleartext SNI remainsTlsClient.settings.sni.IpManipulatorcaptures the outer ClientHello, locates that already- embedded record, and copies it into the early TCP segment. It never generates the decoy, rewrites either SNI, or removes bytes from the outer hello.
This is not standards-based ECH confidentiality. The extension has a GREASE ECH shape, but its payload is recognizable ClientHello plaintext rather than valid ECH ciphertext. Both the real outer SNI and the decoy SNI remain visible as clear bytes on the wire.
The expected destination behavior is to continue with the outer ClientHello when it has no matching ECH configuration, but servers, proxies, and security devices are not universally tolerant of unusual GREASE payloads.
Flow eligibility and ClientHello capture
The trick is sequence-aware rather than packet-ordinal-based:
- A payload-free opening
SYNcreates the flow record.ECEandCWRmay be present independently or together for ECN negotiation;ACK,FIN,RST, any other flag, or TCP Fast Open payload makes the packet ineligible as an opener. If this node did not see the openingSYN, later packets on that tuple pass unchanged. - Payload-free upstream packets, including the TCP handshake's final
ACKand ACKs interleaved between ClientHello segments, pass immediately and do not consume a capture slot. - The first non-empty upstream payload must begin with a recognizable TLS
handshake record and ClientHello prefix. A complete one-packet ClientHello
can be used immediately. A recognizable partial prefix starts capture; an
unrelated first payload makes this flow permanently pass through until a new
opening
SYNstarts another generation. - Further data segments are retained only when each begins exactly at the next expected TCP sequence number. Capture stops when the declared first TLS record is complete. A ClientHello spread across multiple TLS records is not supported. If the completing TCP segment also contains bytes after that record, the whole original segment is retained and later replayed, although only the first record is inspected.
At most 16 original packets may participate, and the declared TLS record, including its five-byte header, may not exceed 16,384 bytes. The capture has a 1,500 ms inactivity timeout, refreshed by accepted data or an exact retransmission. An exact retransmission of a packet already retained is discarded as a duplicate while capture continues. A gap, out-of-order segment, partial or conflicting overlap, unsupported TLS layout, resource failure, size or packet limit, timeout, or worker-affinity mismatch fails open: retained originals are forwarded before the current packet, and the rest of that flow passes normally.
After capture, IpManipulator requires all of the following:
- a parseable outer ClientHello with a usable first
host_nameSNI entry; - a syntactically parseable outer-form
encrypted_client_helloextension and its bounded payload field; and - exactly one complete TLS ClientHello record anywhere in that payload whose
first SNI host name equals the configured
ech-sni-trickvalue byte for byte.
Nonmatching ClientHello candidates do not count. Two matching candidates are
ambiguous and fail open. No match, a malformed or truncated candidate, an outer
hello without SNI or ECH, and an IpManipulator/TlsClient setting mismatch
also fail open by releasing the originals without emitting the decoy segment.
Successful TCP transcript
Let:
Sbe the first captured packet's TCP sequence number;Obe the decoy ClientHello's byte offset from the beginning of the captured TCP payload; andLbe the complete decoy ClientHello record length.
The decoy already occupies stream interval [S + O, S + O + L) inside the
outer ClientHello. On success, the wire order is:
- All original ClientHello packets remain held.
IpManipulatorcreates one packet with sequenceS + Oand payload equal to exactly thoseLembedded bytes. If the embedded record crossed original packet boundaries, this copy still coalesces it into one TCP segment. - That packet is sent immediately. It copies its IPv4 and TCP header template
from the captured data segment covering offset
O, including addresses, ports, acknowledgement number, window, TCP options, TTL, and IPv4 identification. Its length and sequence number are adjusted, checksums are recomputed, and its flags preserveCWR,ECE,URG, andACKfrom the template, addPSH, and omitSYN,FIN, andRST. Reusing the template's IPv4 identification means the decoy and one original can have the same ID. - After
data-shard-1-delay, original packet 1 is released with its original payload boundary, sequence number, and TCP flags. - When there is more than one captured packet,
IpManipulatorwaits the additionaldata-shard-2-delay, then releases original packets 2 through N consecutively in their captured order. There is no per-packet delay within this second group.
Thus the nominal send times are T, T + delay1, and
T + delay1 + delay2. A one-packet ClientHello uses only the first delay. With
both settings at 0, callback order still puts the decoy first, but all packets
leave back to back. If a release timer cannot be scheduled, the affected
originals are flushed immediately rather than left stranded.
The retained originals are not rewritten by this trick. Their payload bytes at
[S + O, S + O + L) are identical to the early copy, so the destination never
has to choose between conflicting overlap contents. Compatible final-egress
features in the same node, such as protocol swap or port ghost, still run after
this transcript is built and can change the on-wire wrapper or segment it as
documented in their own sections.
What the client, middlebox, and server may see
- The client sends one ordinary outer ClientHello and does not know that
IpManipulatorretained it and injected a copy of an internal byte range. The early higher-sequence segment may cause the destination to return a duplicate ACK forS, and, when SACK was negotiated, a SACK block covering the received future range. These responses can influence the client's loss recovery even though the injected bytes equal data it already submitted. - A packet-oriented inspector sees a TCP payload beginning with a complete TLS ClientHello carrying the configured decoy SNI before it sees the lower- sequence outer hello. A device that parses out-of-order payloads or latches the first SNI may classify the flow from that decoy. A strict stream reassembler instead waits for the gap, reconstructs the original outer ClientHello, and can see the real outer SNI; a device that examines both may ignore, replace, or penalize the earlier verdict.
- The destination TCP stack may buffer the early segment as out-of-order data,
acknowledge it with SACK, or discard it because of its receive-window or
normalization policy. Once the originals fill the gap, a tolerant stack
reconstructs the same outer ClientHello bytes that
TlsClientgenerated. Its TLS endpoint is then expected to use the outer SNI and treat the unusable GREASE ECH offer compatibly. TCP normalizers, SYN proxies, TLS gateways, and ECH-aware servers may behave differently.
The delays provide time for an early middlebox decision; they do not force one. Larger values also increase handshake latency and the duration of TCP reordering. Values near a sender's retransmission or application timeout can defeat the connection rather than improve the trick.
Retransmissions, later traffic, and connection close
After the decoy is emitted, only an exact retransmission of a still-pending original segment—same sequence, payload length, and payload bytes—is swallowed. An ACK-only packet, later application data, or a partial/ambiguous overlap is forwarded immediately. Later data is not queued behind the two release timers, so it can itself overtake the held ClientHello and add more out-of-order traffic. Once an original has been released, its retransmissions are no longer swallowed.
Close handling preserves TCP ordering where a graceful upstream close still makes sense:
- An upstream
FINduring incomplete capture releases captured originals before theFIN. During delayed release it flushes every still-pending original before theFIN. - An upstream
RSTduring delayed release discards pending originals and then passes the reset. During incomplete capture, the held originals are released before the reset packet. - A downstream
FINorRSTpasses unchanged but removes the flow record, disposes of an incomplete capture, and cancels any originals that have not yet been released.
Flow records are keyed by the TCP four-tuple and a connection generation. A new
valid opening SYN on a reused tuple invalidates the preceding capture and
release timers before starting the replacement flow, so stale ClientHello bytes
are not injected into it. After release, the record remains in passthrough until
it closes, is replaced, or expires after 20 minutes of inactivity.
How ech-sni-trick differs from the other SNI tricks
first-snimakes one or more full copies of the client's outer ClientHello, rewrites the SNI in each copy, and uses TTL or a random sequence number to try to keep those conflicting decoys away from the server.ech-sni-trickuses one pre-embedded decoy at its real in-stream sequence interval, with no TTL or random-sequence option and no conflicting bytes.smuggle-snisends the real ClientHello over a configured helper branch and a generated decoy over the normal branch after a delay.ech-sni-trickneeds no helper branch and sends both views on the normal path.overlap-sniandsynfin-snireplace captured segment boundaries with deliberately conflicting sequence ranges and inject fakeSYNand close or filler packets.ech-sni-trickinjects no TCP control packet and replays the original segmentation.sni-blenderfragments and reorders the real ClientHello at the IPv4 layer.ech-sni-trickdoes not create IPv4 fragments; it sends one extra TCP data segment copied from the GREASE ECH payload.
Limitations and side effects
- Only whole, non-fragmented IPv4 TCP packets are eligible. IPv6 and IPv4 fragments pass unchanged.
- A normal top-level
nextis required.IpManipulatormust see the openingSYN, and the first non-empty client payload must begin the supported one-record ClientHello layout. - The decoy plus copied IP/TCP headers must fit
mtu. An oversized decoy fails open rather than being split by the ECH trick. A longer configured SNI can enlarge both the outer record and this packet. - The flow table is bounded by
stateful-flow-limit; an unadmitted flow passes unchanged. Incomplete TLS captures also use a finite shared slot pool. Under pressure, an older incomplete capture can be released and moved to passthrough. - Every retained packet for a flow must remain on one WaterWall worker. IpManipulator establishes packet-line affinity internally before stateful processing; a remaining worker-affinity mismatch still fails open.
- The early packet duplicates the template packet's IPv4 identification and can trigger duplicate ACKs, SACKs, retransmission heuristics, traffic-normalizer rules, or anomaly detection. The delays also hold memory and postpone the TLS handshake.
- This is not real ECH and provides no SNI confidentiality: the real SNI is in the outer ClientHello and the decoy SNI is visible inside the GREASE payload.
- The trick cannot share one
IpManipulatorwith another stateful SNI trick,sni-blender,packet-duplicate, or an upstream TCP-bit action. Compatible operations that must shape its emitted packets belong in a followingIpManipulatornode.
synfin-sni
synfin-sni is an upstream-only, stateful TCP overlap trick for a TLS
ClientHello split across exactly two selected data segments. It suppresses those
two original packets and emits a replacement transcript containing real bytes,
a synthetic close, a fake SYN, a generated decoy ClientHello, optional filler,
and the remaining real bytes. Downstream packets are not inspected or modified
by this trick.
The intended split view uses both TCP connection state and conflicting overlap
policy. A destination that retains the first bytes received for a sequence range
can reconstruct the real ClientHello. An intermediate device may instead treat
the close and following SYN as a flow boundary, or prefer the later bytes in an
overlap, and parse the generated ClientHello carrying synfin-sni.
This is one of the most protocol-invasive tricks in the node. It deliberately
sends conflicting TCP data plus a close and another SYN on an established
four-tuple. With default TTLs, sequences, and valid checksums, those synthetic
packets can reach and close or reset the destination connection. Test against
representative clients, servers, middleboxes, and packet captures.
| Setting | Type | Default | Description |
|---|---|---|---|
synfin-sni | string, 1 through 255 bytes | required to enable | Host name for the generated decoy ClientHello. The effective limit comes from the internal TlsClient. |
synfin-sni-hold-timeout-ms | integer ms, > 0 | 50 | Maximum wait for the immediate contiguous segment that completes the selected ClientHello. Expiry releases the held segment unchanged. |
synfin-sni-additional-range-min | integer 0 through 65535 | 0 | Requested minimum extension beyond the generated hello length for the real-first/fake-filler overlap. If set without the maximum, it also becomes the maximum. Runtime safety clamping may reduce it. |
synfin-sni-additional-range-max | integer 0 through 65535 | 0, or the configured minimum when only the minimum is present | Requested maximum overlap extension. If set alone, the minimum remains 0; minimum must be no greater than maximum. |
synfin-sni-syn-ttl | integer 0 through 255 | copy the opening SYN TTL when its template is available | IPv4 TTL for the fake SYN only. Construction falls back to the held data header if the opening-header copy is unavailable. |
synfin-sni-fin-ttl | integer 0 through 255 | copy the held data packet TTL | IPv4 TTL for the synthetic `FIN |
synfin-sni-fake-ttl | integer 0 through 255 | copy the held data packet TTL | IPv4 TTL for packet X, the complete generated ClientHello, only. It does not affect filler or real packets. |
synfin-sni-random-syn-checksum | boolean | false | Put arbitrary random values in the fake SYN's IPv4 and TCP checksum fields instead of valid recomputed checksums. |
synfin-sni-random-fin-checksum | boolean | false | Apply the arbitrary-checksum behavior to the synthetic close packet only. |
synfin-sni-random-syn-sequence | boolean | false | Use a random 32-bit fake-SYN sequence instead of S - 1. Packet X remains at S. |
synfin-sni-random-fin-sequence | boolean | false | Use a random 32-bit close sequence instead of S + Y, the end of the overlap interval. |
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 applicable template TTL. A value of 0 will normally prevent a
routed packet from progressing beyond the local link, but exact handling remains
path-dependent.
The generated record uses an internal TlsClient, with default ALPN order
h2, http/1.1 and x25519mlkem768 disabled. It is not copied from the real
client's fingerprint. Let G be its complete record length. Runtime generation
fails open unless G is at most 900 bytes and no greater than the two selected
real payloads combined. A longer synfin-sni value normally increases G and
makes the real-SNI placement requirement below harder to meet.
The additional range is selected once per successful flow after clamping both
configured ends to what the real payload and SNI position permit. For an actual
selected value A, A < 5 produces A random filler bytes; A >= 5 produces
one TLS Application Data record header (17 03 03, record length A - 5)
followed by random bytes. The configured minimum is therefore a request rather
than a guarantee when the safe range is smaller.
The complete replacement sequence is emitted synchronously and back to back;
the hold timeout does not pace it, and there is no other SynFIN delay setting.
The shared stateful-flow-limit bounds the SynFIN table and defaults to 65536.
This trick requires a normal top-level next and has no helper branch.
Example showing all controls (values are illustrative, not path-independent recommendations):
{
"name": "ip-manipulator",
"type": "IpManipulator",
"settings": {
"synfin-sni": "www.example.com",
"synfin-sni-hold-timeout-ms": 50,
"synfin-sni-additional-range-min": 8,
"synfin-sni-additional-range-max": 24,
"synfin-sni-syn-ttl": 5,
"synfin-sni-fin-ttl": 5,
"synfin-sni-fake-ttl": 5,
"synfin-sni-random-syn-checksum": false,
"synfin-sni-random-fin-checksum": false,
"synfin-sni-random-syn-sequence": false,
"synfin-sni-random-fin-sequence": false,
"synfin-sni-use-rst": false
},
"next": "next-packet-node"
}
Which flow and packets are selected
A record is created only when this node sees a payload-free opening SYN.
ECE and CWR may accompany SYN; ACK, FIN, RST, any other flag, or TCP
Fast Open payload makes the opener ineligible. Packets on a tuple whose opening
SYN was not observed pass unchanged. A later valid opening SYN on the same
tuple starts a replacement generation.
- Warmup — the first two non-close upstream packets pass unchanged and
count as warmup. Normally these are the opening
SYNand the client's final handshakeACK; the downstreamSYN|ACKis not counted. The third upstream packet is therefore normally the first ClientHello segment. - Decision — if that third packet is payload-free, is not the beginning of a recognizable TLS ClientHello, or already contains the complete first TLS record, it passes immediately and the flow becomes passthrough. Only a partial or fragmented one-record ClientHello beginning is held.
- Hold — the selected segment remains inside
IpManipulator; nothing from it is sent while the hold timer runs. Timeout releases it unchanged and makes the flow passthrough. - Pair — the very next upstream packet must have nonzero payload and begin
exactly at
held.seq + held.payload_len. The combined bytes must contain a parseable ClientHello whose handshake exactly fills one TLS record and has a usable firsthost_nameSNI entry. An ACK, exact retransmission, gap, overlap, out-of-order segment, still-incomplete record, or malformed layout releases the held packet followed by the current packet unchanged and selects passthrough. The trick never waits for a third data segment.
The declared first TLS record may not exceed 16,384 bytes. A ClientHello split across TLS records is unsupported. If the completing TCP segment also contains bytes after the selected record, those bytes remain in the combined real payload and are preserved in the real continuation.
The packet-ordinal rule is strict. A client whose ClientHello fits in one segment is never transformed, and any extra client packet between the handshake ACK and the first ClientHello segment can make the third packet select passthrough.
Successful TCP transcript
Let:
Sbe the held segment's first TCP sequence numberHbe the held segment's payload lengthRbe the two real payload lengths combinedGbe the generated decoy ClientHello record lengthObe the real SNI host name's offset from the beginning of the combined TCP payloadAbe the chosen additional range after runtime clampingY = G + Abe the length of the deliberately overlapping interval
A successful transcript requires 0 < G <= 900, G <= R, and O >= G.
IpManipulator clamps the additional range so that
0 <= A <= min(R - G, O - G). Consequently Y <= O: real packet Y can end
exactly before the first real SNI byte, but never carries a byte of the real host
name.
The original held and completing packets are recycled. Before any later egress segmentation, five through seven replacement packets leave synchronously in this order:
- Real packet Y — real bytes
[0, Y)at sequenceS. It copies the held packet's IP/TCP header and exact TCP flags. Its payload can cross the original boundary atH, coalescing bytes from both selected segments. - Synthetic close — a header-only
FIN|ACK, orRST|ACKwhensynfin-sni-use-rstis true. Its default sequence isS + Y. The acknowledgement number, window, TCP options, addresses, and ports come from the held data packet. - Fake SYN — a header-only packet with only
SYNset, acknowledgement0, and default sequenceS - 1. It normally uses the captured openingSYNas its header template, retaining that SYN's TCP header length, options, and window. If retaining the template failed, the held data header is used instead. - Fake packet X — exactly the
Ggenerated ClientHello bytes at sequenceS. Its flags preserveCWR,ECE,URG, andACKfrom the held segment and addPSH;SYN,FIN, andRSTare omitted. X conflicts with real Y over sequence interval[S, S + G). - Optional fake filler — when
A > 0, one packet covers[S + G, S + Y). It contains the random or TLS Application Data-shaped bytes described above and uses the same continuation-plus-PSHflag policy as X. - One or two real tails — real bytes
[Y, R)complete the original stream. IfY < H, the held segment's suffix leaves atS + Y, followed by the completing segment at its original sequenceS + H. Otherwise only the unused suffix of the completing segment leaves atS + Y.
Real Y starts with the held packet's IPv4 identification. The close, fake SYN,
X, filler, and tails receive consecutively increasing identifications, so the
completing packet's original identification is not preserved. Rebuilt packets
normally carry valid IPv4 and TCP checksums. When the held-segment suffix is
followed by a second tail, that first suffix keeps only CWR, ECE, URG, and
ACK; the final tail uses the completing segment's flags.
There is no artificial sleep or configurable pacing between these outputs. Compatible final-egress behavior in the same node, including port ghost, protocol swap, and MTU-driven TCP segmentation, runs after transcript construction and can alter the wrapper or increase the final packet count.
What each participant may see
- The client sent two normal ClientHello segments, but WaterWall suppresses them and sends the replacement transcript. Synthetic controls and conflicting data can provoke duplicate ACKs, challenge ACKs, SACK changes, or a reset. If acknowledgement progress does not match the client's original transmission, its TCP stack may retransmit the original segment boundaries; later retransmissions are not suppressed after the transcript succeeds.
- A stateful middlebox first sees a real prefix ending before the real host
name, followed by a close and another
SYN. With default sequences, the newSYNatS - 1makes X atSlook like first data on a replacement flow. Such a device may parse the decoy SNI and optional TLS-looking filler. A device that ignores the controls but uses last-arrival-wins overlap can reach the same payload view. A strict first-arrival-wins reassembler retains Y instead. - The destination is intended to retain real Y for
[S, S + Y), ignore X and filler as conflicting duplicates, and append the real tail, reconstructing the original ClientHello. That requires Y to arrive first and the synthetic controls not to terminate destination-side state. A valid, in-windowFINatS + Ycan close the receive stream exactly where the real tail starts; an acceptedRSTterminates the connection immediately.
The desired result is path-dependent. Loss, network reordering, first-versus- last overlap policy, checksum validation, receive-window validation, TCP normalization, SYN proxies, NAT/firewall state, and server TCP behavior can all change the outcome. In particular, if X reaches a first-arrival-wins destination before Y, the generated bytes can poison the server-side TLS stream.
Effect of TTL, checksum, sequence, and close settings
The three TTL settings are independent. synfin-sni-syn-ttl applies only to the
fake SYN; synfin-sni-fin-ttl applies only to the close;
synfin-sni-fake-ttl applies only to X, not to the filler. Lower TTLs can let a
nearby observer see a packet that expires before the destination, but they do not
target a particular middlebox and route changes can invalidate a chosen value.
The two random-checksum settings make both the IPv4 and TCP checksum fields on their selected control packet arbitrary. They will normally be invalid, though a random field can coincidentally match the correct value. A passive classifier may inspect such a packet while a validating endpoint drops it; a validating middlebox drops it too. Y, X, filler, and real tails retain valid checksums. A later egress operation that rebuilds packets, such as port-ghost tailing or MTU segmentation, can repair an arbitrary checksum, so account for following stages when bad checksums are essential.
synfin-sni-random-syn-sequence moves only the fake SYN. X remains at S, so
the coherent default relationship SYN(S - 1) -> data(S) is lost. This can keep
the control outside a destination window, but can also stop a sequence-aware
middlebox from treating X as data in the new flow. Similarly,
synfin-sni-random-fin-sequence moves only the close: it may become less likely
to be accepted by the destination and less credible to a sequence-aware
observer.
synfin-sni-use-rst changes FIN|ACK to RST|ACK without changing the default
sequence position S + Y. An accepted RST is generally more destructive than
an orderly FIN and does not represent a graceful half-close consuming one
sequence number.
Increasing the additional range enlarges both real Y's first-arrival coverage
and the later filler coverage, moving the close and real tail to S + Y. It can
never move past the real SNI start. Actual ClientHello length and extension order
may clamp even the configured minimum downward.
Fail-open and blocked behavior
Most unsupported cases fail open. The held and current packets are released or forwarded without the crafted transcript when:
- the opening
SYNwas not observed, the packet is not whole IPv4 TCP, or the flow table cannot admit a record - the third packet is payload-free, non-TLS, or already contains a complete ClientHello record
- the hold expires or its timer cannot be armed
- the immediate next packet has no payload, is not strictly contiguous, or does not finish a supported one-record ClientHello with SNI
- the internal hello cannot be generated,
G > 900, orG > R - a replacement packet cannot be built or retained state crosses workers
The O < G case is an intentional fail-closed exception. If the real host name
begins before the generated record ends, IpManipulator logs the mismatch,
drops both selected real packets, and marks the upstream flow blocked. Every
later upstream packet is dropped. An upstream FIN or RST removes the record
but is itself dropped. A new valid opening SYN on that tuple starts a new
generation. No local reset is sent to explain the failure, so the usual symptom
is a stalled connection.
Retransmissions, closes, and flow lifetime
During the initial hold, an upstream FIN or RST releases the held segment
before forwarding the close and removes the record. An ACK-only packet or a
retransmission arriving as the expected completion instead breaks the two-packet
pair, releases held then current unchanged, and makes the flow passthrough.
After a successful transcript, the flow immediately becomes passthrough. Later
ACKs, application data, original ClientHello retransmissions, and closes are not
queued behind a delay window. A later retransmission can therefore add the
client's original segmentation and overlap policy back to the wire. An upstream
FIN or RST removes a passthrough record and continues normally.
Downstream traffic, including a downstream FIN or RST, is not inspected by
SynFIN and does not remove its record. Otherwise-unused records expire after 20
minutes of inactivity. Every retained pair must remain on one WaterWall worker;
a cross-worker completion fails open, but the current packet continues on its
worker while the held packet is returned to its owner worker, so original wire
order cannot be promised on that error path.
A replacement opening SYN invalidates old hold-timer generations before
starting warmup again. If tuple reuse happens during an active hold, the old held
buffer is disposed rather than inserted into the replacement connection.
How synfin-sni differs from the other SNI tricks
first-sniclones the real ClientHello, changes its host name, and tries to keep the decoy away from the server with TTL or a random sequence. It can work on one ClientHello segment and injects no connection-state controls.smuggle-snicaptures up to 16 segments and sends the real originals through a helper branch while a generated hello uses the normal branch after a delay. SynFIN accepts exactly one held segment plus its immediate contiguous completion, has no helper branch, and emits all views back to back.overlap-sniuses the same two-segment selection and real-Y/fake-X overlap, but injects only a fakeSYN, fixes the overlap length atG, and schedules X and the real tails with delays. SynFIN adds a precedingFIN|ACK/RST|ACK, can enlarge the overlap with filler, offers checksum and sequence controls, and has no post-transcript delay window.ech-sni-tricksends an out-of-order, byte-identical copy of a decoy already embedded byTlsClient; it creates no conflicting TCP bytes or controls.sni-blenderfragments and shuffles the real packet at IPv4 rather than replacing TCP segmentation or generating another ClientHello.
Limitations and side effects
- Only whole, non-fragmented IPv4 TCP packets are eligible. IPv6, IPv4 fragments, and TCP Fast Open SYN data pass unchanged.
- The ClientHello must begin on the third upstream packet, be incomplete there, and become parseable after exactly one contiguous data segment. One-segment and three-or-more-segment ClientHellos are not transformed.
- The generated hello uses the internal
TlsClientfingerprint and default ALPN list rather than the real client's fingerprint. The configured name must be 1 to 255 bytes, andG,R, andOmust satisfy the constraints above. - Original data boundaries and the completing packet's IPv4 identification are discarded. Final MTU shaping can split rebuilt data again, increasing the nominal five-to-seven packet count.
- Synthetic close/SYN packets on an established connection may be normalized, challenged, rate-limited, dropped, or treated as an attack by hosts, firewalls, NATs, load balancers, SYN proxies, and intrusion systems.
- The bounded flow table uses
stateful-flow-limit; admission pressure fails open. Active holds retain packet memory until paired, timed out, closed upstream, replaced, or destroyed, and require consistent worker affinity. - A normal top-level
nextis required. This trick cannot share oneIpManipulatorwith another stateful SNI trick,sni-blender,packet-duplicate, or an upstream TCP-bit action. Put compatible later packet shaping in another node and account for its effects on TTL, checksum, segmentation, tuple, and packet order.
smuggle-fin
smuggle-fin is a two-branch ordering trick. It holds an ordinary data packet
on the normal path, sends a plausible reverse-direction FIN|ACK through a
separate helper path, and waits for an exact copy of that FIN to return on the
downstream path. The intent is to let a stateful device on the helper path see a
connection-closing signal before the original data is released. Whether that
changes classification or connection state depends on the topology and the
device; the trick does not guarantee that any middlebox will echo, accept, or
act on the FIN.
Despite the name fin-sni-delay-ms, this trick does not parse TLS, a
ClientHello, or SNI. It can trigger on any whole, non-fragmented IPv4 TCP packet
that has ACK, has at least one byte of TCP payload, and has none of SYN,
FIN, or RST. Flags such as PSH, ECE, CWR, and URG do not disqualify
an otherwise eligible packet. IPv6, IPv4 fragments, non-TCP packets, ACK-only
packets, and TCP Fast Open SYN data pass without starting this trick.
| Setting | Type | Default | Description |
|---|---|---|---|
smuggle-fin | boolean | false | Enables the trick. It is not a hostname or TLS selector. |
fin-sni-delay-ms | integer ms, >= 0 | 0 | Delay after consuming the exact downstream echoed `FIN |
fin-pause-timeout-ms | integer ms, > 0 | 1000 | Hard maximum pause measured from the triggering data packet. It releases the queue even if no exact echo arrives. |
real-fin-upstream-node | string | required when enabled | Distinct helper branch that receives the synthetic reverse `FIN |
The shared stateful-flow-limit setting also applies and defaults to 65536.
There is no setting for the mirrored FIN's TTL, flags, sequence numbers,
acknowledgement number, or checksum; those values are derived from the trigger.
The node must have a normal top-level next as well as the helper. The named
helper must exist, must not be this IpManipulator, must differ from next, and
must be reachable and available for binding when the chain is constructed.
{
"name": "ip-manipulator",
"type": "IpManipulator",
"settings": {
"smuggle-fin": true,
"fin-sni-delay-ms": 20,
"fin-pause-timeout-ms": 1000,
"real-fin-upstream-node": "fin-helper"
},
"next": "normal-path"
}
The numbers above are illustrative. In particular, choose the hard timeout from
the expected helper round trip, intended delay, and acceptable application
stall, rather than treating 1000 ms as universally suitable.
Trigger packet and mirrored FIN
Let the triggering packet be:
client A:a -> server B:b
SEQ = S, ACK = K, payload length = L
IpManipulator first retains that complete packet instead of forwarding it on
the normal next branch. It then copies the packet's IPv4 and TCP headers,
removes all TCP payload, swaps both IP addresses and both TCP ports, and emits:
server B:b -> client A:a
SEQ = K, ACK = S + L, flags = FIN|ACK, payload length = 0
Sequence arithmetic is TCP's 32-bit sequence arithmetic. The trigger cannot
contain SYN or FIN, so only its payload length advances the acknowledgement.
The copied header retains such fields as the trigger's IPv4 identification and
TTL and its TCP header length, options, and window. IPv4 total length is reduced
to the header-only length, the flags and sequence fields are replaced as shown,
and valid IPv4/TCP checksums are requested for egress. No setting changes these
fields independently.
The reverse numbers are chosen to look plausible for the same live connection:
K is the next server sequence the client said it expected, while S + L
acknowledges all bytes in the held client packet. The client itself sent only
the original data packet; it did not send this FIN. If the helper path actually
delivers the spoofed reverse packet to the client and the client's TCP stack
accepts it as in-window, the FIN can half-close the receive direction, advance
the expected server sequence by one, provoke an ACK, or otherwise disrupt the
connection. That endpoint effect is possible, not required by the trick.
The crafted packet is sent immediately through real-fin-upstream-node. It
does not traverse this node's upstream TCP-bit, stateful-SNI, SNI Blender, packet
duplication, or port-ghost stages. Protocol swap and final checksum handling can
still apply. The original data packet remains queued for the normal path and,
when released, continues through those later configured stages in their usual
order.
Echo recognition and release timing
For echo-confirmed release, the helper topology must return the crafted FIN as
a downstream packet through this IpManipulator. After downstream protocol and
port-ghost restoration, a packet matches only when all of these are true:
- it is a whole, non-fragmented IPv4 TCP packet in direction
B:b -> A:a - it has zero TCP payload
- its sequence is exactly
Kand its acknowledgement is exactlyS + L - after ignoring only
ECEandCWR, its flags are exactlyFIN|ACK
IPv4 identification, TTL, TCP window, and TCP options are not compared. A
normal TCP response to a FIN is normally an ACK with different direction and
sequence semantics, so it is not the required echo. Any additional flag other
than ECE or CWR also prevents a match.
The first exact echo is consumed and never forwarded toward the client. Exact
duplicates received while release is pending are consumed too. On the first
match, fin-sni-delay-ms schedules release of the queue. The hard
fin-pause-timeout-ms timer started when the trigger was captured remains
active, however, so the effective sequence is:
t0: trigger is queued; hard timeout starts; mirrored FIN is sent
te: exact echoed FIN is consumed, if one arrives
release: earlier of (te + fin-sni-delay-ms) and
(t0 + fin-pause-timeout-ms)
Consequently, the pause timeout must exceed the expected helper round trip plus
the desired post-echo delay if the full delay is important. A delay of 0
releases immediately on the owner worker. If no echo arrives, the hard timeout
still releases the flow. Failure to arm the initial hard timer releases the
queued trigger immediately before the helper FIN is sent; failure to arrange a
cross-worker post-echo release leaves the original hard timer as the fallback.
Timer callbacks are tied to the exact pause generation, so a stale callback
cannot release a newer use of the same tuple. Once release has completed, a
later packet matching the old FIN is ordinary downstream traffic and is no
longer consumed by Smuggle-FIN.
What is queued and how it is replayed
While the pause is active in the normal packet-chain orientation, every
parseable upstream packet on the forward tuple and every parseable downstream
packet on its reverse tuple is held, regardless of flags or payload. This
includes retransmitted data, ACKs, later application data, and real FIN or
RST packets. The trigger is the first queue entry; the exact synthetic-FIN
echo is the exception because it is consumed as the release signal. Unparseable
or fragmented packets bypass the queue and can therefore overtake held traffic.
Unrelated flows are never held behind this flow.
The queue holds at most 256 packets and has no separate byte limit. If a 257th packet arrives, or the queue cannot grow, the existing batch is forcibly released in queue order, the current packet follows normally, and the flow is marked confirmed so it is not paused again. Timeout and other forced-release paths have the same no-retry behavior.
One flow-owner worker maintains the queue. Reverse packets received on another
worker are copied to the owner worker before being queued. Replay order is the
order in which the owner worker accepted the entries, which need not equal a
single global wire-arrival order when multiple workers are involved. If a
cross-worker handoff cannot be created or submitted, that reverse packet passes
on its current worker while the pause remains active, so it can overtake the
queue. Only one flow may be paused by smuggle-fin on each worker at a time; a
new eligible packet for another flow on that worker passes normally.
Each queue entry remembers whether its checksum needed recalculation. On release, entries are replayed one at a time in the saved mixed-direction order:
- upstream packets restart at the beginning of the upstream
IpManipulatorpipeline; the now-confirmed Smuggle-FIN record prevents reinjection, and later upstream tricks and normal egress run once - downstream packets resume immediately after
smuggle-fin, because protocol and port-ghost restoration already ran before they were queued; later downstream stages run once
Smuggle-FIN does not split, overlap, rewrite, or reorder bytes within the original TCP packets. Its deliberate ordering change is temporal: the synthetic reverse FIN leaves before the held original data, and unrelated or bypassed traffic may pass during the pause. Holding both directions can add latency and can trigger retransmission timers, delayed-ACK behavior, congestion-window or receive-window changes, and application-visible stalls.
What each participant may see
- Client: sends its original data normally and sees none of WaterWall's queueing directly. It may receive the spoofed FIN if the helper path routes it there; accepting that FIN can disturb or close the connection as described above. It can retransmit held data when the pause approaches its RTO.
- Helper-path middlebox: can see a server-to-client
FIN|ACKwith sequence and acknowledgement values derived from current client data before that data appears on the normal branch. A device that accepts this packet for its flow state may mark one direction closed, discard later packets, change its parser state, or produce the exact downstream echo used by WaterWall. Stateless devices and devices that validate a different path, window, checksum, or flow history may simply ignore it. - Destination server: is normally intended to receive the original client
data later through
next, unchanged by Smuggle-FIN itself. The synthetic packet has the reverse tuple and uses the helper branch, but routing and branch design ultimately determine whether it reaches a real endpoint or server-side observer.
The technique therefore relies on a path-specific difference: an intermediate device must treat the early plausible FIN as meaningful while the actual end-to-end connection remains usable long enough for the delayed original data to succeed. TCP normalization, asymmetric routing, NAT state, SYN/FIN proxies, sequence-window validation, anti-spoofing, checksum offload or validation, and endpoint behavior can all invalidate that assumption.
Flow lifetime and failure behavior
After an exact echo or any forced release, the normalized four-tuple remains
confirmed and bypasses further Smuggle-FIN injection. Activity keeps a confirmed
record for up to 20 minutes of idle time. A same-orientation SYN, FIN, or
RST does not explicitly reset that record, so rapid reuse of exactly the same
tuple can continue to bypass the trick until it expires. In unusual chains where
an eligible reverse-oriented data packet is delivered through the upstream
callback, the sole canonical record can be reoriented; an active old queue is
then discarded rather than replayed.
Because the exact echo is identified only by tuple, flags, payload length,
sequence, and acknowledgement, a legitimate server FIN|ACK that happens to
match all those fields is indistinguishable and will be consumed. Conversely,
minor helper changes to sequence numbers or flags prevent confirmation and make
the flow wait for the hard timeout. Tunnel shutdown disposes of retained packets
rather than forwarding them during teardown.
stateful-flow-limit bounds the Smuggle-FIN table independently of the other
stateful tables. Admission pressure fails open, leaving the candidate packet on
the normal path without sending a mirrored FIN. Flow-affinity changes are
tolerated specially for reverse packets through owner-worker handoff, but
introduce the ordering qualifications above.
Differences and interactions
Unlike the TLS/SNI tricks, smuggle-fin neither locates nor changes a
ClientHello. Unlike smuggle-sni, it sends a newly constructed header-only FIN
through the helper branch and withholds the original data; smuggle-sni sends
captured real ClientHello packets through its helper. Unlike synfin-sni, it
does not generate a TLS record, fake SYN, same-direction close, or overlapping
TCP sequence ranges. Unlike packet duplication, its helper packet has the
reverse tuple, different flags and sequence fields, no payload, and a different
release schedule.
It may be configured alongside a stateful SNI trick, SNI Blender, TCP-bit rewrites, port ghost, protocol swap, or packet duplication when their own compatibility rules permit it. This does not mean the stages operate on the synthetic FIN: the helper FIN follows the bypass path described above, whereas the released original packets enter the ordinary pipeline. The added pause and replay timing can still change how those later stateful tricks classify a flow. Final MTU shaping may segment a released oversized TCP data packet even though Smuggle-FIN itself did not split it.
Stateful Flow Tables
IpManipulator establishes its own packet-flow ownership when first-sni,
smuggle-sni, overlap-sni, synfin-sni, ech-sni-trick, or smuggle-fin is
enabled. Native upstream TCP/UDP packets reach their flow worker before stateful
processing. Downstream packets are assigned after protocol and port restoration,
before the stateful continuation. These internal dispatcher boundaries need no
extra configured node and do not depend on RawSocket or TunDevice selecting the
flow owner. Stateless tricks do not add this requirement. Plain ProtoSwap keeps
both encoding and restoration on the current worker, including fragment output;
it changes protocol/checksums without selecting a flow owner or staging fragments.
Opaque protocols without a native TCP/UDP tuple also stay on their current worker.
Retained packets remain pinned to the worker that opened their capture, delay barrier, or held-packet group. Worker-mismatch fail-open checks remain in place; normal connection lines retain their original owner and are never migrated by the packet dispatcher. Smuggle-FIN retains its separate reverse-traffic owner handoff for an existing paused flow, subject to the ordering and handoff limits in its dedicated section.
| 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.
Each table uses a CSPRNG-generated per-tunnel hash seed, keeping bucket selection unpredictable from observed tuples.
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.
Every delayed release has a fail-open path: if its timer cannot be armed, held bytes are flushed immediately and in order instead of being stranded.
Overlap-SNI and SynFIN-SNI holds use separate per-flow fail-open timers keyed by the normalized tuple plus a nonzero hold generation. Timeout releases the held segment unchanged outside the shard lock. An upstream close releases either trick's live hold, and Overlap-SNI also releases it on a downstream close. If idle expiry or table teardown removes the record before its timer callback, the record destructor deliberately disposes of the held packet instead of forwarding from under the shard lock or during shutdown.
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 and real-FIN 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
sbufGetRecalculateChecksum(buf). - 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.
Node Metadata
Source-backed metadata:
| Property | Value |
|---|---|
| node flags | kNodeFlagNone |
can_have_prev | true |
can_have_next | true |
layer_group | kNodeLayer3 |
layer_group_prev_node | kNodeLayerAnything |
layer_group_next_node | kNodeLayerAnything |
required_padding_left | 0 bytes |