From fa711abefe2d5f0a63895591044064908465f7af Mon Sep 17 00:00:00 2001
From: Franck Bui <fbui@suse.com>
Date: Fri, 18 Sep 2020 13:25:44 +0200
Subject: [PATCH 1002/1002] udev: persistent net rule generator support

This patch reintroduces the ability to rename a NIC even if the new name is
currently in use by another NIC. In such cases, udev waits until the new name
becomes available, while the previously named NIC is renamed in turn.

Additionally, this reintroduces the persistent net rule generator, ensuring
that names of new added NICs don't conflict with existing one and remain
persistent across reboots.

This patch is not required on systems where the predictable naming scheme is
enabled by default, as default NIC names can not conflict with each other.

To facilitate the migration of Micro-based systems to future versions, the
persistent net rule generator is enabled only when predictable network
interface names are explicitly disabled via the kernel command line option
net.ifnames=0.

[fbui: fixes bsc#1241190]
---
 man/systemd-udevd.service.xml                 |   2 +-
 rules.d/75-persistent-net-generator.rules     | 125 ++++++++++++++
 rules.d/meson.build                           |   1 +
 src/libsystemd/sd-netlink/netlink-util.c      |  43 ++++-
 src/udev/meson.build                          |   4 +
 .../rule_generator/rule_generator.functions   | 113 +++++++++++++
 src/udev/rule_generator/write_net_rules       | 153 ++++++++++++++++++
 7 files changed, 439 insertions(+), 2 deletions(-)
 create mode 100644 rules.d/75-persistent-net-generator.rules
 create mode 100644 src/udev/rule_generator/rule_generator.functions
 create mode 100755 src/udev/rule_generator/write_net_rules

diff --git a/man/systemd-udevd.service.xml b/man/systemd-udevd.service.xml
index 3107fb7ce9..7edfda0869 100644
--- a/man/systemd-udevd.service.xml
+++ b/man/systemd-udevd.service.xml
@@ -204,7 +204,7 @@
         <term><varname>net.ifnames=</varname></term>
         <listitem>
           <para>Network interfaces are renamed to give them predictable names
-          when possible. It is enabled by default; specifying 0 disables it.</para>
+          when possible. It is disabled by default; specifying 1 enables it.</para>
         </listitem>
       </varlistentry>
       <varlistentry>
diff --git a/rules.d/75-persistent-net-generator.rules b/rules.d/75-persistent-net-generator.rules
new file mode 100644
index 0000000000..e231263349
--- /dev/null
+++ b/rules.d/75-persistent-net-generator.rules
@@ -0,0 +1,125 @@
+# do not edit this file, it will be overwritten on update
+
+# these rules generate rules for persistent network device naming
+#
+# variables used to communicate:
+#   MATCHADDR             MAC address used for the match
+#   MATCHID               bus_id used for the match
+#   MATCHDRV              driver name used for the match
+#   MATCHIFTYPE           interface type match
+#   COMMENT               comment to add to the generated rule
+#   INTERFACE_NAME        requested name supplied by external tool
+#   INTERFACE_NEW         new interface name returned by rule writer
+
+ACTION!="add", GOTO="persistent_net_generator_end"
+SUBSYSTEM!="net", GOTO="persistent_net_generator_end"
+
+# ignore the interface if a name has already been set
+NAME=="?*", GOTO="persistent_net_generator_end"
+
+# device name whitelist
+KERNEL!="eth*|ath*|wlan*[0-9]|msh*|ra*|sta*|ctc*|lcs*|hsi*", GOTO="persistent_net_generator_end"
+
+IMPORT{cmdline}="net.ifnames"
+ENV{net.ifnames}!="0", GOTO="persistent_net_generator_end"
+
+# read MAC address
+ENV{MATCHADDR}="$attr{address}"
+
+# match interface type
+ENV{MATCHIFTYPE}="$attr{type}"
+
+# detect virtualization (none is set if we are not a guest)
+ENV{ID_VIRT}="none", PROGRAM=="/usr/bin/systemd-detect-virt", RESULT=="?*", ENV{ID_VIRT}="$result"
+
+# KVM virtual interfaces, not to be confused with Realtek interfaces
+ENV{MATCHADDR}=="52:54:00:*", ENV{ID_VIRT}=="kvm", ENV{MATCHADDR}=""
+
+# ignore VMWare virtual interfaces
+ENV{MATCHADDR}=="00:0c:29:*|00:50:56:*", GOTO="persistent_net_generator_end"
+# ignore Hyper-V virtual interfaces
+ENV{MATCHADDR}=="00:15:5d:*", GOTO="persistent_net_generator_end"
+
+# These vendors are known to violate the local MAC address assignment scheme
+# Interlan, DEC (UNIBUS or QBUS), Apollo, Cisco, Racal-Datacom
+ENV{MATCHADDR}=="02:07:01:*", GOTO="globally_administered_whitelist"
+# 3Com
+ENV{MATCHADDR}=="02:60:60:*", GOTO="globally_administered_whitelist"
+# 3Com IBM PC; Imagen; Valid; Cisco; Apple
+ENV{MATCHADDR}=="02:60:8c:*", GOTO="globally_administered_whitelist"
+# Intel
+ENV{MATCHADDR}=="02:a0:c9:*", GOTO="globally_administered_whitelist"
+# Olivetti
+ENV{MATCHADDR}=="02:aa:3c:*", GOTO="globally_administered_whitelist"
+# CMC Masscomp; Silicon Graphics; Prime EXL
+ENV{MATCHADDR}=="02:cf:1f:*", GOTO="globally_administered_whitelist"
+# Prominet Corporation Gigabit Ethernet Switch
+ENV{MATCHADDR}=="02:e0:3b:*", GOTO="globally_administered_whitelist"
+# BTI (Bus-Tech, Inc.) IBM Mainframes
+ENV{MATCHADDR}=="02:e6:d3:*", GOTO="globally_administered_whitelist"
+# Realtek
+ENV{MATCHADDR}=="52:54:00:*", GOTO="globally_administered_whitelist"
+# Novell 2000
+ENV{MATCHADDR}=="52:54:4c:*", GOTO="globally_administered_whitelist"
+# Realtec
+ENV{MATCHADDR}=="52:54:ab:*", GOTO="globally_administered_whitelist"
+# Kingston Technologies
+ENV{MATCHADDR}=="e2:0c:0f:*", GOTO="globally_administered_whitelist"
+# Xensource
+ENV{MATCHADDR}=="00:16:3e:*", GOTO="globally_administered_whitelist"
+
+# ibmveth/ibmvnic like to use "locally administered" MAC addresses
+DRIVERS=="ibmveth", ENV{MATCHADDR}="$attr{address}", GOTO="globally_administered_whitelist"
+DRIVERS=="ibmvnic", ENV{MATCHADDR}="$attr{address}", GOTO="globally_administered_whitelist"
+
+# mlx4 on s390 uses to be a Mellanox SR-IOV function for an S/390 LPAR
+# where the physical function is controlled by the LPAR hypervisor
+# (unlike x86_64 KVM guest, where the physical function is controlled
+# by the linux host OS). In this case all virtual SR-IOV virtual
+# functions will have a "locally administered" MAC address and will
+# share the same bus (PCI) id (0000:00:00.0) unlike on on x86_64 where
+# the MAC addresses are random.
+DRIVERS=="mlx4_core", KERNELS=="0000:00:00.0", ENV{MATCHADDR}="$attr{address}", GOTO="globally_administered_whitelist"
+
+# match interface dev_id
+# HACK: for s390x qeth devices, if layer2 == 0, dont use dev_id
+ENV{LAYER2}="1", TEST=="device/layer2", ENV{LAYER2}="$attr{device/layer2}"
+ENV{LAYER2}!="0", ATTR{dev_id}=="?*", ENV{MATCHDEVID}="$attr{dev_id}"
+
+# do not use "locally administered" MAC address
+ENV{MATCHADDR}=="?[2367abef]:*", ENV{MATCHADDR}=""
+
+# if mac is "locally administered", then use $id (KERNELS) and $driver (DRIVERS)
+ENV{MATCHADDR}=="", DRIVERS=="?*", SUBSYSTEMS=="pci|ccw", ENV{MATCHID}="$id", ENV{MATCHDRV}="$driver"
+
+# do not use empty address
+ENV{MATCHADDR}=="00:00:00:00:00:00", ENV{MATCHADDR}=""
+
+LABEL="globally_administered_whitelist"
+
+# build comment line for generated rule:
+SUBSYSTEMS=="pci", ENV{COMMENT}="PCI device $attr{vendor}:$attr{device} ($driver)"
+SUBSYSTEMS=="ccw", ENV{COMMENT}="S/390 $driver device at $id"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="?*", ENV{COMMENT}="USB device 0x$attr{idVendor}:0x$attr{idProduct} ($driver)"
+SUBSYSTEMS=="pcmcia", ENV{COMMENT}="PCMCIA device $attr{card_id}:$attr{manf_id} ($driver)"
+SUBSYSTEMS=="ieee1394", ENV{COMMENT}="Firewire device $attr{host_id})"
+DRIVERS=="ibmveth", ENV{COMMENT}="ibmveth ($id)"
+DRIVERS=="ibmvnic", ENV{COMMENT}="ibmvnic ($id)"
+DRIVERS=="mlx4_core", ENV{COMMENT}="mlx4_core ($id)"
+
+# S/390 uses id matches only, do not use MAC address match
+SUBSYSTEMS=="ccwgroup", ENV{COMMENT}="S/390 $driver device at $id", ENV{MATCHID}="$id", ENV{MATCHDRV}="$driver", ENV{MATCHADDR}=""
+
+# see if we got enough data to create a rule
+ENV{MATCHADDR}=="", ENV{MATCHID}=="", ENV{INTERFACE_NAME}=="", GOTO="persistent_net_generator_end"
+
+# default comment
+ENV{COMMENT}=="", ENV{COMMENT}="net device ($attr{driver})"
+
+# write rule
+DRIVERS=="?*", IMPORT{program}="write_net_rules"
+
+# rename interface if needed
+ENV{INTERFACE_NEW}=="?*", NAME="$env{INTERFACE_NEW}"
+
+LABEL="persistent_net_generator_end"
diff --git a/rules.d/meson.build b/rules.d/meson.build
index 20fca222da..736430a5b8 100644
--- a/rules.d/meson.build
+++ b/rules.d/meson.build
@@ -25,6 +25,7 @@ rules = [
                '70-mouse.rules',
                '70-touchpad.rules',
                '75-net-description.rules',
+               '75-persistent-net-generator.rules',
                '75-probe_mtd.rules',
                '78-sound-card.rules',
                '80-net-setup-link.rules',
diff --git a/src/libsystemd/sd-netlink/netlink-util.c b/src/libsystemd/sd-netlink/netlink-util.c
index 81e0d7aa05..56d95f52ab 100644
--- a/src/libsystemd/sd-netlink/netlink-util.c
+++ b/src/libsystemd/sd-netlink/netlink-util.c
@@ -11,7 +11,8 @@
 #include "process-util.h"
 #include "strv.h"
 
-static int set_link_name(sd_netlink **rtnl, int ifindex, const char *name) {
+
+static int do_set_link_name(sd_netlink **rtnl, int ifindex, const char *name) {
         _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL;
         int r;
 
@@ -31,6 +32,46 @@ static int set_link_name(sd_netlink **rtnl, int ifindex, const char *name) {
         return sd_netlink_call(*rtnl, message, 0, NULL);
 }
 
+static int do_set_link_name_wait(sd_netlink **rtnl, int ifindex, const char *name) {
+        char tmp[IFNAMSIZ];
+        int r;
+
+        log_debug("ifindex %i: waiting for name %s to be released", ifindex, name);
+
+        /* free our own name, another process may wait for us */
+        snprintf(tmp, IFNAMSIZ, "rename%d", ifindex);
+        r = do_set_link_name(rtnl, ifindex, tmp);
+        if (r < 0)
+                  return r;
+
+        log_debug("ifindex %i: while waiting, renamed to %s to release our own name", ifindex, tmp);
+
+        /* wait a maximum of 90 seconds for our target to become available */
+        for(int loop = 90 * 20; loop; loop--) {
+                const struct timespec duration = { 0, 1000 * 1000 * 1000 / 20 };
+
+                nanosleep(&duration, NULL);
+
+                r = do_set_link_name(rtnl, ifindex, name);
+                if (r >= 0)
+                        break;
+                if (r != -EEXIST)
+                        break;
+        }
+
+        return r;
+}
+
+static int set_link_name(sd_netlink **rtnl, int ifindex, const char *name) {
+        int r;
+
+        r = do_set_link_name(rtnl, ifindex, name);
+        if (r >= 0 || r != -EEXIST)
+                return r;
+
+        return do_set_link_name_wait(rtnl, ifindex, name);
+}
+
 int rtnl_set_link_name(sd_netlink **rtnl, int ifindex, const char *name, char* const *alternative_names) {
         _cleanup_strv_free_ char **original_altnames = NULL, **new_altnames = NULL;
         bool altname_deleted = false;
diff --git a/src/udev/meson.build b/src/udev/meson.build
index de988ac248..4704a4023d 100644
--- a/src/udev/meson.build
+++ b/src/udev/meson.build
@@ -222,3 +222,7 @@ tests += [
                 'base' : test_libudev_base,
         },
 ]
+
+install_data('rule_generator/rule_generator.functions',
+             'rule_generator/write_net_rules',
+             install_dir : udevlibexecdir)
diff --git a/src/udev/rule_generator/rule_generator.functions b/src/udev/rule_generator/rule_generator.functions
new file mode 100644
index 0000000000..087bc34ad3
--- /dev/null
+++ b/src/udev/rule_generator/rule_generator.functions
@@ -0,0 +1,113 @@
+# functions used by the udev rule generator
+
+# Copyright (C) 2006 Marco d'Itri <md@Linux.IT>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+PATH='/usr/bin:/bin:/usr/sbin:/sbin'
+
+# Read a single line from file $1 in the $DEVPATH directory.
+# The function must not return an error even if the file does not exist.
+sysread() {
+        local file="$1"
+        [ -e "/sys$DEVPATH/$file" ] || return 0
+        local value
+        read value < "/sys$DEVPATH/$file" || return 0
+        echo "$value"
+}
+
+sysreadlink() {
+        local file="$1"
+        [ -e "/sys$DEVPATH/$file" ] || return 0
+        readlink -f /sys$DEVPATH/$file 2> /dev/null || true
+}
+
+# Return true if a directory is writeable.
+writeable() {
+        if ln -s test-link $1/.is-writeable 2> /dev/null; then
+                rm -f $1/.is-writeable
+                return 0
+        else
+                return 1
+        fi
+}
+
+# Create a lock file for the current rules file.
+lock_rules_file() {
+        RUNDIR="/run/udev"
+        [ -e "$RUNDIR" ] || return 0
+
+        RULES_LOCK="$RUNDIR/.lock-${RULES_FILE##*/}"
+
+        retry=30
+        while ! mkdir $RULES_LOCK 2> /dev/null; do
+                if [ $retry -eq 0 ]; then
+                         echo "Cannot lock $RULES_FILE!" >&2
+                         exit 2
+                fi
+                sleep 1
+                retry=$(($retry - 1))
+        done
+}
+
+unlock_rules_file() {
+        [ "$RULES_LOCK" ] || return 0
+        rmdir $RULES_LOCK || true
+}
+
+# Choose the real rules file if it is writeable or a temporary file if not.
+# Both files should be checked later when looking for existing rules.
+choose_rules_file() {
+        RUNDIR="/run/udev"
+        local tmp_rules_file="$RUNDIR/tmp-rules--${RULES_FILE##*/}"
+        [ -e "$RULES_FILE" -o -e "$tmp_rules_file" ] || PRINT_HEADER=1
+
+        if writeable ${RULES_FILE%/*}; then
+                RO_RULES_FILE='/dev/null'
+        else
+                RO_RULES_FILE=$RULES_FILE
+                RULES_FILE=$tmp_rules_file
+        fi
+}
+
+# Return the name of the first free device.
+raw_find_next_available() {
+        local links="$1"
+
+        local basename=${links%%[ 0-9]*}
+        local max=-1
+        for name in $links; do
+                local num=${name#$basename}
+                [ "$num" ] || num=0
+                [ $num -gt $max ] && max=$num
+        done
+
+        local max=$(($max + 1))
+        # "name0" actually is just "name"
+        [ $max -eq 0 ] && return
+        echo "$max"
+}
+
+# Find all rules matching a key (with action) and a pattern.
+find_all_rules() {
+        local key="$1"
+        local linkre="$2"
+        local match="$3"
+
+        local search='.*[[:space:],]'"$key"'"('"$linkre"')".*'
+        echo $(sed -n -r -e 's/^#.*//' -e "${match}s/${search}/\1/p" \
+                $RO_RULES_FILE \
+                $([ -e $RULES_FILE ] && echo $RULES_FILE) \
+                2>/dev/null)
+}
diff --git a/src/udev/rule_generator/write_net_rules b/src/udev/rule_generator/write_net_rules
new file mode 100755
index 0000000000..27be878af9
--- /dev/null
+++ b/src/udev/rule_generator/write_net_rules
@@ -0,0 +1,153 @@
+#!/bin/sh -e
+
+# This script is run to create persistent network device naming rules
+# based on properties of the device.
+# If the interface needs to be renamed, INTERFACE_NEW=<name> will be printed
+# on stdout to allow udev to IMPORT it.
+
+# variables used to communicate:
+#   MATCHADDR             MAC address used for the match
+#   MATCHID               bus_id used for the match
+#   MATCHDEVID            dev_id used for the match
+#   MATCHDRV              driver name used for the match
+#   MATCHIFTYPE           interface type match
+#   COMMENT               comment to add to the generated rule
+#   INTERFACE_NAME        requested name supplied by external tool
+#   INTERFACE_NEW         new interface name returned by rule writer
+
+# Copyright (C) 2006 Marco d'Itri <md@Linux.IT>
+# Copyright (C) 2007 Kay Sievers <kay.sievers@vrfy.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# debug, if UDEV_LOG=<debug>
+if [ -n "$UDEV_LOG" ]; then
+        if [ "$UDEV_LOG" -ge 7 ]; then
+                set -x
+        fi
+fi
+
+RULES_FILE='/etc/udev/rules.d/70-persistent-net.rules'
+
+. /usr/lib/udev/rule_generator.functions
+
+interface_name_taken() {
+        local value="$(find_all_rules 'NAME=' $INTERFACE)"
+        if [ "$value" ]; then
+                return 0
+        else
+                return 1
+        fi
+}
+
+find_next_available() {
+        raw_find_next_available "$(find_all_rules 'NAME=' "$1")"
+}
+
+write_rule() {
+        local match="$1"
+        local name="$2"
+        local comment="$3"
+
+        {
+        if [ "$PRINT_HEADER" ]; then
+                PRINT_HEADER=
+                echo "# This file was automatically generated by the $0"
+                echo "# program, run by the persistent-net-generator.rules rules file."
+                echo "#"
+                echo "# You can modify it, as long as you keep each rule on a single"
+                echo "# line, and change only the value of the NAME= key."
+        fi
+
+        echo ""
+        [ "$comment" ] && echo "# $comment"
+        echo "SUBSYSTEM==\"net\", ACTION==\"add\"$match, NAME=\"$name\""
+        } >> $RULES_FILE
+}
+
+if [ -z "$INTERFACE" ]; then
+        echo "missing \$INTERFACE" >&2
+        exit 1
+fi
+
+# Prevent concurrent processes from modifying the file at the same time.
+lock_rules_file
+
+# Check if the rules file is writeable.
+choose_rules_file
+
+# the DRIVERS key is needed to not match bridges and VLAN sub-interfaces
+if [ "$MATCHADDR" ]; then
+        # Check if MACADDR doesn't exist already in the generated rules
+        MAC="$(/usr/bin/grep -w -o -C1 -m1 "$MATCHADDR" "$RULES_FILE" 2>/dev/null || true)"
+        if [ "$MAC" = "$MATCHADDR" ]; then
+                unlock_rules_file
+                exit 0
+        fi
+        match="$match, DRIVERS==\"?*\", ATTR{address}==\"$MATCHADDR\""
+fi
+
+if [ "$MATCHDRV" ]; then
+        match="$match, DRIVERS==\"$MATCHDRV\""
+fi
+
+if [ "$MATCHDEVID" ]; then
+        match="$match, ATTR{dev_id}==\"$MATCHDEVID\""
+fi
+
+if [ "$MATCHID" ]; then
+        # Check if KERNEL doesn't exist already in the generated rules
+        KERNEL="$(find_all_rules 'KERNELS==' "$MATCHID")"
+        if [ "$KERNEL" = "$MATCHID" ]; then
+                unlock_rules_file
+                exit 0
+        fi
+        match="$match, KERNELS==\"$MATCHID\""
+fi
+
+if [ "$MATCHIFTYPE" ]; then
+        match="$match, ATTR{type}==\"$MATCHIFTYPE\""
+fi
+
+if [ -z "$match" ]; then
+        echo "missing valid match" >&2
+        unlock_rules_file
+        exit 1
+fi
+
+basename=${INTERFACE%%[0-9]*}
+match="$match, KERNEL==\"$basename*\""
+
+if [ "$INTERFACE_NAME" ]; then
+        # external tools may request a custom name
+        COMMENT="$COMMENT (custom name provided by external tool)"
+        if [ "$INTERFACE_NAME" != "$INTERFACE" ]; then
+                INTERFACE=$INTERFACE_NAME;
+                echo "INTERFACE_NEW=$INTERFACE"
+        fi
+else
+        # if a rule using the current name already exists, find a new name
+        if interface_name_taken; then
+                INTERFACE="$basename$(find_next_available "$basename[0-9]*")"
+                # prevent INTERFACE from being "eth" instead of "eth0"
+                [ "$INTERFACE" = "${INTERFACE%%[ \[\]0-9]*}" ] && INTERFACE=${INTERFACE}0
+                echo "INTERFACE_NEW=$INTERFACE"
+        fi
+fi
+
+write_rule "$match" "$INTERFACE" "$COMMENT"
+
+unlock_rules_file
+
+exit 0
-- 
2.43.0

