usbkill the OpenBSD way

posted: 2021-01-11
modified: 2021-01-15

usbkill is a kill-switch that shuts down your computer on any USB change. It does this by watching the output of lsusb and when there is a change it runs shutdown -h now.

Simple, right? One thing to note is that lsusb is not included in OpenBSD's base packages, but hotplugd(8) is.

hotplugd

So what is hotplugd, and how can it be useful? The man page does a good job of explaining it. Simply put: when any device is attached to or detached from your machine hotplugd will execute a script.

To see how simple it is to write a hotplug script we can start by simply logging device attach events. First, start by enabling and starting hotplugd (as root):

# rcctl enable hotplugd
# rcctl start hotplugd
hotplugd(ok)

Next we will write the script that is executed on a device attach event:

# mkdir -p /etc/hotplug
# $EDITOR /etc/hotplug/attach

In this file we will log the device class and name:

#!/bin/sh

DEVCLASS=$1
DEVNAME=$2

# log attach event
logger -t hotplug "$DEVCLASS:$DEVNAME attached"

To see this in action in another terminal type

$ tail -f /var/log/messages

Then connect a USB device to your machine and you should see messages tagged ‘hotplug’. This is what I get when I plug in my Logitech USB adapter for my wireless mouse (which I usually don't use):

Jan 11 11:48:32 x1 hotplug: 0:uhid3 attached
Jan 11 11:48:32 x1 hotplug: 5:wsmouse2 attached
Jan 11 11:48:32 x1 hotplug: 0:ums0 attached
Jan 11 11:48:32 x1 hotplug: 0:uhid1 attached
Jan 11 11:48:32 x1 hotplug: 5:wskbd1 attached
Jan 11 11:48:32 x1 hotplug: 0:uhid0 attached
Jan 11 11:48:32 x1 hotplug: 0:uhid4 attached
Jan 11 11:48:32 x1 hotplug: 0:uhid2 attached
Jan 11 11:48:32 x1 hotplug: 0:uhidev1 attached
Jan 11 11:48:32 x1 hotplug: 0:uhidev2 attached
Jan 11 11:48:32 x1 hotplug: 0:ukbd0 attached
Jan 11 11:48:32 x1 hotplug: 0:uhidev0 attached
Jan 11 11:48:32 x1 hotplug: 0:uhid5 attached
Jan 11 11:48:32 x1 hotplug: 0:uhid6 attached

Site note: What is happening here? Why have so many events popped up after inserting one USB device? My theory is that because it's a Unifying Receiver, it allows more than one Logitech HID to be connected to a single receiver, so it must register a bunch of HIDs to the kernel.

The ‘kill’ part

Now that we've seen how hotplugd works, we can work on the kill part of usbkill.
We want to shutdown the machine on a device attach or detach event, but we also want to be able to arm and disarm the kill part, since I might need to plug something in once in a while. Make the /etc/hotplug/attach script as so:

#!/bin/sh

arm_file=/tmp/arm_usbkill

DEVCLASS="$1"
DEVNAME="$2"

logger -t "${0##*/}" "${DEVCLASS}:${DEVNAME} attached"

if [ -f "$arm_file" ]; then
        shutdown -p now
else
        logger -t "${0##*/}" "unarmed - aborted shutdown"
fi

If the /tmp/arm_usbkill file exists then the machine will shutdown, if it doesn't exist it will just add a log entry to syslog.

hotplug also supports running a different script, /etc/hotplug/detach on detach events. Since both scripts are exactly the same I've symlinked the attach script to the location of the detach script.

# ln -s /etc/hotplug/attach /etc/hotplug/detach

The ${0##*/} logger tag gets substituted to the filename of the script being executed (in this case it is either attach or detach).

One thing to note is that this setup will execute the shutdown command on any device change event. This includes USBs but also includes network devices, disk drives, and serial line interfaces1. If that is a problem, you can implement a device class-based whitelist by checking $DEVCLASS:

#!/bin/sh

# class whitelist (space-separated)
class_whitelist="0 2 3 5"

# arm file
arm_file=/tmp/arm_usbkill

DEVCLASS="$1"
DEVNAME="$2"

whitelisted() {
    for class in $class_whitelist; do
        if [ "$class" = "$1" ]; then
            return 0
        fi
    done
    return 1
}

logger -t "${0##*/}" "${DEVCLASS}:${DEVNAME} attached"

if [ -f "$arm_file" ]; then
    if whitelisted "$DEVCLASS"; then
        logger -t "${0##*/}" "whitelisted - aborted shutdown"
        exit 0
    fi
    shutdown -p now
else
    logger -t "${0##*/}" "unarmed - aborted shutdown"
fi

But this is not really useful to me since I use a laptop and I'm not attaching and detaching network devices or disk drives.

One disadvantage of this setup is the inability to whitelist devices based on their USB IDs like you can with usbkill. This could be acheived through a much more complicated script using usbdevs to get USB IDs, but as of yet I have no need for that.

Arming and disarming

To arm and disarm this script all you have to do is create or remove the $arm_file respectively. Here are some shell functions to add to your shell rc to make it easier:

# arm/disarm usbkill
arm() {
        touch /tmp/arm_usbkill && printf 'armed\n' || printf 'error arming\n'
}
disarm() {
        rm -f /tmp/arm_usbkill && printf 'disarmed\n' || printf 'error disarming\n'
}

The script can be armed or disarmed by any regular user which may or may not be preferable to you (and isn't the case for usbkill, which requires root to run).
Originally I had added the uchg flag to the file to lock it. But this proved to be counterproductive since it wasn't cleared from /tmp on reboot and when hotplugd would first start up, it would immediately shutdown the machine again. That was fun to fix (not).

Why?

So why do all this? Why not just pkg_add usbutils, download + run usbkill and be done?
Well for starters that isn't as fun. The main reason is that all this works in a default OpenBSD install - no additional packages needed. Why install a program to solve a problem when the default tools and services can solve the problem just as (if not more) elegantly?

SEE ALSO


  1. As per the hotplugd(8) man page 

  2. In the future I might write a script to act more similarly to usbkill, using usbdevs to poll for device changes instead of lsusb