This is part 2 of a series of posts:
Last time we broke down the problem of implementing AirDrop so that we can support sending and receiving files from devices that do not natively support AirDrop. After some to and fro we landed on an implementation that involves creating a “proxy” running on an Apple device (which supports AWDL natively) or Linux with OWL.
I’ve decided to structure all of this as a “Core” project that’ll contain the bulk of the AirDrop implementation. This will be able to be plugged into any .NET Core application in order to provide AirDrop services and each application can provide an implementation of an interface in order to handle file sending and receiving. Our project hierarchy will look like this:
AirDropAnywhere Project Structure
AirDropAnywhere.Core will contain all our shared services (e.g. mDNS advertisements, core AirDrop HTTP API) and the means to configure them in a
AirDropAnywhere.Cli will be a CLI application hosting the services and rendered using Spectre.Console. It’ll typically be used as a way to send / receive for the current machine via the command line.
Individual devices will connect to an application hosted by
AirDropAnywhere.Web. I’ve gone back and forth on this a little - I thought about implementing the bits that allow a device to send / receive content via the “proxy” as a GRPC-streaming service and implementing a UI using MAUI. This satisfies the need of being cross-platform, but requires each device to have a copy of the application installed locally. I could use gRPC-Web and hook everything into Blazor or some client-side JS but it feels like a fair bit of boilerplate shennanigans that is elegantly solved by SignalR.
To create the project I’m using a combination of JetBrains Rider and VS Code all running on an ancient 2013 MacBook Pro. I’m using Rider because I’ve become fond of the speed and refactoring wizardry of the JetBrains toolset and VS Code so that I can remote debug on a Raspberry Pi over SSH (more on that later).
To allow us to inspect the mDNS / DNS-SD services we’re publishing I’ve made use of an app called Discovery DNS-SD Browser. This was previously called Bonjour Browser. It gives us a tree view of the mDNS services available to us. Additionally the
dns-sd utility is very useful.
It’s also incredibly useful to be able to inspect network traffic with some of the things we’re using here (particularly mDNS) so I’ve made copious use of Wireshark to make sure the application does the right thing over the wire by monitoring the
awdl0 interface on the Mac and using remote SSH packet capture for doing the same on the Raspberry Pi.
Building the system
The folks at the Secure Mobile Networking Lab have a really great diagram detailing the lifecycle of an AirDrop interaction by a user and how it translates to network requests on page 4 of their paper A Billion Open Interfaces for Eve and Mallory: MitM, DoS, and Tracking Attacks on iOS and macOS Through Apple Wireless Direct Link:
AirDrop Protocol Interactions from the OWL project
We don’t need to handle any of the Bluetooth interactions, but we do need to be able to handle mDNS requests and the HTTPS services for AirDrop. To help visualize how I anticipate this working I drew a simple diagram of the system and how it interacts with the various devices. As we work through implementation I’ll refer back to this diagram and refine parts of it as the problems we face become clear!
AirDrop Anywhere Architecture
We’ll start by implementing the two components that allow us to communicate with devices using AirDrop - mDNS and the AirDrop HTTP API. To do so we’ll spin up a .NET Core
WebHost and configure the endpoints needed for the HTTP API and an
IHostedService that will manage the lifetime of our mDNS service. Off we go!
To allow AirDrop-compatible devices to find our implementation of AirDrop we need to advertise our service and its related SRV and TXT records over mDNS. Richard Schneider’s mDNS implementation looks like it would fit the bill here, but, after implementing the relevant pieces in
AirDropAnywhere.Core, I found that Wireshark was not picking up mDNS responses from the service to queries sent from my iOS-based devices over the
Further investigation uncovered this post from a project called yggdrasil that creates a mesh of devices to provide an end-to-end encrypted IPv6 network. That post details how they enabled meshing across the AWDL interface in Apple devices and there are two things that need to happen for a socket to be able to communicate over the
- OS needs to “wake-up” AWDL. Typically this is handled by the
NSNetServiceBrowserclass when the
includesPeerToPeerproperty is enabled. Unfortunately it’s a pain to call this from C# - -
Xamarin.MacOSprovides a C# implementation but that’s currently tied to Mono (.NET 6 will likely address this).
yggdrasiluses a small snippet of ObjC and uses it to wake up the
I’ve taken a similar approach -
libnative.m contains code similar to above. I’ve added a
BeforeBuild target in
AirDropAnywhere.Core.csproj that runs
clang to compile
libnative.so and then used P/Invoke to execute the
StopAWDLBrowsing functions. This instructs the OS to wake-up AWDL so we can receive traffic on the
- Next we need to allow our sockets to talk using AWDL - in MacOS we need to configure some socket options that notify the OS that we want to do be able to use
awdl0. That means calling the following on whatever sockets need to do so:
This works great if I control the socket, but
net-mdns handles all of that for us. In an attempt to get this to work I hacked some reflection together to get at the
Socket instances used by
net-mdns and call
SetAwdlSocketOption on them. It compiled, it ran, it didn’t throw exceptions, but still nothing from our service in Wireshark - I see traffic coming from my iPhone over
awdl0 - so our ObjC code executes and wakes up the interface, but
net-mdns does not respond to the packets. Sigh.
After debugging through the
net-mdns code base I found a couple of empty
catch blocks that were hiding some socket-binding issues. Binding UDP-based sockets to
awdl0 is a little painful and some of the binds and socket options used by
net-mdns were causing it to throw and not actually use the
awdl0 interface at all! I’ve gone back and forth whether it’s worth fixing the issues in
net-mdns or whether it’s simpler to take some of its approaches and implement something a little more specific for this scenario and landed on implementing the code I need directly in
AirDropAnywhere.Core. There are some things that just don’t make sense in a general mDNS implementation.
As a result I’ve taken key parts of
net-mdns, re-used the excellent net-dns library for handling the various DNS record types that are needed for mDNS and implemented a
MulticastDnsServer that addresses the specific needs of binding to the
- removal of service discovery and querying. We only need to advertise
_airdrop._tcpwith the relevant SRV, PTR and TXT records so this implementation focuses on the advertisement bits of mDNS
- it handles setting the right socket options for AWDL
- mDNS multicast responses are sent over the same interface that the request was received on - something which AWDL is particularly sensitive to.
- it’s async throughout and uses
net-mdnshad a number of sync-over-async patterns in the code that could cause deadlocks.
And the result is…. it works!
Wireshark mDNS Traffic
Here we can see traffic to the IPv6 multicast
ff02:fb address from my iPhone on the
awdl0 interface. The row underlined in red is our call to
StartAWDLBrowsing - this is what initiates AWDL on my Macbook. Next, the rows underlined in orange are an mDNS query for
_airdrop._tcp from my iPhone, followed by an mDNS response from my Macbook answering the query.
And to confirm that all the relevant DNS records are working using
You can find all the code for the mDNS implementation here. There’s still some rough edges (I’ve done little around TTLs and there are no tests yet) but they’ll get tidied up as we go.
This post is already getting pretty lengthy so I’ll wrap it up for now. Next time we’ll go into implementing the HTTP API for AirDrop. This should be relatively simple - the OpenDrop and PrivateDrop projects have implementations in Python and Swift that we can use as a basis for a Kestrel-based implementation.