I implemented a way to automatically create Bluetooth Classic sockets between two nearby Android devices running the same app. This method works continuously in the background without wakelocks, and it does not require pairing, root, or any user interaction.
The video1 is of two physical phones, in this case my Moto G4 Play (left) and original Google Pixel (right), automatically connecting and syncing. The G4 is in airplane mode with Bluetooth on. These phones didn’t start paired and never became paired. Neither phone is rooted.
Motivation
A while ago, I was thinking about how fragile the infrastructure supporting long-distance wireless communication is:
- Cell phone towers and the servers implementing chat services are single points of failure.
- Natural disasters can easily wipe out infrastructure. In Puerto Rico, it took months to restore cell phone service after Hurricane Maria.
- Cell phone service tends to be unreliable in sparsely populated areas.
- At concerts, conventions, protests, or similar situations, cell phone towers can be overloaded making it difficult to get packets through.
- In China, North Korea, Turkey, and anywhere else that freedom of speech is not respected, existing communications infrastructure is monitored and censored.
With these concerns in mind, I began working on Noise, a peer-to-peer messaging app that works over long distances using epidemic routing. Epidemic routing works best when nearby devices are able to connect automatically, which I’m able to do by using a Bluetooth LE beacon to bootstrap a Bluetooth Classic socket. In Noise, Bluetooth connection management is handled entirely in the BluetoothSyncService
class.
Discovery using Bluetooth LE
Like the name implies, Bluetooth Low Energy is a standard designed to reduce the power needed to transfer certain types of data. It does this using a broadcast model, where central devices listen for beacons from peripherals2. Android Lollipop and higher support BLE in both the central and peripheral roles3. By constantly advertising and scanning for a predefined beacon, an app can locate nearby running instances of itself.
Because BLE advertisements are limited to 31 bytes, Noise’s beacon consists of exactly one service UUID, which is 16 bytes long:
5ac825f4-6084-42a6-0000-XXXXXXXXXXXX
[Noise UUID half ] [--] [MAC addr. ]
For the service UUID, I randomly generated a UUID and ignored its second half. The first half identifies that this advertisement is for Noise and the second half contains this device’s Bluetooth MAC address. The 15 leftover bytes in the beacon plus the 2 unused bytes in the UUID leaves 17 bytes that can be used later, such as for a subset of the message vector used in epidemic routing.
Noise retrieves BluetoothLeAdvertiser
and BluetoothLeScanner
from the BluetoothAdapter
to simultaneously start the beacon and listen for it. A crucial benefit of using BLE for discovery is that both advertising and scanning occur directly on the radio. To save power, the phone can sleep while this happens4 – Android will wake the app if it finds a nearby device.
Making the connection
Android provides listenUsingInsecureRfcommWithServiceRecord
and createInsecureRfcommSocketToServiceRecord
to create unauthenticated connections without pairing as long as both the server and client agree on a UUID. For simplicity, I also used the beacon’s service UUID to facilitate the connection.
After setting up BLE, Noise listens for Bluetooth Classic connections in a separate thread so it can continue in the background. The other (“client”) end makes these connections in the BLE scan callback using the MAC address from service UUID rather than the default one from the beacon. This is needed because the address that Android automatically includes on the beacon is a temporary one that cannot be used to create a Bluetooth Classic socket.
After these steps, each device has a socket connection to the other, and sync can proceed as defined by epidemic routing.
MAC address whack-a-mole
Android provides a getAddress()
on the BluetoothAdapter
that originally retrieved the Bluetooth adapter’s physical MAC address. However, Google has been making this more and more difficult:
- Pre-Marshmallow:
getAddress()
works fine - Marshmallow up to Oreo:
getAddress()
always returns02:00:00:00:00:00
. On these versions, Noise is able to use reflection to retrieve the address. - Oreo and up: using
getAddress()
requires the app to have theLOCAL_MAC_ADDRESS
permission, which is only granted to system apps or via root. Attempting to call it anyway will result in an uncatchableSecurityException
. As a workaround, the MAC address is visible in Settings, so it can be manually pasted into the app’s storage5.
I recognize that Google is trying to improve privacy with this move – in advertising, the MAC address is used to track users between device resets. Additionally, because the MAC address is included in the beacon, if an attacker manages to correlate an address to a person, that attacker can then determine if that person is nearby.
Unfortunately, as far as I know this is the most accessible way to create sockets between two Android phones, so this hack will have to stay. Suggestions are welcome.
Permissions
As expected, this requires the BLUETOOTH
and BLUETOOTH_ADMIN
permissions. On Marshmallow and up, starting a BLE scan requires the ACCESS_COARSE_LOCATION
permission (presumably because an app can determine where it is if a unique beacon is nearby). Noise’s AndroidManifest.xml also has LOCAL_MAC_ADDRESS
in the unlikely event that it is installed as a system app.
-
Recorded using scrcpy, which records the screens of physical Android devices over adb. ↩︎
-
BLE also provides a pair-free key-value store via GATT, but this does not scale to this application. ↩︎
-
I’ve tested that this works on my original Google Pixel, Moto G4 Play, and Sony Z5 Compact. Here’s a list of other devices that should work. ↩︎
-
I have not implemented this yet since I only own one phone that supports Oreo. ↩︎
-
This wasn’t implemented when I originally posted this, but the setting exists as of this commit. ↩︎