22 August 2025

Stable USB Names for RTL-SDR (and Passing the Right Stick into Docker)

Stable USB Names for RTL-SDR (and Passing the Right Stick into Docker)

When you run multiple RTL-SDR sticks (ADS-B, airband, weather, etc.), you want each container to receive the correct dongle every time—regardless of USB bus renumbering or reboots. This post shows a robust udev rule that creates a persistent, human-readable symlink per stick (based on its serial), plus a deep dive on security choices (uaccess vs plugdev). We’ll also cover setting your own serials with rtl_eeprom.


The Finished udev Rule

Create /etc/udev/rules.d/99-rtlsdr.rules with a single line:

SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="0bda", ATTR{idProduct}=="2838", IMPORT{builtin}="usb_id", ENV{DEVNAME}!="" , ENV{ID_SERIAL_SHORT}!="" , TAG+="uaccess", MODE="0666", SYMLINK+="sdr-%E{ID_SERIAL_SHORT}"

Reload rules and cold-trigger the device (or replug):

sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=usb --attr-match=idVendor=0bda --attr-match=idProduct=2838

You should see a stable link like:

/dev/sdr-80868086 -> /dev/bus/usb/001/007
Why one physical line?

Backslashes / line continuations and smart quotes sometimes sneak in during copy/paste and break parsing. Keep each rule on a single line with straight quotes.


Explaining Every Component

SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device"

Targets the USB device node (not an interface). Only the device node has a real DEVNAME (e.g., /dev/bus/usb/<bus>/<dev>) that a symlink can point to.

ATTR{idVendor}=="0bda", ATTR{idProduct}=="2838"

Matches Realtek RTL2832U family sticks (0bda:2838). Prevents touching unrelated USB gear.

IMPORT{builtin}="usb_id"

Runs udev’s builtin parser so we get ID_SERIAL_SHORT from the descriptors.

ENV{DEVNAME}!="" , ENV{ID_SERIAL_SHORT}!=""

Race-proofing: require that the kernel node already exists and the serial string is present. This avoids creating a dangling name like /dev/sdr- or failing due to timing.

TAG+="uaccess"

Grants access via ACLs to the active desktop/console user (through systemd-logind). Safer than world-writable; details in the security section.

MODE="0666"

Optional. Allows any user to open the device (handy in labs/headless setups). Remove it if you want to rely solely on uaccess ACLs.

SYMLINK+="sdr-%E{ID_SERIAL_SHORT}"

Creates a persistent, friendly name per stick, e.g. /dev/sdr-ADS-B01. This stays stable across reboots and bus renumbering—perfect for Docker bindings.

Fallback for sticks without a serial

Add this rule after the main one to name by physical port:

SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="0bda", ATTR{idProduct}=="2838", IMPORT{builtin}="path_id", ENV{DEVNAME}!="" , SYMLINK+="sdr-port-%E{ID_PATH}"

This produces stable, port-based names (e.g., /dev/sdr-port-pci-0000_00_14_0-usb-0_3_1_0).


Security: uaccess vs plugdev vs 0666

TAG+="uaccess" (modern, desktop-friendly)

  • How: udev tags the device; systemd-logind applies ACLs to the active seat user.
  • Pros: Least privilege, no group management, follows the logged-in user.
  • Cons: Requires systemd-logind; on headless servers there’s no “active seat”.

Tight variant (no world write):

SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="0bda", ATTR{idProduct}=="2838", IMPORT{builtin}="usb_id", ENV{DEVNAME}!="" , ENV{ID_SERIAL_SHORT}!="" , TAG+="uaccess", SYMLINK+="sdr-%E{ID_SERIAL_SHORT}"

GROUP="plugdev", MODE="0660" (classic, headless/server-friendly)

  • How: set device group to plugdev; any member can access it.
  • Pros: Works without systemd; simple mental model.
  • Cons: Anyone in the group can access all matching devices; manage GIDs for Docker.

Rule variant:

SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="0bda", ATTR{idProduct}=="2838",
  IMPORT{builtin}="usb_id", ENV{DEVNAME}!="" , ENV{ID_SERIAL_SHORT}!="" ,
  GROUP="plugdev", MODE="0660", SYMLINK+="sdr-%E{ID_SERIAL_SHORT}"

Create/add group:

sudo groupadd plugdev 2>/dev/null || true
sudo usermod -aG plugdev $USER
# Re-login or: newgrp plugdev

MODE="0666" (open lab)

  • How: Anyone on the box can open the device.
  • Pros: Zero friction; containers “just work”.
  • Cons: Least restrictive; only use on trusted machines.

Give Sticks Human-Friendly Serials with rtl_eeprom

Many RTL-SDR dongles store their USB serial in EEPROM. You can set your own (e.g., ADS-B01, AIRBAND1) so your symlinks are readable and obvious.

# Install the tools (Debian/Ubuntu):
sudo apt install rtl-sdr

# Inspect current EEPROM (non-destructive):
rtl_eeprom -d 0

# Set a new serial (example: ADS-B01):
sudo rtl_eeprom -d 0 -s ADS-B01

# Unplug/replug the dongle for changes to take effect.

Notes:

  • -d 0 selects the first detected device (increment for additional sticks).
  • -s sets the serial string (8 characters or fewer is widely compatible).
  • Warning: only change the serial unless you know what you’re doing; other fields affect calibration.

After setting serials, the udev link names become self-documenting:

/dev/sdr-ADS-B01
/dev/sdr-AIRBAND1
/dev/sdr-WEATHER

Using Stable Names with Docker

docker run

docker run --rm \
  --name adsb \
  --device /dev/sdr-ADS-B01 \
  ghcr.io/<your-org>/readsb:latest

docker-compose

services:
  adsb:
    image: ghcr.io/<your-org>/readsb:latest
    devices:
      - /dev/sdr-ADS-B01:/dev/sdr-adsb
    # If you rely on uaccess or plugdev, run as your user to inherit host permissions:
    user: "1000:1000"

If running containers as non-root, ensure the container user’s UID/GID matches your host user (for uaccess) or is a member of the host’s plugdev GID (for group-based access).


Reliability Checklist

  • Use a late filename like 99-rtlsdr.rules so the kernel node exists when your rule runs.
  • Keep each rule on one physical line with straight quotes.
  • Strip Windows line endings if you edited on Windows:
    sudo sed -i 's/\r$//' /etc/udev/rules.d/99-rtlsdr.rules
    
  • Ensure coldplug runs at boot so present devices get rules applied:
    systemctl is-enabled systemd-udev-trigger.service
    sudo systemctl enable --now systemd-udev-trigger.service
    

Debugging Like a Pro

See the exact sysfs object and attributes

# Replace /dev/ttyUSB0 with any node you have (or locate by lsusb)
udevadm info -q path -n /dev/ttyUSB0
udevadm info -a -p /sys/bus/usb/devices/1-1

Dry-run rules (no changes, very verbose)

sudo udevadm test --action=add /sys/bus/usb/devices/1-1

Watch real events and environment while (un)plugging

udevadm monitor --udev --environment

Live udev logs

sudo udevadm control --log-level=debug
journalctl -fu systemd-udevd

TL;DR

  • Use udev to create stable per-stick symlinks: /dev/sdr-<serial>.
  • Prefer TAG+="uaccess" on desktops (least privilege); use GROUP="plugdev", MODE="0660" on headless servers; 0666 only for trusted labs.
  • Set your own serials with rtl_eeprom for human-readable names.
  • Bind the symlink into Docker with --device so each service gets the right stick.

No comments:

Post a Comment