network-generator: support BOOTIF= and rd.bootif=0 options (#41028)

The network generator currently supports many of the options described
by dracut.cmdline(7), but not everything.

This commit adds support for the BOOTIF= option (and the related
rd.bootif= option) used in PXE setups.

This is implemented by treating BOOTIF as a special name/placeholder
when used as an interface name, and expecting a MAC address to be set in
the BOOTIF= parameter. The resulting .network file then uses MACAddress=
in the [Match] section, instead of Name=.
This commit is contained in:
Lennart Poettering
2026-04-02 16:17:11 +02:00
committed by GitHub
8 changed files with 231 additions and 25 deletions
+18 -1
View File
@@ -100,10 +100,27 @@
</listitem>
</varlistentry>
<varlistentry>
<term><varname>BOOTIF=</varname></term>
<term><varname>rd.bootif=</varname></term>
<listitem>
<para>When <varname>BOOTIF</varname> is specified in the interface field of <varname>ip=</varname>, it is treated
as a special placeholder rather than a real interface name. Then, in combination with the MAC address provided
by <varname>BOOTIF=</varname>, this is translated into a
<citerefentry><refentrytitle>systemd.network</refentrytitle><manvolnum>5</manvolnum></citerefentry> file
which matches on the the provided MAC address. When <varname>rd.bootif=0</varname> is passed, this functionality
is disabled, and the <varname>BOOTIF=</varname> option is ignored.</para>
<para>See <citerefentry project='man-pages'><refentrytitle>dracut.cmdline</refentrytitle><manvolnum>7</manvolnum></citerefentry>
for details on the usage of these options.</para>
<xi:include href="version-info.xml" xpointer="v261"/>
</listitem>
</varlistentry>
<!-- unsupported:
team=<teammaster>:<teamslaves>
bootdev=
BOOTIF=
bootdev=
bootdev=
bootdev=
+24
View File
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <net/ethernet.h>
#include <net/if_arp.h>
#include <stdio.h>
#include "ether-addr-util.h"
@@ -68,6 +69,29 @@ bool hw_addr_is_null(const struct hw_addr_data *addr) {
return addr->length == 0 || memeqzero(addr->bytes, addr->length);
}
bool hw_addr_is_valid(const struct hw_addr_data *addr, uint16_t iftype) {
assert(addr);
switch (iftype) {
case ARPHRD_ETHER:
/* Refuse all zero and all 0xFF. */
if (addr->length != ETH_ALEN)
return false;
return !ether_addr_is_null(&addr->ether) && !ether_addr_is_broadcast(&addr->ether);
case ARPHRD_INFINIBAND:
/* The last 8 bytes cannot be zero. */
if (addr->length != INFINIBAND_ALEN)
return false;
return !memeqzero(addr->bytes + INFINIBAND_ALEN - 8, 8);
default:
return false;
}
}
DEFINE_HASH_OPS(hw_addr_hash_ops, struct hw_addr_data, hw_addr_hash_func, hw_addr_compare);
DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR(hw_addr_hash_ops_free, struct hw_addr_data, hw_addr_hash_func, hw_addr_compare, free);
+1
View File
@@ -57,6 +57,7 @@ static inline bool hw_addr_equal(const struct hw_addr_data *a, const struct hw_a
return hw_addr_compare(a, b) == 0;
}
bool hw_addr_is_null(const struct hw_addr_data *addr) _pure_;
bool hw_addr_is_valid(const struct hw_addr_data *addr, uint16_t iftype);
extern const struct hash_ops hw_addr_hash_ops;
extern const struct hash_ops hw_addr_hash_ops_free;
@@ -233,6 +233,8 @@ static int run(int argc, char *argv[]) {
}
}
context_finalize_bootif(&context);
r = context_merge_networks(&context);
if (r < 0)
return log_warning_errno(r, "Failed to merge multiple command line options: %m");
+95 -2
View File
@@ -1,5 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <net/if_arp.h>
#include "alloc-util.h"
#include "extract-word.h"
@@ -25,6 +26,8 @@
rd.route=<net>/<netmask>:<gateway>[:<interface>]
nameserver=<IP> [nameserver=<IP> ...]
rd.peerdns=0
BOOTIF=<MAC>
rd.bootif=0 # Causes BOOTIF= to be ignored.
# .link
ifname=<interface>:<MAC>
@@ -38,8 +41,6 @@
# ignored
bootdev=<interface>
BOOTIF=<MAC>
rd.bootif=0
biosdevname=0
rd.neednet=1
*/
@@ -515,6 +516,40 @@ static int network_set_mac_address(Context *context, const char *ifname, const c
return 0;
}
static int network_set_bootif_mac_address(Context *context, const char *mac) {
int r;
assert(context);
if (isempty(mac))
return 0;
/* "BOOTIF" is a special placeholder interface name, used to configure the
* interface referred to by BOOTIF=. I.e., ip=...:BOOTIF:... is valid if and
* only if BOOTIF= is also set. */
Network *network;
r = network_acquire(context, "BOOTIF", &network);
if (r < 0)
return log_debug_errno(r, "Failed to acquire network for BOOTIF: %m");
r = parse_hw_addr(mac, &network->match_mac);
if (r < 0) {
/* PXE bootloaders may provide the MAC with a "hardware type prefix", e.g.
* 01-12:34:56:78:90:ab, where 01 indicates ethernet. Technically, other
* hardware types are possible, but only ethernet is handled here. */
const char *p = startswith(mac, "01-");
if (p)
r = parse_hw_addr(p, &network->match_mac);
}
if (r < 0)
return log_debug_errno(r, "Invalid MAC address '%s' for BOOTIF", mac);
if (!hw_addr_is_valid(&network->match_mac, ARPHRD_ETHER))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid MAC address '%s' for BOOTIF", mac);
return 0;
}
static int network_set_address(
Context *context,
const char *ifname,
@@ -1251,6 +1286,38 @@ static int parse_cmdline_ifname_policy(Context *context, const char *key, const
return 0;
}
static int parse_cmdline_rd_bootif(Context *context, const char *key, const char *value) {
int r;
assert(context);
assert(key);
/* rd.bootif=<bool> */
if (proc_cmdline_value_missing(key, value))
return 0;
r = value ? parse_boolean(value) : true;
if (r < 0)
return log_debug_errno(r, "Invalid boolean value '%s'", value);
/* rd.bootif=0 => skip BOOTIF= parsing */
context->skip_bootif = !r;
return 0;
}
static int parse_cmdline_bootif_mac(Context *context, const char *key, const char *value) {
assert(context);
assert(key);
/* BOOTIF=<MAC> */
if (proc_cmdline_value_missing(key, value))
return 0;
return network_set_bootif_mac_address(context, value);
}
int parse_cmdline_item(const char *key, const char *value, void *data) {
Context *context = ASSERT_PTR(data);
@@ -1274,10 +1341,34 @@ int parse_cmdline_item(const char *key, const char *value, void *data) {
return parse_cmdline_ifname(context, key, value);
if (proc_cmdline_key_streq(key, "net.ifname_policy"))
return parse_cmdline_ifname_policy(context, key, value);
if (proc_cmdline_key_streq(key, "rd.bootif"))
return parse_cmdline_rd_bootif(context, key, value);
if (proc_cmdline_key_streq(key, "BOOTIF"))
return parse_cmdline_bootif_mac(context, key, value);
return 0;
}
void context_finalize_bootif(Context *context) {
assert(context);
Network *network = hashmap_get(context->networks_by_name, "BOOTIF");
if (!network)
return;
/* rd.bootif=0 disables BOOTIF= handling */
if (context->skip_bootif) {
network_free(hashmap_remove_value(context->networks_by_name, "BOOTIF", network));
return;
}
if (hw_addr_is_null(&network->match_mac)) {
log_debug("Expected MAC address for BOOTIF, but BOOTIF= is unset.");
network_free(hashmap_remove_value(context->networks_by_name, "BOOTIF", network));
return;
}
}
int context_merge_networks(Context *context) {
Network *all, *network;
int r;
@@ -1369,6 +1460,8 @@ void network_dump(Network *network, FILE *f) {
* physical interfaces. */
fputs("Kind=!*\n"
"Type=!loopback\n", f);
else if (streq(network->ifname, "BOOTIF"))
fprintf(f, "MACAddress=%s\n", HW_ADDR_TO_STR(&network->match_mac));
else
fprintf(f, "Name=%s\n", network->ifname);
@@ -54,6 +54,8 @@ struct Route {
struct Network {
/* [Match] */
char *ifname;
/* Parsed from BOOTIF= parameter. */
struct hw_addr_data match_mac;
/* [Link] */
struct ether_addr mac;
@@ -101,11 +103,15 @@ typedef struct Context {
Hashmap *networks_by_name;
Hashmap *netdevs_by_name;
Hashmap *links_by_filename;
/* If rd.bootif=0, ignore BOOTIF= parsing */
bool skip_bootif;
} Context;
int parse_cmdline_item(const char *key, const char *value, void *data);
int context_merge_networks(Context *context);
void context_clear(Context *context);
void context_finalize_bootif(Context *context);
Network *network_get(Context *context, const char *ifname);
void network_dump(Network *network, FILE *f);
@@ -28,12 +28,39 @@ static void test_network_two(const char *ifname,
ASSERT_OK(parse_cmdline_item(key1, value1, &context));
ASSERT_OK(parse_cmdline_item(key2, value2, &context));
context_finalize_bootif(&context);
ASSERT_OK(context_merge_networks(&context));
ASSERT_NOT_NULL((network = network_get(&context, ifname)));
ASSERT_OK(network_format(network, &output));
ASSERT_STREQ(output, expected);
}
static void test_network_three(const char *ifname,
const char *key1, const char *value1,
const char *key2, const char *value2,
const char *key3, const char *value3,
const char *expected) {
_cleanup_(context_clear) Context context = {};
_cleanup_free_ char *output = NULL;
Network *network;
log_debug("/* %s(%s=%s, %s=%s, %s=%s) */", __func__, key1, value1, key2, value2, key3, value3);
ASSERT_OK(parse_cmdline_item(key1, value1, &context));
ASSERT_OK(parse_cmdline_item(key2, value2, &context));
ASSERT_OK(parse_cmdline_item(key3, value3, &context));
context_finalize_bootif(&context);
ASSERT_OK(context_merge_networks(&context));
network = network_get(&context, ifname);
if (expected) {
ASSERT_NOT_NULL(network);
ASSERT_OK(network_format(network, &output));
ASSERT_STREQ(output, expected);
} else
ASSERT_NULL(network);
}
static void test_netdev_one(const char *ifname, const char *key, const char *value, const char *expected) {
_cleanup_(context_clear) Context context = {};
_cleanup_free_ char *output = NULL;
@@ -573,5 +600,61 @@ int main(int argc, char *argv[]) {
"Gateway=192.168.0.1\n"
);
test_network_two("BOOTIF",
"ip", "::::hogehoge:BOOTIF:dhcp",
"BOOTIF", "01-00:11:22:33:44:55",
"[Match]\n"
"MACAddress=00:11:22:33:44:55\n"
"\n[Link]\n"
"\n[Network]\n"
"DHCP=ipv4\n"
"\n[DHCP]\n"
"Hostname=hogehoge\n"
);
test_network_two("BOOTIF",
"ip", "::::hogehoge:BOOTIF:dhcp",
"BOOTIF", "00:11:22:33:44:55",
"[Match]\n"
"MACAddress=00:11:22:33:44:55\n"
"\n[Link]\n"
"\n[Network]\n"
"DHCP=ipv4\n"
"\n[DHCP]\n"
"Hostname=hogehoge\n"
);
test_network_two("BOOTIF",
"ip", "::::hogehoge:BOOTIF:dhcp",
"BOOTIF", "01-00-11-22-33-44-55",
"[Match]\n"
"MACAddress=00:11:22:33:44:55\n"
"\n[Link]\n"
"\n[Network]\n"
"DHCP=ipv4\n"
"\n[DHCP]\n"
"Hostname=hogehoge\n"
);
test_network_three("BOOTIF",
"ip", "::::hogehoge:BOOTIF:dhcp",
"BOOTIF", "01-00:11:22:33:44:55",
"rd.bootif", "1",
"[Match]\n"
"MACAddress=00:11:22:33:44:55\n"
"\n[Link]\n"
"\n[Network]\n"
"DHCP=ipv4\n"
"\n[DHCP]\n"
"Hostname=hogehoge\n"
);
test_network_three("BOOTIF",
"ip", "::::hogehoge:BOOTIF:dhcp",
"BOOTIF", "01-00:11:22:33:44:55",
"rd.bootif", "0",
NULL
);
return 0;
}
+2 -22
View File
@@ -558,26 +558,6 @@ static int link_apply_ethtool_settings(Link *link, int *ethtool_fd) {
return 0;
}
static bool hw_addr_is_valid(Link *link, const struct hw_addr_data *hw_addr) {
assert(link);
assert(hw_addr);
switch (link->iftype) {
case ARPHRD_ETHER:
/* Refuse all zero and all 0xFF. */
assert(hw_addr->length == ETH_ALEN);
return !ether_addr_is_null(&hw_addr->ether) && !ether_addr_is_broadcast(&hw_addr->ether);
case ARPHRD_INFINIBAND:
/* The last 8 bytes cannot be zero. */
assert(hw_addr->length == INFINIBAND_ALEN);
return !memeqzero(hw_addr->bytes + INFINIBAND_ALEN - 8, 8);
default:
assert_not_reached();
}
}
static int link_generate_new_hw_addr(Link *link, struct hw_addr_data *ret) {
struct hw_addr_data hw_addr = HW_ADDR_NULL;
bool is_static = false;
@@ -647,7 +627,7 @@ static int link_generate_new_hw_addr(Link *link, struct hw_addr_data *ret) {
* systems booting up at the very same time. */
for (;;) {
random_bytes(p, len);
if (hw_addr_is_valid(link, &hw_addr))
if (hw_addr_is_valid(&hw_addr, link->iftype))
break;
}
@@ -662,7 +642,7 @@ static int link_generate_new_hw_addr(Link *link, struct hw_addr_data *ret) {
assert(len <= sizeof(result));
memcpy(p, &result, len);
if (!hw_addr_is_valid(link, &hw_addr))
if (!hw_addr_is_valid(&hw_addr, link->iftype))
return log_link_warning_errno(link, SYNTHETIC_ERRNO(EINVAL),
"Could not generate valid persistent MAC address.");
}