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); useGROUP="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