A Unicode text adventure in the land of Linux interface names where we cast systemd
as the villain.
The Linux kernel takes a laissez-faire attitude to interface naming, letting almost any
character be used and only disallowing directory names, space, /
and :
. I should be
able to sneak some Unicode characters in there, right? So I thought it would be nice to give my
interfaces colourful happy names to brighten my day.
ip link add 😻 type bridge
Unfortunately systemd
had other ideas. The kernel delegates device naming to user space which
means that systemd
or, more accurately, systemd-udevd
gets involved.
ip link show type bridge
5: ____: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 06:bf:1c:90:66:35 brd ff:ff:ff:ff:ff:ff
It turns out that systemd-udevd
takes a quite authoritarian attitude to interface naming. As I
write this, on Fedora 39, systemd-udevd
is enforcing v253
of its naming commandments while
also applying a few thousand lines of udev rules.
Back in the pre v249
days, systemd-udevd
only outlawed /
characters in interface names and
translated them into _
characters. Since then, with the advent of NAMING_REPLACE_STRICTLY,
only ASCII characters between 32 and 127 are now deemed acceptable. The strategy is still to
translate disallowed characters into _
, foreshadowing a systemd
pratfall.
The udev(7) manual page and the code in src/udev/udev-rules.c suggest that I could override the
v253
enforcement policy with a rule that says OPTIONS+="string_escape=none"
. I tried this
with the following rule, but it didn't have any effect.
# /etc/udev/rules.d/01-raw-names.rules
SUBSYSTEM=="net", OPTIONS+="string_escape=none"
I can verify that my rule does run, with a systemtap probe on the rules_apply_line
tracepoint
in systemd-udevd
– which is actually a symlink to /usr/bin/udevadm
:
probe process("/usr/bin/udevadm").mark("rules_apply_line")
{
printf("udev evaluating file=%s line=%d\n", user_string($arg5), $arg6);
}
stap udev-rules.stp
udev evaluating file=/usr/lib/udev/rules.d/01-md-raid-creating.rules line=7
udev evaluating file=/etc/udev/rules.d/01-raw-names.rules line=1
udev evaluating file=/usr/lib/udev/rules.d/10-dm.rules line=31
...
The udev(7) manual page also tells us that net udev event processing can be debugged with the following rule:
# /etc/udev/rules.d/00-debug-net.rules
SUBSYSTEM=="net", OPTIONS="log_level=debug"
Now we can see what happens from the journal output:
(udev-worker)[3134]: 🐈: The log level is changed to 'debug' while processing device (SEQNUM=4567, ACTION=add)
(udev-worker)[3134]: 🐈: /usr/lib/udev/rules.d/75-net-description.rules:6 Importing properties from results of builtin command 'net_id'
(udev-worker)[3134]: 🐈: addr_assign_type=1, MAC address is not permanent.
(udev-worker)[3134]: 🐈: sd_device_get_parent() failed: No such file or directory
(udev-worker)[3134]: 🐈: sd_device_get_parent() failed: No such file or directory
(udev-worker)[3134]: 🐈: sd_device_get_parent() failed: No such file or directory
(udev-worker)[3134]: 🐈: sd_device_get_parent_with_subsystem_devtype() failed: No such file or directory
(udev-worker)[3134]: 🐈: /usr/lib/udev/rules.d/80-net-setup-link.rules:5 Importing properties from results of builtin command 'path_id'
(udev-worker)[3134]: 🐈: /usr/lib/udev/rules.d/80-net-setup-link.rules:5 Failed to run builtin 'path_id': No such file or directory
(udev-worker)[3134]: 🐈: /usr/lib/udev/rules.d/80-net-setup-link.rules:9 Importing properties from results of builtin command 'net_setup_link'
(udev-worker)[3134]: 🐈: Device has name_assign_type=3
(udev-worker)[3134]: 🐈: Device has addr_assign_type=1
NetworkManager[1300]: <info> [1699542979.3467] manager: (🐈): new Bridge device (/org/freedesktop/NetworkManager/Devices/16)
(udev-worker)[3134]: 🐈: Config file /usr/lib/systemd/network/98-default-mac-none.link is applied
(udev-worker)[3134]: 🐈: Using static MAC address.
(udev-worker)[3134]: 🐈: Policy *keep*: keeping existing userspace name
(udev-worker)[3134]: 🐈: /usr/lib/udev/rules.d/80-net-setup-link.rules:11 Replaced 4 character(s) from result of NAME="$env{ID_NET_NAME}"
(udev-worker)[3134]: 🐈: /usr/lib/udev/rules.d/80-net-setup-link.rules:11 NAME '____'
(udev-worker)[3134]: 🐈: /usr/lib/udev/rules.d/99-systemd.rules:68 RUN '/usr/lib/systemd/systemd-sysctl --prefix=/net/ipv4/conf/$name --prefix=/net/ipv4/neigh/$name --prefix=/net/ipv6/conf/$nam>
(udev-worker)[3134]: 🐈: sd-device: Created db file '/run/udev/data/n15' for '/devices/virtual/net/🐈'
kernel: ____: renamed from 🐈
(udev-worker)[3134]: ____: Network interface 15 is renamed from '🐈' to '____'
NetworkManager[1300]: <info> [1699542979.3595] device (🐈): interface index 15 renamed iface from '🐈' to '____'
This lets us see that it is line #11
of /usr/lib/udev/rules.d/80-net-setup-link.rules
that
triggers the rename.
NAME=="", ENV{ID_NET_NAME}!="", NAME="$env{ID_NET_NAME}"
The guard at the start of the line makes me think that I should be able to set NAME
in my own
rule and then this line won't get executed. In order to find out what field contains the
original interface name, I can inspect the originating kernel udev message by running udevadm
monitor -p
:
KERNEL[5671.341572] add /devices/virtual/net/🐈 (net)
ACTION=add
DEVPATH=/devices/virtual/net/🐈
SUBSYSTEM=net
DEVTYPE=bridge
INTERFACE=🐈
IFINDEX=15
SEQNUM=4567
I updated my rule to pre-emptively set NAME
with string_escape=none
in the same rule:
# /etc/udev/rules.d/01-raw-names.rules
SUBSYSTEM=="net", ACTION=="add", OPTIONS+="string_escape=none", NAME="$env{INTERFACE}"
ip link add 😺 type bridge
donaldh@tosh ~ $ ip link show type bridge
19: 🐈: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 76:aa:f8:c4:87:b0 brd ff:ff:ff:ff:ff:ff
20: 😺: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 12:66:dd:ec:0f:b8 brd ff:ff:ff:ff:ff:ff
Happy days! 😼
If that hadn't worked, my last resort would have been to use a more blunt tool and roll
back time, so to speak. I could tell systemd-udevd
to use an older naming policy by adding a
net.naming-scheme
kernel boot option:
options root=/dev/mapper/fedora-root ro rd.lvm.lv=fedora/root net.naming-scheme=v247
Something that I noted along the way – it's not ideal that the kernel accepts some control
characters in interface names, e.g. \b
:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
16: 🐈: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 12:7c:de:eb:0c:66 brd ff:ff:ff:ff:ff:ff
: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 8e:c3:b4:53:28:86 brd ff:ff:ff:ff:ff:ff
: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 4e:bb:67:1d:64:11 brd ff:ff:ff:ff:ff:ff
Also of note is that systemd-udevd
doesn't handle self-generated name collisions at all.
Before I added my custom rule, if I repeated the request to create 😻 then systemd-udevd
again
tried to rename it to ____
which already existed. The rename would fail and systemd-udevd
would just give up. Pretty wild huh?
If you want to learn more about systemd
then the manpages have got you covered.