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.