Writing udev Rules on Linux: Fixed Network Interface Names and Auto-triggering Scripts on USB Plug-in

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

The Real Problem: Devices Renamed After Every Reboot

Have you ever run into this situation: a server with 2 network cards, where today eth0 is the WAN card, but after a reboot eth0 becomes the LAN card? Or you plug in a USB scanner and have to check whether it got assigned /dev/ttyUSB0 or /dev/ttyUSB1?

It’s not a bug. Linux assigns device names in the order the kernel detects them — not physical order, not the order you plugged them in. That order changes after every reboot, every kernel update, and sometimes just unplugging and replugging is enough to shuffle things around.

udev was created to fix exactly this problem. You write rules to declare: which device gets which name, what permissions it should have, and which script runs when it’s plugged in — udev handles the rest.

Comparing Approaches

Linux has several ways to handle device naming. A quick comparison to help you pick the right tool for the right situation:

Option 1: Manual configuration in /etc/network/interfaces or Netplan

On Ubuntu/Debian, you can hardcode interface names in the network configuration file. But this only solves the networking side — it doesn’t apply to USB, serial ports, or other device types. And if you physically swap the cards’ positions, the entire configuration breaks immediately.

Option 2: Using systemd-networkd with MACAddress matching

systemd-networkd supports matching devices by MAC address and assigning alias names. It works fine for network interfaces, but doesn’t handle other device types and is slightly more complex to configure than udev.

Option 3: udev rules — the Linux-native way

udev is the layer the Linux kernel uses to manage devices from the ground up. It operates at a lower level, applies to all device types (network, USB, block devices, serial…), and can trigger scripts automatically. In short: you’re using it exactly where it was designed to be used.

Criteria Manual config systemd-networkd udev rules
Device types supported Network only Network only All
Auto-run scripts No Limited Yes
Stable after reboot Kernel-dependent Good Very good
Learning curve Low Medium Medium

How udev Works

You plug in a device → the kernel fires a uevent. systemd-udevd receives the signal, scans through rule files in /etc/udev/rules.d/ and /lib/udev/rules.d/, finds any matching rules, then executes the corresponding actions. The entire process takes just a few milliseconds — by the time you plug something in, it’s already been processed.

Rules are read in filename order (the number prefix at the start of the name). Files in /etc/udev/rules.d/ override same-named files in /lib/udev/rules.d/.

Getting Device Information Before Writing Rules

Before writing a rule, you need to know the device’s attributes to match against. Use the udevadm command:

# View device info by /sys path
udevadm info -a -p /sys/class/net/enp3s0

# Or by /dev path
udevadm info -a -p $(udevadm info -q path -n /dev/ttyUSB0)

# Test a rule without reloading
udevadm test /sys/class/net/enp3s0

The output will show attributes like ATTRS{idVendor}, ATTRS{idProduct}, KERNELS, SUBSYSTEMS… These are the keys you use as match conditions in your rules.

Structure of a udev Rule

Each rule is a single line consisting of KEY=="value" pairs (match conditions) and KEY="value" pairs (actions):

SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="aa:bb:cc:dd:ee:ff", NAME="wan0"

Simple rule: == is comparison, = is assignment. Using the wrong one means the rule silently does nothing — no error message will tell you what went wrong.

Case 1: Assigning Fixed Network Interface Names by MAC Address

I manage an Ubuntu 22.04 production server with 2 network cards. After a kernel update, the two interfaces got swapped, and firewall rules ended up pointing at the wrong card — it took about 20 minutes of debugging to figure out. Since then I’ve used udev rules to lock the names down.

Step 1: Get the MAC address of each card:

ip link show
# or
cat /sys/class/net/enp*/address

Step 2: Create the rule file:

sudo nano /etc/udev/rules.d/10-network-names.rules
# WAN card (ISP connection)
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="aa:bb:cc:11:22:33", NAME="wan0"

# LAN card (internal switch connection)
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="aa:bb:cc:44:55:66", NAME="lan0"

Step 3: Update your network configuration to use the new names (wan0, lan0 instead of enp3s0), then reload udev and reboot:

sudo udevadm control --reload-rules
sudo udevadm trigger
sudo reboot

After that, no matter how many times you reboot, swap PCIe slots, or change the kernel — the interface names stay the same.

Case 2: Auto-running a Script When a USB Device Is Plugged In

A practical scenario: you have a USB backup drive, and every time it’s plugged into the server you want it to automatically mount and run rsync. Or something simpler: log every USB device plugged in for a security audit.

Identifying the USB vendor/product ID

# Plug in the USB then run
lsusb
# Output: Bus 001 Device 003: ID 0781:5581 SanDisk Corp. Ultra
# 0781 = idVendor, 5581 = idProduct

# Or use udevadm for more details
udevadm info -a -p $(udevadm info -q path -n /dev/sdb) | grep -E 'idVendor|idProduct|serial'

Writing the rule to trigger a script

sudo nano /etc/udev/rules.d/90-usb-backup.rules
# When a specific SanDisk USB is plugged in
ACTION=="add", SUBSYSTEM=="block", SUBSYSTEMS=="usb", \
  ATTRS{idVendor}=="0781", ATTRS{idProduct}=="5581", \
  RUN+="/usr/local/bin/usb-backup.sh"

The script /usr/local/bin/usb-backup.sh:

#!/bin/bash
# Script run by udev — NO interactive terminal
# udev runs with a minimal environment, no full PATH
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

LOGFILE="/var/log/usb-backup.log"
MOUNT_POINT="/mnt/usb-backup"

echo "$(date '+%Y-%m-%d %H:%M:%S') - USB backup drive detected" >> "$LOGFILE"

mkdir -p "$MOUNT_POINT"
sleep 2  # Wait for kernel to finish device recognition

mount /dev/sdb1 "$MOUNT_POINT" >> "$LOGFILE" 2>&1

if mountpoint -q "$MOUNT_POINT"; then
  rsync -av --delete /home/user/important/ "$MOUNT_POINT/backup/" >> "$LOGFILE" 2>&1
  echo "$(date '+%Y-%m-%d %H:%M:%S') - Backup completed" >> "$LOGFILE"
fi
sudo chmod +x /usr/local/bin/usb-backup.sh

Important note: Scripts run by udev have no terminal, no full PATH, and run as root. Forgetting to export PATH or using relative paths means the script dies silently — no error message, no log, nothing.

Case 3: Setting Fixed Permissions and Symlinks for Serial Devices

If you use an Arduino, RS485 converter, or GPS receiver over USB — they typically get assigned /dev/ttyUSB0 or /dev/ttyUSB1 at random. This rule creates a persistent symlink so your code doesn’t need to change every time you replug the device:

sudo nano /etc/udev/rules.d/99-usb-serial.rules
# Arduino Uno
SUBSYSTEM=="tty", ATTRS{idVendor}=="2341", ATTRS{idProduct}=="0043", \
  SYMLINK+="arduino", MODE="0666"

# GPS Receiver
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", \
  SYMLINK+="gps0", GROUP="dialout", MODE="0664"

After plugging in an Arduino, you’ll have /dev/arduino pointing to the correct device, regardless of whether the kernel assigned it as ttyUSB0 or ttyUSB3.

Debugging When a Rule Doesn’t Work

udev rules commonly fail for two reasons: confusing = with ==, or match conditions that don’t align with the device’s actual attributes. Here are the commands you need:

# View udev logs in real time
journalctl -f -u systemd-udevd

# Test a rule against a specific device (no execution, simulation only)
udevadm test /sys/class/net/enp3s0 2>&1 | grep -E 'IMPORT|RUN|NAME'

# Monitor events as devices are plugged in
udevadm monitor --environment --udev

# Force re-trigger the event for a device
udevadm trigger --action=add /sys/bus/usb/devices/1-1

Reload rules after every edit:

sudo udevadm control --reload-rules && sudo udevadm trigger

When to Use udev: A Summary

Use udev when you need: fixed network interface names by MAC address, persistent symlinks for USB/serial devices, or scripts that auto-run when a device is connected. If you just need to rename a single network interface on Ubuntu 22.04+, /etc/systemd/network/*.link is sufficient — but the moment any other device type enters the picture, udev is the only flexible option.

Once you’ve written your rules, test thoroughly before going to production. Use udevadm test to simulate, journalctl -f -u systemd-udevd to monitor in real time. And most importantly: back up your network configuration first — if interface names change but you haven’t updated Netplan or ifupdown, your SSH connection will drop immediately after reboot.

Share: