To Kill -9 a Drone, Part 2
This post first appeared on the Big Nerd Ranch blog.
Last time, we took a tour of TTYs and serial connections to shell into the Linux-powered Parrot Rolling Spider quadcopter. This time, we’ll telnet in over Bluetooth.
Before stumbling upon the USB method for shelling into my drone, I noticed that my drone didn’t shut down after 60 seconds if a Bluetooth device was connected. A fleeting glance at /etc/init.d/rcS
revealed calls to telnetd
and BLEproxy
.
telnet
opens a shell session over TCP/IP, usually over a WiFi or Ethernet connection. However, the Rolling Spider is not equipped with WiFi, so why the references to networking? I hypothesized that a lot of the mini-drone's software is based on the WiFi-equipped AR.Drone, which receives flight commands over UDP/TCP.
To keep the software reusable on the Rolling Spider, BLEproxy
might set up a bnep network interface, which would allow me to control to the Rolling Spider on 192.168.1.1
, just like the AR.Drone. To that end, I'll establish a PAN network with the drone so that I can open UDP/TCP connections through the bnep interface. Since telnetd
is listening on port 23, I should be able shell to the drone over Bluetooth.
But first things first: I need a computer with Bluetooth set up.
Bluetooth on OS X
Like most of my colleagues, I use a Mac. The BSD flavor of UNIX is great for day-to-day development, but the tooling differs drastically from a Linux stack.
Bluetooth is no exception: on Mac, there are few (i.e., zilch) terminal utilities for working with OS X’s Bluetooth stack. Linux, however, is well-stocked with open-source tools to configure Bluetooth and network interfaces, thanks to the BlueZ stack. The generous assemblage of blog and forum posts is especially crucial for wading through nondescript errors.
Linux VM #1: Debian
Rather than build a Linux computer from scratch, I’ll run a virtual machine with Virtual Box. Still, it takes a couple hours to spin up the machine, install the OS and install guest additions. Maybe some scripts could save me some time?
Enter Vagrant: it makes spinning up a VM a snap by downloading and setting up common VM configurations for you. Most folks use it to set up a consistent staging environment, but it also works great for quickly setting up a Linux box with shell access.
There are plenty of VM templates to choose from; for entirely arbitrary reasons, I decide to spin up a Debian 7.6 machine:
$ vagrant init chef/debian-7.6
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Box 'chef/debian-7.6' could not be found. Attempting to find and install...
default: Box Provider: virtualbox
default: Box Version: >= 0
==> default: Loading metadata for box 'chef/debian-7.6'
default: URL: https://atlas.hashicorp.com/chef/debian-7.6
==> default: Adding box 'chef/debian-7.6' (v1.0.0) for provider: virtualbox
default: Downloading: https://atlas.hashicorp.com/chef/boxes/debian-7.6/versions/1.0.0/providers/virtualbox.box
==> default: Successfully added box 'chef/debian-7.6' (v1.0.0) for 'virtualbox'!
==> default: Importing base box 'chef/debian-7.6'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'chef/debian-7.6' is up to date...
==> default: Setting the name of the VM: drone_default_1435939856993_21528
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
default: Adapter 1: nat
==> default: Forwarding ports...
default: 22 => 2222 (adapter 1)
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
default: SSH address: 127.0.0.1:2222
default: SSH username: vagrant
default: SSH auth method: private key
default:
default: Vagrant insecure key detected. Vagrant will automatically replace
default: this with a newly generated keypair for better security.
default:
default: Inserting generated public key within guest...
default: Removing insecure key from the guest if its present...
default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Mounting shared folders...
$ vagrant halt
==> default: Attempting graceful shutdown of VM...
Three minutes later, I have a ready-to-go Debian instance. Next, I need to give the VM access to my MacBook's internal Bluetooth card. Guest Additions need to be installed for the VM to recognize various hardware devices, but this Debian VM template already has it installed. So I simply need to enable the USB 2.0 controller and add my Bluetooth card:
Just to make sure the hardware settings work, I start up the VM directly in VirtualBox rather than using Vagrant's CLI. It boots into a shell, so I log in with username vagrant
and password vagrant
. Now for the hardware: my Bluetooth card shows up in the list of available USB Devices:
Time to mount my Bluetooth USB device to the Debian VM:
Reclaiming Bluetooth
Uh oh! For the virtual machine to use Bluetooth, it needs to have the device all to itself, but the host OS is using it. Turning off Bluetooth in OS X won't suffice: I need to kill -9
the blued
process on my Mac, but it regenerates itself every time. For blued
to really die, it needs to be unloaded with launchctl
:
$ sudo launchctl unload /System/Library/LaunchDaemons/com.apple.blued.plist
That's still not quite enough: some kernel extensions (KEXT) are holding on. Let's search all the loaded KEXTs for those relating to Bluetooth:
$ kextstat -l | awk '{gsub(/^ +| +$/,"")}1' | tr -s ' ' | cut -f6 -d ' ' | grep Bluetooth
com.apple.iokit.IOBluetoothFamily
com.apple.iokit.IOBluetoothHostControllerUSBTransport
com.apple.iokit.BroadcomBluetoothHostControllerUSBTransport
com.apple.iokit.IOBluetoothSerialManager
The third item BroadcomBluetoothHostControllerUSBTransport
looks pertinent, maybe unloading it will free up the Bluetooth device:
$ sudo kextunload -b com.apple.iokit.BroadcomBluetoothHostControllerUSBTransport
Success! Now when we try to connect the Bluetooth controller to Virtual Box, it doesn't give any errors. Let's shut down the VM and start it up from Vagrant now:
$ vagrant up
[ lots of output ]
$ vagrant ssh
vagrant@packer-debian-7:~$
Just to make sure, I'll use lsusb
to see if the Linux VM recognizes my Bluetooth controller:
$ sudo apt-get update
$ sudo apt-get install usbutils
$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 002 Device 002: ID 05ac:8286 Apple, Inc.
The output is stripped of pleasantries, but that last device has Apple's manufacturer ID (05ac), so it must be the Bluetooth controller. I'm ready to install the BlueZ stack:
$ sudo apt-get install bluez bluez-utils bluez-compat
Next up, I need to tell Linux what Bluetooth device to use.
$ hciconfig
Can't open HCI socket.: Address family not supported by protocol
What’s going on? The Linux kernel is super modular and made of many low-level modules. Most device drivers (like USB and Ethernet) are just Linux modules, including Bluetooth. So, does our Linux kernel have a Bluetooth module?
$ lsmod | grep bluetooth
[ nothing :-( ]
Nope! Debian 7.6's version of the Linux kernel doesn’t support the Bluetooth module.
Linux VM #2: Debian Again
Time for an upgrade: I'll rinse and repeat the setup steps with a more recent Debian:
$ vagrant init chef/debian-7.8
$ vagrant up
$ vagrant halt
After running the rest of the commands, I can double check kernel support:
$ lsmod | grep bluetooth
bluetooth 119455 1 btusb
rfkill 19012 1 bluetooth
crc16 12343 2 ext4,bluetooth
That looks much more promising! This kernel is built with the Bluetooth module. Let's try hcitool
:
$ hcitool dev
Devices:
hci0 28:CF:E9:21:6C:1A
Great! Linux recognizes my Bluetooth device. I need to set up the device on every reboot:
$ sudo hciconfig hci0 up
Now I should be able to scan for Bluetooth Low Energy devices. Turn my little drone on, and...
$ sudo hcitool lescan
LE Scan ...
9E:08:00:AC:0B:19 RS_R112233
9E:08:00:AC:0B:19 (unknown)
It's alive, but all is not well. When I try to connect to the drone with sudo hcitool lecc
or gatttool
, it fails with a Host is down (112)
message. Maybe BLE support in my old version of BlueZ is buggy?
BlueZ is currently version 5.30, but why is Debian 7.8 bundled with BlueZ 4? The BlueZ 5 API diverges drastically from BlueZ 4, so it takes a fair amount of effort to upgrade apps. Coupled with the shockingly poor documentation, very few Linux distributions have successfully upgraded to BlueZ 5.
I initially built BlueZ 5.30 from source, but a few binaries are missing, so I'll install BlueZ 5.23 from the Debian Unstable package source.
As part of major refactors, BlueZ 5 directs all commands to the bluetoothd
daemon over D-Bus. To send commands from the terminal, I can run bluetoothctl
:
$ sudo bluetoothctl
[bluetooth]#
I get a REPL, but it just hangs. Maybe bluetoothd
isn't listening?
$ ps aux | grep bluetoothd
[ nothing :( ]
Doh, bluetoothd
isn't even running! I'll run it myself in no-detach mode:
$ sudo bluetoothd -n
bluetoothd[4440]: Bluetooth daemon 5.14
bluetoothd[4440]: Failed to access management interface
bluetoothd[4440]: Adapter handling initialization failed
$ uname -r
3.2.0-4-amd64
How embarrassing—our Linux kernel is still too old! BlueZ 5 requires a kernel version >= 3.4 to support the new Bluetooth Management kernel interface, and >= 3.5 for BLE support!
Maybe it's time for a third Linux distribution?
Linux VM #3: Ubuntu
This time, I'll pick a recent distribution: Ubuntu 14.04 LTS. It ships with version 3.13 of the Linux kernel. After repeating all the setup commands I ran on the Debian distros, things seem to be in order:
$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 002: ID 05ac:8286 Apple, Inc. Bluetooth Host Controller
Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
The BlueZ version for Ubuntu is still too old, but thankfully another package source has the latest builds. I'll add it to my apt-get sources:
$ echo "deb http://ppa.launchpad.net/vidplace7/bluez5/ubuntu trusty main" >> /etc/apt/sources.list
$ sudo apt-get install bluez
$ bluetoothctl -v
5.30
I'm definitely running BlueZ 5.30, time to see if bluetoothd
started up:
$ sudo bluetoothctl
[NEW] Controller 28:CF:E9:21:6C:1A vagrant [default]
[bluetooth]#
Looks like I have working a Bluetooth setup!
PANning for Networks
With Bluetooth working correctly on my VM, I am ready to set up a PAN with my drone. BlueZ 4 includes a utility called pand
that does just this, but in BlueZ 5 I have to code up my own script. Since everything happens over D-Bus, I can write a Python script to initiate a PAN. Thankfully, Mike Kazantsev has already written that script.
$ wget https://github.com/mk-fg/fgtk/raw/master/bt-pan
$ chmod +x bt-pan
$ sudo apt-get install python-dbus
The bt-pan
script is a modified version of the test-network
script bundled with BlueZ 5. It instructs the bluetoothd
to create a new network interface called bnep0
. I need to grab the MAC address of my drone, then feed that into the script:
$ sudo hcitool lescan
LE Scan ...
9E:08:00:AC:0B:19 RS_R112233
9E:08:00:AC:0B:19 (unknown)
$ sudo ./bt-pan -u panu client 9E:08:00:AC:0B:19
Traceback (most recent call last):
File "./bt-pan", line 207, in <module>
if __name__ == '__main__': sys.exit(main())
File "./bt-pan", line 180, in main
try: iface = net.Connect(opts.uuid)
File "/usr/lib/python2.7/dist-packages/dbus/proxies.py", line 145, in __call__
**keywords)
File "/usr/lib/python2.7/dist-packages/dbus/connection.py", line 651, in call_blocking
message, timeout)
dbus.exceptions.DBusException: org.freedesktop.DBus.Error.UnknownMethod: Method "Connect" with signature "s" on interface "org.bluez.Network1" doesn't exist
A D-Bus issue? After all that work, it turns out that the PAN interface Network1
is not available. This isn't a spurious failure: Bluetooth Low Energy doesn't support the PAN profile, so Network1
is never exposed on the D-Bus interface. Only regular Bluetooth supports PAN!
Next time...
To my disappointment, we aren't quite to the point of telnet
ing into the drone. But never fear! In Part 3, we will backtrack to another clue in /etc/init.d/rcS
to complete our journey of wirelessly shelling into the mini-drone.