An AirDrop implementation for Windows, built from scratch by reverse-engineering the protocol. Learning project: understanding over shortcuts, no wrapping of existing tools.
| Milestone | State |
|---|---|
| 0. Capture Apple's Continuity BLE beacon | done — captured on iOS 26.6, contact-hash field confirmed |
| 1. Repo scaffold + transport seam | done |
2. Binary plist (bplist00) encode/decode |
done — differentially checked against Python plistlib |
| 3. Self-signed TLS + minimal HTTP/1.1 | done |
| 4. Discover → Ask → Upload state machine | done — verified against opendrop, an independent implementation |
5. DVZip compression + CPIO newc archive |
done — CPIO checked against bsdtar/libarchive |
| 6. Reaching an iPhone | working, slowly and intermittently — real iPhones on iOS 26.6, 27.0 and 27.0.1 have AirDropped files to WinDrop, arriving intact. Over Linux + OWL, at 12–27 KB/s and roughly one transfer in three in recent sessions, on the hardware tested. See where it stands |
279 tests, all passing.
AirDrop's discovery runs over AWDL, a second Wi-Fi link layer that time-slices the
radio against your normal AP connection on a schedule synchronised across every Apple
device in range. Windows' NDIS stack exposes no way to drive the 802.11 MAC that way,
and iOS implements no alternative — it has never supported Wi-Fi Direct, and binds
AirDrop's browser to awdl0 with no override.
This was argued structurally in ADR-001 and then confirmed by measurement: the captured beacon is 18 bytes, every one of them accounted for, with no field in which to name a channel, an address or a transport. The beacon cannot redirect a peer anywhere. It can only mean "wake AWDL".
So everything above the link layer is built, tested and working, and has been proven against a real iPhone. The link layer is what limits it, and that is a hardware question, not a software one.
An iPhone has sent files to WinDrop's receiver running on Linux, with
OWL providing awdl0. Photos arrived byte-for-byte
intact on iOS 26.6, 27.0 and 27.0.1: one image, several images in one share, and PNGs
whose upload mixed compressed and stored DVZip blocks. The log of every session is in
protocol-notes.md.
| iPhone → WinDrop | works, verified on real devices. A share arrives whole or not at all; a failed one leaves nothing behind |
| Reliability | about one transfer in three recently: 3 of 10 in session 10, none of 17 at another place in sessions 11–12, then 3 of 10 there in session 13. Every recent success finished within about 15 s, and whether small files fare better is the open question |
| WinDrop → iPhone | untested. The sender is proven only against opendrop |
| Speed | 12–27 KB/s in recent sessions on the one card tested (Intel AX211), 15–40 KB/s earlier. A 200 KB photo takes ~15 s, a 2.5 MB one ~100 s. Largest to arrive: 3.6 MB as one file, 6.4 MB as a four-photo share (267 s). ~25 MB fails |
| Phone setting | Everyone for 10 Minutes only. Contacts Only requires an Apple-issued identity, which a non-Apple device cannot hold |
| Windows alone | cannot reach an iPhone (see above). The radio has to be Linux: booted directly, or later a bridge |
The speed is the radio, not the protocol, and that is measured. The AX211 can only run plain monitor mode, not active monitor mode, so it never acknowledges the frames the phone sends it. The phone takes every frame as lost and sends it again until its retry limit. Six captures in session 10 all showed the same thing: 86% of the phone's frames to us were resends, and each frame went out about seven times. Nine sessions of software and configuration changes could not move the rate, because none of them could make this card acknowledge. The record of what was tried and why each failed is in the notes.
The fix is an adapter whose Linux driver supports active monitor mode, typically MediaTek
on the mt76 driver. bridge-hardware-setup.md has the
criterion and a one-line test for any card.
dotnet build
dotnet run --project src/WinDrop.App # the GUI
dotnet run --project src/WinDrop.Cli -- receive
dotnet run --project src/WinDrop.Cli -- send path\to\file.jpg
dotnet run --project src/WinDrop.Cli -- browse
receive takes --dir <path>, --yes (skip the consent prompt — it is the only real
security boundary in the protocol, so only for scripted testing) and --no-early-ask.
Early-ask is on by default. Over AWDL the /Ask request is slow, mostly the preview
image iOS attaches, and iOS gives up on it before a normal answer arrives, reporting a
decline. So the receiver answers /Ask first and asks the person afterwards. The upload
is not read until they say yes, so an unapproved sender gets no more than the /Ask body
it already had to send. A "no" is answered the moment the upload starts, without reading
any of it. --no-early-ask restores the classic order.
Who this reaches from Windows: another WinDrop instance, opendrop, or a Mac started
with defaults write com.apple.NetworkBrowser BrowseAllInterfaces -bool true.
Reaching an iPhone means running the receiver on Linux alongside OWL, which is how
every iPhone transfer so far was made. On Linux, awdl0 is an ordinary interface, so the
CLI works there unchanged. The runbook is bridge-hardware-setup.md.
Getting the hardware working is written up in
bridge-hardware-setup.md — including the finding that
WSL2 already ships vhci-hcd, cfg80211 and mac80211, so a USB adapter can be
passed into WSL and only one driver module needs building. No second machine.
When the hardware exists, --bridge <host> routes everything through a Linux box
running OWL:
dotnet run --project src\WinDrop.Cli -- receive --bridge 192.168.1.50
dotnet run --project src\WinDrop.Cli -- send --bridge 192.168.1.50 photo.jpgOn that box:
sudo ./tools/windrop-bridge.py --interface awdl0The bridge relays mDNS datagrams and pipes TCP. It parses no property lists, holds no keys and terminates no TLS — every protocol decision stays in the Windows process, so a compromised bridge still cannot read a transfer.
What is verified. Both directions of the relay, end to end, against the real daemon
run over an ordinary interface (--interface eth0), including an inbound transfer
driven by opendrop. Since settled by the field sessions: awdl0 behaves like an
ordinary interface to a socket, and a current iPhone does talk to a non-Apple peer, which
the ADR-001 addendum had doubted. Still not verified: the bridge itself over awdl0,
since every iPhone transfer so far used the CLI directly on the Linux machine. Also
whether OWL holds a link long enough for large files, which on this hardware it does not.
dotnet publish src\WinDrop.App -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true -o publishProduces a single publish\WinDrop.exe of about half a megabyte, which needs the .NET 8
runtime present. Pass --self-contained true instead to get a ~150 MB executable that
runs on a machine with no .NET installed.
The icon is generated rather than committed as an opaque binary:
.\tools\make-icon.ps1dotnet run --project src/WinDrop.Tools.ContinuitySniffer -- --seconds 30
dotnet run --project src/WinDrop.Tools.ContactHash -- --observed 1A2B,3C4D <identifier>
The sniffer captures Apple Continuity BLE advertisements to captures/*.jsonl; open a
share sheet on an iPhone and tap AirDrop while it runs. contact-hash tests whether a
contact identifier reproduces a hash prefix seen in a beacon — it runs entirely locally
and prints only digest prefixes, so an identifier never has to leave the machine.
Smart App Control (SAC) must be off on the development machine.
SAC allows binaries by cloud reputation. A freshly compiled assembly has no reputation, so it is blocked at load with:
System.IO.FileLoadException: ... An Application Control policy has blocked this file. (0x800711C7)
Every rebuild produces a new hash, so this recurs unpredictably — one build runs, the next does not. Check the current state with:
(Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy').VerifiedAndReputablePolicyState
# 0 = Off, 1 = Enforced, 2 = EvaluationTurn it off under: Settings > Privacy & security > Windows Security > App & browser control > Smart App Control settings > Off.
This is a one-way door. Windows does not allow SAC to be re-enabled afterwards without resetting or reinstalling Windows. Microsoft's own guidance is that SAC is not intended for machines used to build software. The alternatives are worse for this project specifically: code-signing with a certificate SAC already trusts needs a reputation-bearing commercial cert, and moving development into a VM cuts off the Bluetooth and Wi-Fi radio access that every milestone here depends on.
Ruled out as a workaround: driving the WinRT BLE APIs from Windows PowerShell 5.1 to
avoid compiling anything. PowerShell's in-box WinRT projection cannot subscribe to WinRT
events at all (Register-ObjectEvent fails with "Windows PowerShell cannot subscribe to
Windows RT events"), and BluetoothLEAdvertisementWatcher.Start() refuses to run
without a Received handler. Add-Type would compile an assembly and hit the same SAC
block.
Python 3.12 is also needed to regenerate the bplist test fixtures, but not to run the tests — the fixtures are committed.
src/WinDrop.Protocol/
Plist/ bplist00 reader and writer
Http/ minimal HTTP/1.1 client and server over one owned connection
Tls/ self-signed certificates and the no-validation TLS setup
Archive/ CPIO newc reader and writer
Compression/ DVZip chunked zlib, gzip fallback
Dns/ DNS wire format for mDNS
Discovery/ ITransport seam, mDNS, the infra-Wi-Fi and bridge transports
AirDrop*.cs the Discover/Ask/Upload state machine, both halves
PeerText.cs peer text made safe to show, and the rules for safe saved names
PreviewImage.cs which preview bytes may reach an image decoder
src/WinDrop.App/ WPF desktop app, AirDrop-styled
src/WinDrop.Cli/ send / receive / browse
src/WinDrop.Tools.ContinuitySniffer/ milestone 0 BLE capture
src/WinDrop.Tools.ContactHash/ local contact-hash probe
tools/windrop-bridge.py the Linux-side AWDL relay daemon
tools/owl-session.sh brings OWL up for a field session on Kali, and logs it
tools/retry-capture.sh counts the phone's resent frames during a transfer
tools/plist_oracle.py plistlib fixtures and differential oracle
docs/ ADRs and reverse-engineering notes
Layers are checked against implementations we did not write, because a codec can be wrong in a way that round-trips through itself perfectly:
-
bplist — fixtures generated by Python
plistlib, andplistlibreads back what our writer emits. -
CPIO — bsdtar/libarchive extracts our archives, and we read archives it produces, in both the newc and odc variants. (Trap:
tar --format cpiomeans odc, magic070707. We write newc,070701; use--format newcto compare against ours.) -
mDNS — python-zeroconf discovers and fully resolves our service: PTR, SRV, TXT and both address families (
tools/mdns_browse.py). -
The Continuity beacon — captured from a real iPhone, not taken from a write-up.
-
The full stack, both directions — real transfers to and from opendrop, an independent implementation of the same protocol, byte-identical each way. Sending exercises our writers; receiving exercises our readers, and found two real bugs. Together they cover the bplist writer and reader, cpio in both variants, gzip in both directions, the state machine on both sides, and TLS as client and as server.
-
DVZip and the receive path, against Apple itself — no third-party implementation of DVZip exists (opendrop only ever speaks gzip), so real iPhone uploads were the only possible check. They decoded intact, and they are how the stored-block flag was found.
opendrop is a reimplementation, so a shared misreading of Apple would pass it unnoticed. For receiving, an Apple device has now settled that. For sending it has not: nothing has yet been sent to an iPhone.
See CONTRIBUTING.md. Results from real Apple devices are especially welcome, working or not; there is an issue template for them. Report security problems privately, as SECURITY.md describes, not in a public issue.