This one is pure curiosity — a weekend project with no product behind it. I wanted to see, with my own hands, whether a claim I keep repeating in my day job is actually true: that encryption hides content, not shape, and that a flow’s byte sizes and timing survive TLS, WPA2, NAT, and a proxy hop all the way onto the air.
It’s a fun thing to say. It’s a different thing to sit at a bench, drive a pattern through a doubly-encrypted link, and watch it come out the other side in radio frames you can’t decrypt. So I did — and then spent most of the weekend not on the physics, which worked on the first honest try, but on the far more stubborn problem of a monitor radio that refused to show me half the frames. Everything below is on my own devices, my own network, my own accounts.
The thesis, concretely
Take a connection that’s encrypted twice: a TLS tunnel inside an 802.11 link secured with WPA2. Two properties leak straight through both layers:
- Burst size. CCMP (WPA2’s cipher) is a stream mode — ciphertext length equals plaintext length plus a fixed header and MIC. A 100-byte write is a bigger frame than a 20-byte write, encrypted or not. TLS adds a constant record overhead; a proxy relays byte-for-byte.
- Timing. The cadence of when bursts leave the far end survives the whole path. Latency adds jitter; it doesn’t erase rhythm.
So if a server emits a recognizable rhythm of sizes and gaps, that rhythm should reappear in the WiFi frames of whatever device pulls the flow — no key, no decryption, just a passive sniffer.
I encoded a Morse-like code into a plain TCP flow: a long burst for 1, a short
one for 0, one fixed time unit apart, with a five-mark preamble as a sync
marker. The bytes are filler. Only the envelope matters.
preamble payload = 1 1 1 0 1 0 0 1 0 1 1 0 1 0
█ █ █ █ █ · █ █ █ ▂ █ ▂ ▂ █ ▂ █ █ ▂ █ ▂
└ 5 long ┘ █ long burst = 1 ▂ short burst = 0
What held up (r = 0.7–0.9)
The core result was unambiguous. Driving the code 11101001011010 through the
target and correlating the air capture against it:
| Trigger on the target | r (correlation) | ON/OFF contrast |
|---|---|---|
| ICMP ping, paced | 0.89 | ×42 |
| UDP echo | 0.89 | ×72 |
| TCP SYN → RST (closed port) | 0.92 | ×19 |
The shape provably survives onto the air. The cleanest trigger turned out to be TCP SYN → RST: a closed port makes the target’s own kernel emit a reset per SYN, so the rhythm rides its uplink with nothing installed and no open port. (One trigger died: UDP → ICMP port-unreachable, because the OS rate-limits ICMP errors down into the noise. Worth knowing which primitives the kernel throttles — that one dropped to r ≈ −0.1, pure noise.)

One real capture. Orange (top) is the code I pushed down the wire; blue (bottom) is what the sniffer heard over the air. The highlighted ON-bits line up with the bursts — the shape crossed the encryption. r = 0.70, ON/OFF contrast 17×.
The monitor was the hard part
Nobody warns you that the physics is the easy part. Most of the weekend went to fighting my own radio, which kept dropping the frames I needed.
I sniffed with a MediaTek MT7612U — a common, cheap USB monitor adapter, WiFi 5. It could not demodulate the high-rate downstream data frames. Not “sometimes.” In a three-second capture under a full flood it decoded on the order of sixteen data frames — from every station on the channel combined, not just my target. What it caught by the thousand were the low-rate control frames: block-acks, CTS, the ACKs the AP fires back at low, robust rates.
I did the obvious things. Reloaded the driver. Forced HT20, then HT40. Checked for stray monitor flags. None of it moved the data-frame count off the floor. This is a known failure mode — a monitor stuck effectively in “no-HT” mode silently drops every HT/VHT data frame while happily logging the legacy control traffic — and on a bleeding-edge kernel the driver simply would not come out of it.
So I did the only thing left: I correlated on what I could see. The control frames the AP sends back toward the target are triggered almost 1:1 by the target’s own activity — every uplink burst earns a block-ack, every transmit opportunity a CTS. Ride those and you’re reading the acknowledgements of a flow you can’t decode at all. It worked (that’s the r=0.70 figure above), but with a caveat I want to be honest about: those frames are transmitted by the AP, so their signal strength points at the router, not the target. The rhythm is the target’s; the RSSI is the AP’s.
(And a smaller, dumber problem, because a lab notebook should include the dumb
ones: my first pass at counting frames was pure fiction, because I let awk
split on whitespace and control frames have no transmitter-address field — the
empty column collapsed and every row shifted one to the left. Half an hour of
confidently wrong numbers before I switched to splitting on tabs. The measurement
lies in more ways than one.)
There’s a deeper asymmetry underneath all this, and it’s the genuinely interesting finding. A sniffer sitting next to the target hears the target’s uplink almost perfectly — single stream, robust low rate. But the downlink from the AP is rate-adapted and beamformed toward the target’s antennas, at a high MCS. A monitor a few centimetres away, at different antennas and different multipath, can’t separate it — so it drops the very frames carrying the content. You are trying to decode a signal that was physically shaped for someone else’s antennas. The better the victim’s link, the more the eavesdropper loses. And this holds no matter what gear I use. It’s baked into how the channel works.
Which brings me to the one experiment I most wanted to run and couldn’t. The cleanest, most hands-off version of this attack is a plain one-way UDP flood at the target — you don’t even need it to answer. And the pattern is radiated: a one-way flood arrives as downlink data frames addressed to the target’s MAC, in exactly the rhythm you sent. It’s right there in the air, attributable, no cooperation required. My radio just can’t demodulate it. The signal exists; the receiver doesn’t. So that’s where it stalls, and it stalls on hardware, not on the physics. My radio just can’t hear it yet.
Teaching a network to recognize the rhythm
Cross-correlation is fine at high SNR and brittle at low. So for fun — and because the whole point of a bench project is to overdo it — I trained a small siamese 1D-CNN with an InfoNCE contrastive loss and heavy noise augmentation. A learned correlator instead of a hand-rolled one: one encoder eats the wire code, the other eats the air envelope, and the network learns to pull matching pairs together and shove mismatched ones apart.
The trick to not fooling myself was to never let it see the real data during training. I built a generator that produces synthetic wire/air pairs from first principles — the OOK code, the control-frame bursts, timing jitter, dropped bits, background traffic, a clock offset between the two capture machines — and seeded its parameters from the summary statistics of the one real run. Seeded well enough that the synthetic reproduced the real capture at r=0.69 against the real 0.70, same ON/OFF contrast, same RSSI spread. Then I trained only on synthetic and tested against the real capture the network had never seen. (All of it on a spare box with a GPU; the model is tiny and the data is generated on the fly, so “training” is a couple of minutes.)
- Retrieval: given the wire code, find its air capture among 128 — top-1 96% at low noise, and it beat classical cross-correlation by 5–7 points as noise rose. The augmentation earned its keep exactly where the hand-rolled method falls apart.
- Sim2real: the real capture, never seen in training, retrieved at rank #1 of 301 in both directions — wire finds its air, air finds its wire.
- Detection, the one I actually care about: AUC for beacon-present vs beacon-absent was 0.999. At a threshold tuned to a 1% false-alarm rate it flagged real beacons 97.8% of the time. And a real empty channel — a capture with no beacon at all — scored well below the threshold and was correctly rejected. The correlator does not hallucinate a signal into silence.
That last property is the whole ballgame, and it’s easy to prove to yourself visually. Here’s the detector across three back-to-back captures on one time axis: an empty channel, then beacon A, then a different code B.

Three real captures, one time axis: nothing, then beacon A, then a different code B. The envelope is flat when there’s no beacon, and traces whichever code I drive — including telling two codes apart. This is the detector not hallucinating.
So the machinery was sound and noise-robust. Which, if you’ve done any of this, is exactly the state of mind that gets you into trouble.
The part where I fooled myself
The moment your tool is good, you start pushing it at problems it isn’t good enough for — and it will happily tell you it succeeded.
I went after the hard version: a much fainter, more indirect way to nudge the target’s airtime, and finding the device among a crowd of stations that all randomize their MACs. The signal got faint. To line things up I added a lag search — slide the code against each candidate station until the correlation peaks — and up popped a station at r ≈ 0.41 with a plausible delivery lag. It looked like a hit. I was drafting the paragraph.
Then I ran the control, and it fell apart:
- Re-correlating the same air data against a deliberately wrong code produced the same ~0.42 for the top stations. A wrong code scoring as well as the right one means the correlation isn’t real.
- With the lag search constrained to physically sane values, the “winning” station became a device that wasn’t even part of the experiment, at a negative lag — airtime arriving before the code that supposedly caused it. Impossible.
The ~0.41 was an artifact. A lag search over a short code in noisy, multi-station traffic will manufacture a ~0.4 peak out of nothing if you let it — it has enough degrees of freedom to find some alignment that looks convincing. Contrast that with the IP floods above: r = 0.7–0.9, contrast ×17–84, surviving any null control you throw at them, at zero lag, on the station you expected. The difference is signal-to-noise, and no amount of clever search makes bandwidth appear that isn’t there.
So I threw the faint-signal result away. My initial gut (“this’ll drown in noise”) was right, and I’d briefly talked myself out of it because the number looked good. The lesson is cheap to state and expensive to learn: never trust a lag-searched correlation without a wrong-code control. It’s now the first thing I run, not the last. The r=0.41 that turned out to be nothing taught me more than the clean r=0.9 did.
The honest ledger
What this is, and what it isn’t:
- Proved: shape (size + timing) survives TLS + WPA2 onto the air; a driven pattern reappears in a device’s frames at r = 0.7–0.9; a zero-install trigger (SYN→RST) works; the learned correlator generalizes sim2real and rejects empty channels.
- The measurement is fragile, and the chipset is why. The physics is robust; my receiver is not. A cheap WiFi-5 monitor sees the acknowledgements but not the content, and can’t touch the one-way downlink case at all. Every number here is bottlenecked by hardware I’m about to replace.
- A proximity primitive, not a map. Even where it works, this needs a passive radio in range — tens of metres. It locates a device you’re already near; it does not find anyone from a desk, and driving a beacon through a proxy lights up whichever exit you were routed to, not one you chose. The physics works; the targeting doesn’t.
- Scope: own devices, own network, own accounts, passive capture of frame size and timing only. Measuring the shape of your own traffic is one thing; anything that touches a device you don’t own is another, and not part of this.
Where I stopped: a shopping list
So this is where the weekend actually ended — not on a grand conclusion, but on a
parts order. The physics is proven and the correlator works; the thing standing
between me and the experiments I couldn’t run — the one-way downlink, decoding
an ax phone, seeing content instead of just acknowledgements — is the receiver.
So the next move is hardware, and I did the homework so I don’t buy the wrong
thing twice.
- A WiFi-6 monitor, first. A MediaTek mt7921au adapter (the ALFA
AWUS036AXML is the easy one to find) demodulates HT/VHT/HE data frames — the
exact thing my MT7612U couldn’t. That’s what lets me finally see the one-way
downlink
→ targetframes directly, and target modernaxdevices instead of being blind to them. This one upgrade unblocks the entire stuck half of the project. The caveat I’m carrying forward: verify it actually decodes data frames before trusting it — flood the target, count the data frames, make sure it’s thousands and not sixteen. I got burned once. - An SDR, second, for the other half. For raw energy detection of the airtime pattern (no decode needed — just watch the channel’s power trace the code) and for transmitting, a HackRF covers 2.4 GHz and up. Worth stating the mistake I nearly made: a plain RTL-SDR does not work here — it tops out around 1.7 GHz and can’t even tune to WiFi’s 2.4 GHz. And if I later want to crack the beamformed two-stream downlink that defeats a single-antenna receiver, that’s a two-channel radio (BladeRF / USRP) — a different, more expensive weekend, and I’m not there yet.
That’s the honest stopping point. So the physics is proven and the correlator’s trained and validated, false positive included. All that’s left is a receiver that can actually see the data frames, and that’s on order. The next post is what happens when that radio shows up.
Why a proxy person finds this interesting
A residential proxy is an application-layer disguise: it changes the IP the server sees. It does nothing about the fact that the exit is a physical radio on a physical link, and that the flow’s shape crosses every layer of the disguise. It’s the same principle as the rest of my work — a TCP fingerprint that contradicts the browser’s claimed OS, a STUN allocation that leaks the real IP. You can spoof the one field you control. You leak at every layer you don’t.
But mostly I did this because it was a good puzzle — and the two things I’m taking away from it are that the shape really does survive, and that my own null control caught me before I published a number that wasn’t there. Those are the ones worth writing down.