Part one ended with a shopping list and a promise: the next post is what happens when that radio shows up. It showed up — an ALFA AWUS036AXML, the mt7921au, WiFi-6, two antennas — and this is that post. Same bench, same disclaimer: everything below is my own devices, my own network, my own accounts.
First, a correction I owe from last time. I spent most of part one cursing a cheap MT7612U for “not decoding data frames,” and I floated a driver regression as the cause. That was wrong. It wasn’t the driver — it was position. Data decode is gated by signal strength: with the laptop next to the sniffer I get thousands of data frames, across the room I get sixteen. Both chips behave identically. I blamed the silicon; it was the metre of air between them. Noted, and carried forward.
With that out of the way — the new idea.
Two vantage points
Part one drove a synthetic beacon down the wire and hunted for it on the air. This time I wanted a real app, and I wanted ground truth. So I captured the same traffic twice, simultaneously:
- The air — the mt7921au in monitor mode, seeing the encrypted 802.11 frames. This is the eavesdropper’s view.
- Inside the target — the Mac’s own
tcpdumpon its Wi-Fi interface, which natively decrypts its own traffic. This is the truth: exact server IPs, exact packet sizes, exact millisecond timestamps.
The event I picked was a Telegram call.
I have to be careful here, because Telegram already burned me once. Part one’s false positive was a Telegram push “beacon” — I thought I was lighting up a phone with nothing installed on it, and a wrong-code null control matched my “detection” almost exactly. It was a lag-search artifact and I retracted it. This is not that. I am not beaconing a phone blind. Telegram here is just a convenient way to produce a real, timestamped event: the Mac’s own capture tells me the precise instant the call’s packets arrived from Telegram’s servers. That’s ground truth I measure, not a number I hope for.
Two clocks, one fiducial
The awkward part of two independent captures is that they run on two independent clocks — the Linux box driving the monitor, and the Mac. NTP pulls them within a few milliseconds of each other, but “a few milliseconds” is exactly the slack a lag search needs to invent a correlation out of noise. I’ve paid that tuition already, so I don’t trust the raw timestamps.
Instead I fire a dense burst of pings from the Mac to the Linux box at both ends of the recording. That burst lands in both captures — as ICMP on the wire, as the Mac’s own uplink on the air — so it’s a shared fiducial I can align on, derived from something that isn’t the signal I’m testing. The offset came out 0.00 s. Both clocks were already honest; now I can lay the two timelines side by side, bin for bin, with no free knob to fudge.
The call, seen from the air
Here’s one real call, three panels, one time axis:

One real Telegram call. Top is the ground truth from inside the Mac; bottom is what the monitor heard over the air, knowing none of the keys. The dashed prediction — just ’every Mac packet makes a frame plus an ACK’ — lands almost on top of the measured air envelope. r = 0.79; the call is a 3–4× airtime bump; sliding the call window 20 s (the null) leaves ×0.7, i.e. nothing.
The bottom panel is the whole point: that purple envelope is reconstructed from frames I can’t decrypt, and it reproduces the Mac’s real traffic at r = 0.79. The call itself is a ×3.2–3.6 bump in the Mac’s airtime. And when I slide the call window twenty seconds sideways — the null control — the bump drops to ×0.7. There’s nothing there to find in the wrong place, which is how I know there’s something real in the right place.
Why does a call leak at all, when every payload byte is SRTP- and
MTProto-encrypted? Because every frame forces an acknowledgement. An 802.11
ACK for every data frame, at basic rate, always decodable. And Telegram stacks
its own application-level ack (msgs_ack) on top of TCP’s — so even a message
arriving makes the client transmit a small uplink reply that the monitor sees,
long after the incoming content itself has vanished into the undecodable
downlink. The words are invisible. The conversation’s shape is not.
What Telegram spells out with the lights off
Drop below the crypto to the IP layer — which, remember, is cleartext on the air by design, because the network has to route on it — and a Telegram call is almost chatty:
- The setup is a burst of STUN to Telegram’s call reflectors —
91.108.x, ports 1400 and 595–598. That pattern alone says “a Telegram call is starting.” - The media is either audio at ~50 packets/s or video at tens of Mbit/s. You can tell which from the shape without decoding a frame.
- A small text message is a 121-byte MTProto payload — a 187-byte frame —
the same size every time. Filter
frame.len == 187and you’re watching short messages tick by. - A pure TCP ACK is 66 bytes, and that one isn’t even Telegram-specific.
None of this was ever hidden, because none of it was ever encrypted: 802.11 addresses, frame lengths, timing, sequence numbers — all in the clear, so the AP and the switches can do their jobs. WPA2 wraps the body. The envelope is public.
There’s one asymmetry that runs through the entire project, and it’s worth stating plainly: what you send leaks; what you receive mostly doesn’t. Your uplink decodes when you’re near the sniffer. The downlink — what’s sent to you — is a different animal.
The incoming call, and the “be the loudest thing” rule
Second run: a friend called me (incoming), and I deliberately left a video stream playing in the background to see what a busy device does to the signal.

An incoming call under a heavy background stream. The air tracks the Mac’s total airtime at r = 0.93 — but while the stream ran, the call and the stream were both just ‘Mac airtime,’ indistinguishable. The instant I switched on video and the stream happened to dip (dotted line), the call became the loudest thing the Mac was doing, and the air jumped with it.
The air reproduced the Mac’s total airtime at r = 0.93 — better than the first call, simply because there was more traffic to track. But the call, the smaller event, was swallowed by the stream: from the air, “call” and “stream” are both just the Mac using the channel, and you can’t separate them by airtime alone. The moment I turned on my camera and the stream paused, the call vaulted to the top of the Mac’s airtime and the air followed it exactly.
That’s the rule that keeps reappearing: the target has to dominate its own airtime for a specific event to stand out. A busy device hides the small stuff. MAC randomization doesn’t save it — I attribute frames by signal strength and timing, not the (randomized) address — but a device that’s drowning itself in other traffic does.
One tangent I couldn’t resist: that background “YouTube” stream resolved not to Google, but to an address block belonging to my own ISP. It was YouTube — served from a Google cache node living inside the ISP’s network. From the air you can’t even tell the content came “from Google”; at the IP layer it wears the ISP’s coat. It changes nothing about the airtime, but it’s a small reminder of how little the network layer actually tells you about what you’re looking at.
The wall, still standing: downlink data
The thing part one was stuck on is the thing I’m still stuck on. I can watch a call happen, I can time it, I can shape it — but I still cannot read the data frames flowing to the device. The numbers from these captures: the monitor decoded ~16,000 uplink data frames and about 320 downlink ones — roughly 2%.
So it isn’t zero. Those 2% are the moments the AP transmitted single-stream and low-rate: a weak client, a retransmit, a lull. The other 98% are two spatial streams, beamformed — radio energy shaped to constructively land on the target’s antennas, and nowhere else.
I spent a while convinced there had to be a shortcut, and there isn’t, for a reason that’s worth writing down because it surprised me. The beamforming steering matrix — the “V-matrix” — is not a secret you brute-force. It’s transmitted in the clear anyway, and you don’t even need it: the precoded preamble hands every receiver the effective channel for free. The wall is plain linear algebra. Two streams overlapping in the same time and frequency are two unknowns per symbol, and a one-antenna radio has one equation per symbol. Underdetermined. No key, no cleverness, no captured V-matrix fixes having too few antennas. You need a real two-antenna MIMO receiver — an SDR, not a monitor dongle — or you get the 2%.
And no, you can’t cheat by telling the adapter to wear the target’s MAC address. Beamforming isn’t aimed by the address in the frame; it’s an active sound-and-feedback loop with an associated client. Spoof the address passively and the AP still steers at the real device. Associate for real and you become a second, separate session with your own keys — you’ve replaced the target, not overheard it. The downlink is a private point-to-point beam. You either stand in it (be the device) or rebuild it from enough antennas (be a MIMO radio). There’s no third door.
A clean negative: the ICMP trick
One more, and I’m including it for the same reason part one included its false positive: the null controls are the part worth writing down.
The idea was pretty. Poke a closed UDP port on the target and its operating system fires back an ICMP “port unreachable” — a reply the target transmits, on the uplink, decodable, with nothing installed on it. So code a pattern of pokes and read the pattern back from the target’s ICMP replies on the air. A free, built-in beacon.
On the wire it was flawless: 29 replies per ON-bit, 0 per OFF-bit. On the air it failed — and the null control said so out loud: 176 of 300 wrong codes scored as high as the real one. Two reasons, both fatal. The laptop sat at −60 dBm, too weak for the monitor to decode its small uplink ICMP frames — near zero of them made it through. And the OS rate-limits ICMP errors to about 10 per second, so even up close it’s a trickle against the ambient roar of control frames. It’s strictly worse than just flooding the target and reading the rhythm of its forced ACKs — those are basic-rate and always decode. A dead end. But a null-validated dead end, which is the only kind I trust.
The honest ledger
- Held: the two-vantage method. A real Telegram call is detectable from the encrypted air alone — r = 0.8–0.9, a 3–4× airtime bump, and a wrong-window null that stays dark. Content unreadable the entire time.
- Held: the fingerprint. Call vs message vs idle, and roughly audio vs video, are legible at the IP layer with zero decryption — and most of the giveaway (addresses, sizes, timing) is cleartext by design.
- Held: the eavesdrop asymmetry, now quantified. What you send leaks on the uplink; what you receive is 98% dark on the beamformed downlink.
- Didn’t: downlink content and size. Still ~2%. Needs a genuine MIMO SDR, not a monitor adapter. That’s the next shopping list.
- Didn’t: the one-way ICMP channel. Null-validated failure — rate-limited and too quiet to decode at range.
The through-line since part one hasn’t changed: the shape survives the encryption, and the honest limits are the interesting part. What I can see from outside is that you’re on a call, when, and roughly what kind — and none of that requires a key, because none of it was ever encrypted. What I still can’t see is anything the network sends to you, and the reason isn’t cryptography, it’s antennas.
A residential proxy hides 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 shape of the flow crosses every layer of the disguise — up to the exact wall where the physics, not the math, decides what leaks. That wall is a two-channel radio away. The next post is what happens when it arrives.