As we know, AirDrop is Apple’s proprietary protocol.
However, it is possible to use AirDrop on non-Apple devices and enable those devices to work with Apple devices.

Of course, there are some prerequisites and limits.
I’m specifically using Android as an example in this article.

At the time of writing, I haven’t finished the tool’s development.

https://github.com/I-Info/AnyDrop

Background

References to papers published by Secure Mobile Networking Lab, we can fully understand how AirDrop works:

flow

The AirDrop’s web service runs at application layer. And AWDL operates at the data link layer and the physical layer.

Besides, I found that on macOS, the receiver registers a mDNS service not only on the AWDL interface (awdl0) but also on all network interfaces (lo0, en0) after discovering a BLE advertisement.

On macOS we can enable the capability to browse all interfaces. With it on, sharing via AirDrop will discover devices on non-AWDL interfaces.

Although the AWDL protocol is very complicated and difficult to implement, AirDrop does not absolutely rely on AWDL on macOS. So we can replace AWDL with any other Ethernet without looking into it.

Limitation

  1. Mac-only
  2. AirDrop in Everyone mode
  3. LAN-only
  4. Only if BrowseAllInterfaces is turned on and Ethernet (non-WiFi) is connected, sharing via AirDrop will be able to discover devices on the LAN.

Note: even though condition 4 is not fulfilled, other devices can AirDrop to the Mac

Implementation

Just some samples

Enable BrowseAllInterfaces

Run the following command:

1
defaults write com.apple.NetworkBrowser BrowseAllInterfaces 1

Meanwhile, you can turn it off by:

1
defaults write com.apple.NetworkBrowser BrowseAllInterfaces 0

BLE Advertise

Use a Bluetooth LE Advertisement to activate Mac’s AirDrop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package com.i1nfo.anydrop

import android.Manifest
import android.bluetooth.BluetoothManager
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.bluetooth.le.BluetoothLeAdvertiser
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat

class BleController(private val context: Context) {
private val bleLeAdvertiser: BluetoothLeAdvertiser

private val data = byteArrayOf(
0x05, // Subtype
18, // Length
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x01, // Start
0x00, 0x00, // ID 1
0x00, 0x00, // ID 2
0x00, 0x00, // ID 3
0x00, 0x00, // ID 4
0x00 // End
)

private val callback = object : AdvertiseCallback() {
override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
super.onStartSuccess(settingsInEffect)
Log.d("BleController", "onStartSuccess")
}

override fun onStartFailure(errorCode: Int) {
super.onStartFailure(errorCode)
Log.d("BleController", "onStartFailure")
}
}

init {
val manager = context.getSystemService(BluetoothManager::class.java)
?: throw Exception("BluetoothManager not found")
val adapter = manager.adapter ?: throw Exception("BluetoothAdapter not found")
bleLeAdvertiser =
adapter.bluetoothLeAdvertiser ?: throw Exception("BluetoothLeAdvertiser not found")
}

private fun checkPermission(): Boolean {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.BLUETOOTH_ADVERTISE
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
context as MainActivity,
arrayOf(Manifest.permission.BLUETOOTH_ADVERTISE),
0
)
return false
}
} else if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.BLUETOOTH_ADMIN
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
context as MainActivity,
arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
),
0
)
return false
}
return true
}

fun startAdvertising() {
val settings = AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
.setConnectable(false)
.setTimeout(0)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
.build()

// Apple's manufacturer ID: 0x004C
val data = AdvertiseData.Builder()
.addManufacturerData(0x004C, data)
.build()

if (checkPermission()) {
Log.d("BleController", "Permission granted")
} else {
Log.d("BleController", "Permission not granted")
return
}

bleLeAdvertiser.startAdvertising(
settings,
data,
callback
)
}
}

Discover AirDrop services by mDNS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.i1nfo.anydrop

import android.util.Log
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.net.NetworkInterface
import javax.jmdns.JmDNS
import javax.jmdns.ServiceEvent
import javax.jmdns.ServiceListener

class MDNSController {
private lateinit var mDNSs: List<JmDNS>
private val addresses = mutableListOf<InetAddress>()
private val ifNames = arrayOf("wlan0", "wlan1")

private val listener = object : ServiceListener {
override fun serviceAdded(event: ServiceEvent?) {
Log.d("JmDNS", "added: $event")
}

override fun serviceRemoved(event: ServiceEvent?) {
Log.d("JmDNS", "removed: $event")
}

override fun serviceResolved(event: ServiceEvent?) {
Log.d("JmDNS", "resolved: $event")
}

}

init {
val interfaces = mutableListOf<NetworkInterface>()
for (ifName in ifNames) {
val networkInterface = NetworkInterface.getByName(ifName)
if (networkInterface != null) {
interfaces.add(networkInterface)
}
}

for (networkInterface in interfaces) {
var if4: Inet4Address? = null
var if6: Inet6Address? = null
for (address in networkInterface.inetAddresses) {
if (address.isLoopbackAddress) {
continue
}
if (address is Inet4Address) {
if4 = address
} else if (address is Inet6Address) {
if6 = address
}
}
addresses.add(if6 ?: if4 ?: continue)
}

Log.d("JmDNS", "addresses: $addresses")
}

// Start JmDNS instances, run in non-UI thread
fun startDiscover() {
// Each InetAddress create a JmDNS instance
mDNSs = addresses.map {
JmDNS.create(it).apply {
addServiceListener("_airdrop._tcp.local.", listener)
}
}
Log.v("JmDNS", "mDNSs: $mDNSs")
}

}

Send HTTP Requests

Send request to AirDrop files.

TODO

Register a AirDrop service

Register a mDNS service and create a HTTP server to receive files.

TODO

References

  1. Paper: Milan Stute, Sashank Narain, Alex Mariotto, Alexander Heinrich, David Kreitschmann, Guevara Noubir, and Matthias Hollick. A Billion Open Interfaces for Eve and Mallory: MitM, DoS, and Tracking Attacks on iOS and macOS Through Apple Wireless Direct Link. 28th USENIX Security Symposium (USENIX Security ’19), August 14–16, 2019, Santa Clara, CA, USA. Link
  2. Blog: https://bakedbean.org.uk/tags/airdrop/
  3. Code: https://github.com/seemoo-lab/opendrop